mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 06:28:59 +02:00
fix(memory-ingest): store the normalized git_remote so unattributed pages hit the policy filter
buildTranscriptPage wrote the normalized '_unattributed' sentinel into the page FRONTMATTER but stored the raw resolved remote ('' when unresolvable) on the page object. The policy filter fast-paths !p.git_remote, so under --include-unattributed an explicit '_unattributed → deny' (or read-only) policy never applied to exactly the pages it names — they ingested unpoliced. The stored value now matches the frontmatter.
Regression test (fails on v1.68.3.0): seeds the REAL bin/gstack-gbrain-repo-policy store with '_unattributed → deny' through its own set verb, ingests an unresolvable-remote session with --include-unattributed, and asserts nothing reaches gbrain — pre-fix the '' remote bypassed the filter and the import ran. A fake echoing tiers would pass on both sides of the fix; the real helper prints 'none' for unknown keys, so only a genuinely applied deny distinguishes the two.
Fixes #2353
This commit is contained in:
@@ -788,7 +788,11 @@ function buildTranscriptPage(path: string, session: ParsedSession): PageRecord {
|
||||
source_path: path,
|
||||
session_id: session.session_id,
|
||||
cwd: session.cwd,
|
||||
git_remote: remote,
|
||||
// Store the normalized sentinel, matching the frontmatter above: a raw ""
|
||||
// is falsy and slid through the policy filter's !p.git_remote fast-path,
|
||||
// so under --include-unattributed a `_unattributed → deny` policy never
|
||||
// applied to exactly the pages it names (#2353).
|
||||
git_remote: remote || "_unattributed",
|
||||
start_time: session.start_time,
|
||||
end_time: session.end_time,
|
||||
partial: session.partial,
|
||||
|
||||
@@ -484,6 +484,44 @@ describe("gstack-memory-ingest writer (gbrain v0.20+ batch `import` interface)",
|
||||
expect(stagedList).toMatch(/^\.\/transcripts\/claude-code\/.+\.md$/m);
|
||||
});
|
||||
|
||||
// #2353: buildTranscriptPage stored the RAW resolved remote ("" when
|
||||
// unresolvable) while the frontmatter wrote the normalized "_unattributed".
|
||||
// The policy filter fast-paths !p.git_remote, so under --include-unattributed
|
||||
// a `_unattributed → deny` policy never applied to exactly the pages it
|
||||
// names. Uses the REAL bin/gstack-gbrain-repo-policy (resolved by the client
|
||||
// relative to lib/, and seeded here through its own `set` verb) — a fake
|
||||
// echoing tiers would pass on both sides of the fix.
|
||||
it("a deny policy keyed _unattributed applies to unattributable transcripts (#2353)", () => {
|
||||
const home = makeTestHome();
|
||||
const gstackHome = join(home, ".gstack");
|
||||
mkdirSync(gstackHome, { recursive: true });
|
||||
const { binDir, logFile } = installFakeGbrain(home);
|
||||
|
||||
const POLICY = join(import.meta.dir, "..", "bin", "gstack-gbrain-repo-policy");
|
||||
const seeded = spawnSync("bash", [POLICY, "set", "_unattributed", "deny"], {
|
||||
encoding: "utf-8",
|
||||
env: { ...process.env, HOME: home, GSTACK_HOME: gstackHome },
|
||||
});
|
||||
expect(seeded.status).toBe(0);
|
||||
expect(existsSync(join(gstackHome, "gbrain-repo-policy.json"))).toBe(true);
|
||||
|
||||
const session =
|
||||
`{"type":"user","message":{"role":"user","content":"hi"},"timestamp":"2026-05-01T00:00:00Z","cwd":"/tmp/foo"}\n` +
|
||||
`{"type":"assistant","message":{"role":"assistant","content":"hello"},"timestamp":"2026-05-01T00:00:01Z"}\n`;
|
||||
writeClaudeCodeSession(home, "tmp-foo", "abc123", session);
|
||||
|
||||
const r = runScript(["--bulk", "--include-unattributed", "--quiet"], {
|
||||
HOME: home,
|
||||
GSTACK_HOME: gstackHome,
|
||||
PATH: `${binDir}:${process.env.PATH || ""}`,
|
||||
});
|
||||
|
||||
// The only candidate page is policy-denied, so nothing may reach gbrain:
|
||||
// pre-fix, the "" remote bypassed the filter and gbrain import ran.
|
||||
expect(r.exitCode).toBe(0);
|
||||
expect(existsSync(logFile)).toBe(false);
|
||||
});
|
||||
|
||||
// Silent-data-loss regression: gbrain accepts the import call, exits 0, and
|
||||
// reports imported=0 because collect_files found nothing in the staging dir
|
||||
// (real-world cause: gstack-artifacts-init writes `.gitignore = "*"` into
|
||||
|
||||
Reference in New Issue
Block a user