diff --git a/bin/gstack-memory-ingest.ts b/bin/gstack-memory-ingest.ts index fd7e53a73..67e885ef2 100644 --- a/bin/gstack-memory-ingest.ts +++ b/bin/gstack-memory-ingest.ts @@ -1778,7 +1778,15 @@ async function ingestPass(args: CliArgs): Promise { ); } } finally { - cleanupStagingDir(stagingDir); + // #1802 D1: in remote-http mode `stagingDir` is the PERSISTENT transcript + // dir (makePersistentTranscriptDir, under ~/.gstack/transcripts/) that + // gstack-brain-sync push must pick up — it is NOT a `.staging-ingest-*` dir + // and must never be deleted here. The remote-http branch above already + // documents this intent ("Skip the ... cleanupStagingDir paths"), but a + // `finally` runs on its `return`, so the gate has to live here. Gating on + // mode (rather than widening the ownership guard) keeps checkOwnedStagingDir + // strict: it only ever sees `.staging-ingest-*` dirs. + if (!remoteHttpMode) cleanupStagingDir(stagingDir); _activeStagingDir = null; } diff --git a/test/regression-1611-gbrain-sync-resume.test.ts b/test/regression-1611-gbrain-sync-resume.test.ts index 481d3284c..1f262a596 100644 --- a/test/regression-1611-gbrain-sync-resume.test.ts +++ b/test/regression-1611-gbrain-sync-resume.test.ts @@ -350,3 +350,30 @@ describe("#1802 checkOwnedStagingDir — ownership matrix", () => { expect(sync).toMatch(/checkOwnedStagingDir\(stagingDir, gstackHome\)/); }); }); + +// ── #1802 D1: remote-http persistent dir must never hit cleanupStagingDir ─── +// In remote-http mode `stagingDir` is the PERSISTENT transcript dir +// (makePersistentTranscriptDir, under ~/.gstack/transcripts/) that +// gstack-brain-sync push consumes. The finally runs on the remote-http `return`, +// so the cleanup call there must be gated on `!remoteHttpMode` — otherwise the +// guard refuses it on every sync (false "prevent data loss" warning) and, pre- +// gate, the dir was deleted outright (broken artifacts handoff). +describe("#1802 D1 — remote-http finally gate (static invariant)", () => { + const ingest = fs.readFileSync( + path.join(ROOT, "bin", "gstack-memory-ingest.ts"), + "utf-8", + ); + + test("finally gates cleanupStagingDir on !remoteHttpMode", () => { + expect(ingest).toMatch(/if \(!remoteHttpMode\) cleanupStagingDir\(stagingDir\)/); + }); + + test("the only finally-scoped cleanup call is the gated one", () => { + // Locate the finally block and assert it does not contain a bare + // `cleanupStagingDir(stagingDir);` that would run regardless of mode. + const finallyAt = ingest.lastIndexOf("} finally {"); + expect(finallyAt).toBeGreaterThan(-1); + const finallySlice = ingest.slice(finallyAt, finallyAt + 800); + expect(finallySlice).not.toMatch(/^\s*cleanupStagingDir\(stagingDir\);/m); + }); +});