mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 00:44:07 +01:00
fix(context-bill): --exact reads only the file each walk approved
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
c7ddca62db
commit
fd4809e0e1
2 files changed
+86
-5
No files matched your search
+35
-5
@@ -815,9 +815,15 @@ async function countTokens(text: string, { model, apiKey, fetchImpl }: CountToke
|
||||
return json.input_tokens;
|
||||
}
|
||||
|
||||
export interface ExactFile extends WalkedFile {
|
||||
/** The walked file's identity; --exact reads only that same file. */
|
||||
dev: number;
|
||||
ino: number;
|
||||
}
|
||||
|
||||
export interface ExactSendSet {
|
||||
/** Each real file once, under the first lexical path a skill's walk reaches it by. */
|
||||
files: WalkedFile[];
|
||||
files: ExactFile[];
|
||||
/** Realpaths of the SKILL.md files whose frontmatter the bill prices. */
|
||||
frontmatter: Set<string>;
|
||||
}
|
||||
@@ -833,7 +839,7 @@ export interface ExactSendSet {
|
||||
export function exactSendSet(root: string): ExactSendSet {
|
||||
const resolved = path.resolve(root);
|
||||
if (!fs.existsSync(resolved)) throw new Error(`No such tree: ${resolved}`);
|
||||
const files: WalkedFile[] = [];
|
||||
const files: ExactFile[] = [];
|
||||
const frontmatter = new Set<string>();
|
||||
const seen = new Set<string>();
|
||||
for (const dir of findSkillDirs(resolved)) {
|
||||
@@ -841,12 +847,36 @@ export function exactSendSet(root: string): ExactSendSet {
|
||||
for (const file of walkMdFiles(dir, { billedRoot: resolved, skipNestedSkills: true })) {
|
||||
if (seen.has(file.real)) continue;
|
||||
seen.add(file.real);
|
||||
files.push(file);
|
||||
const { dev, ino } = fs.statSync(file.real);
|
||||
files.push({ ...file, dev, ino });
|
||||
}
|
||||
}
|
||||
return { files, frontmatter };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the exact file the walk approved: its realpath, opened without
|
||||
* following a final link, and only if it is still the same regular file. A
|
||||
* link retargeted or a file swapped for a link after the walk is refused, so
|
||||
* nothing the bill did not walk can be sent.
|
||||
*/
|
||||
function readWalkedFile(real: string, dev: number, ino: number, file: string, root: string): string {
|
||||
const changed = () => new Error(`${file} changed after it was walked; nothing from ${root} was sent. Run again.`);
|
||||
let fd: number;
|
||||
try {
|
||||
fd = fs.openSync(real, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
||||
} catch {
|
||||
throw changed();
|
||||
}
|
||||
try {
|
||||
const st = fs.fstatSync(fd);
|
||||
if (!st.isFile() || st.dev !== dev || st.ino !== ino) throw changed();
|
||||
return fs.readFileSync(fd, "utf8");
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ExactMeasurement {
|
||||
tokenSource: string;
|
||||
tokensOf: TokensOf;
|
||||
@@ -894,8 +924,8 @@ export async function measureExactTokens(
|
||||
// real file back to the one key that was measured.
|
||||
const lexicalOf = new Map<string, string>();
|
||||
const { files, frontmatter } = sendSet ?? exactSendSet(root);
|
||||
for (const { path: file, real } of files) {
|
||||
const text = fs.readFileSync(real, "utf8");
|
||||
for (const { path: file, real, dev, ino } of files) {
|
||||
const text = readWalkedFile(real, dev, ino, file, root);
|
||||
texts.set(file, text);
|
||||
lexicalOf.set(real, file);
|
||||
if (!frontmatter.has(real)) continue;
|
||||
|
||||
@@ -1059,6 +1059,57 @@ describe("--exact (opt-in measurement; offline here via an injected fetch)", ()
|
||||
expect(calls.length).toBe(4);
|
||||
});
|
||||
|
||||
it("sends only the files the walk approved, even when links or files change during --diff", async () => {
|
||||
if (process.platform === "win32") return;
|
||||
const run = async (swap: (store: string, qa: string) => void) => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "context-bill-swap-"));
|
||||
for (const tree of ["a", "b"]) {
|
||||
fs.mkdirSync(path.join(tmp, tree, "qa"), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmp, tree, "qa", "SKILL.md"), "---\nname: qa\ndescription: d\n---\n# qa\n");
|
||||
}
|
||||
const store = path.join(tmp, "store");
|
||||
fs.mkdirSync(store);
|
||||
fs.writeFileSync(path.join(store, "safe.md"), "safe");
|
||||
fs.writeFileSync(path.join(store, "secret.env"), "SYNTHETIC_PRIVATE_NONMARKDOWN");
|
||||
const qa = path.join(tmp, "b", "qa");
|
||||
fs.symlinkSync(path.join(store, "safe.md"), path.join(qa, "ref.md"));
|
||||
const calls: { body: string }[] = [];
|
||||
const inner = fakeFetch(calls) as unknown as (u: string, i: { body: string }) => Promise<unknown>;
|
||||
let swapped = false;
|
||||
const fetchImpl = (async (u: string, i: { body: string }) => {
|
||||
if (!swapped) {
|
||||
swapped = true;
|
||||
swap(store, qa);
|
||||
}
|
||||
return inner(u, i);
|
||||
}) as never;
|
||||
const err = capture();
|
||||
const code = await contextBillMain(["--diff", path.join(tmp, "a"), path.join(tmp, "b"), "--exact", "--json"], {
|
||||
stdout: capture().stream,
|
||||
stderr: err.stream,
|
||||
apiKey: "sk-test",
|
||||
fetchImpl,
|
||||
egressHome,
|
||||
});
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
return { code, calls, err: err.text() };
|
||||
};
|
||||
|
||||
const retargeted = await run((store, qa) => {
|
||||
fs.unlinkSync(path.join(qa, "ref.md"));
|
||||
fs.symlinkSync(path.join(store, "secret.env"), path.join(qa, "ref.md"));
|
||||
});
|
||||
expect(retargeted.calls.some((c) => c.body.includes("SYNTHETIC_PRIVATE"))).toBe(false);
|
||||
|
||||
const replaced = await run((store) => {
|
||||
fs.unlinkSync(path.join(store, "safe.md"));
|
||||
fs.symlinkSync(path.join(store, "secret.env"), path.join(store, "safe.md"));
|
||||
});
|
||||
expect(replaced.calls.some((c) => c.body.includes("SYNTHETIC_PRIVATE"))).toBe(false);
|
||||
expect(replaced.code).toBe(1);
|
||||
expect(replaced.err).toContain("changed after it was walked");
|
||||
});
|
||||
|
||||
it("names a missing tree, without a raw ENOENT or any request", async () => {
|
||||
const parent = fs.mkdtempSync(path.join(os.tmpdir(), "context-bill-missing-"));
|
||||
const out = capture();
|
||||
|
||||
Reference in new issue
Block a user