fix(memory-ingest): apply the _unattributed fallback to page.git_remote (#2353)

buildTranscriptPage wrote the raw resolveGitRemote() result into
page.git_remote while the frontmatter and repoSlug both applied the
"_unattributed" fallback. The skip guard reads page.git_remote, so
`"" === "_unattributed"` was never true and --include-unattributed
was inert: every session whose repo could not be resolved got imported
regardless of the flag, and skipped_unattributed always reported 0.

buildArtifactPage already stores the defaulted value (slug_repo); this
brings the transcript builder in line.

Adds three regression tests: non-git cwd is skipped by default, imported
with --include-unattributed, and a cwd that does resolve is unaffected.
This commit is contained in:
Lin Junrong 2026-07-28 15:07:03 +08:00
parent a3259400a3
commit deb2b9f656
2 changed files with 62 additions and 1 deletions

View File

@ -744,7 +744,10 @@ function buildTranscriptPage(path: string, session: ParsedSession): PageRecord {
source_path: path,
session_id: session.session_id,
cwd: session.cwd,
git_remote: remote,
// Apply the same "_unattributed" fallback the frontmatter and repoSlug use.
// The unattributed skip guard reads this field, so leaving the raw "" here
// makes `--include-unattributed` inert (`"" === "_unattributed"` is never true).
git_remote: remote || "_unattributed",
start_time: session.start_time,
end_time: session.end_time,
partial: session.partial,

View File

@ -296,6 +296,64 @@ describe("internal: parseTranscriptJsonl + buildTranscriptPage shape", () => {
});
});
// ── unattributed skip policy ───────────────────────────────────────────────
describe("gstack-memory-ingest unattributed skip", () => {
function sessionIn(cwd: string): string {
return (
`{"type":"user","message":{"role":"user","content":"hi"},"timestamp":"2026-05-01T00:00:00Z","cwd":"${cwd}"}\n` +
`{"type":"assistant","message":{"role":"assistant","content":"hello"},"timestamp":"2026-05-01T00:00:01Z"}\n`
);
}
it("skips a session whose cwd is not a git repo", () => {
const home = makeTestHome();
const work = join(home, "work");
mkdirSync(work, { recursive: true });
writeClaudeCodeSession(home, "not-a-repo", "sess1", sessionIn(work));
const r = runScript(["--bulk", "--no-write"], { HOME: home, GSTACK_HOME: join(home, ".gstack") });
expect(r.exitCode).toBe(0);
expect(r.stdout).toContain("written: 0");
expect(r.stdout).toContain("skipped (unattrib): 1");
rmSync(home, { recursive: true, force: true });
});
it("imports that same session when --include-unattributed is passed", () => {
const home = makeTestHome();
const work = join(home, "work");
mkdirSync(work, { recursive: true });
writeClaudeCodeSession(home, "not-a-repo", "sess1", sessionIn(work));
const r = runScript(["--bulk", "--no-write", "--include-unattributed"], {
HOME: home,
GSTACK_HOME: join(home, ".gstack"),
});
expect(r.exitCode).toBe(0);
expect(r.stdout).toContain("written: 1");
expect(r.stdout).toContain("skipped (unattrib): 0");
rmSync(home, { recursive: true, force: true });
});
it("still imports a session whose cwd does resolve to a git remote", () => {
const home = makeTestHome();
const repo = join(home, "repo");
mkdirSync(repo, { recursive: true });
spawnSync("git", ["-C", repo, "init", "-q"]);
spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/foo/bar.git"]);
writeClaudeCodeSession(home, "real", "sess2", sessionIn(repo));
const r = runScript(["--bulk", "--no-write"], { HOME: home, GSTACK_HOME: join(home, ".gstack") });
expect(r.exitCode).toBe(0);
expect(r.stdout).toContain("written: 1");
expect(r.stdout).toContain("skipped (unattrib): 0");
rmSync(home, { recursive: true, force: true });
});
});
// ── --limit shortcut for smoke tests ───────────────────────────────────────
describe("gstack-memory-ingest --limit", () => {