/* Regression test for stash cleanup in commitAndPushBordbuch. Verifies that stashes are dropped after failed stash pop and on push failure, preventing stale stash accumulation in cache clones. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-1311: carried legacy entry — Initial: regression test for stash drop after failed pop and on push failure. RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared */ import { test, expect, beforeEach, afterEach, vi } from "vitest"; import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; const gitCalls = vi.hoisted(() => ({ calls: [] as string[], failOnPop: false, failOnPush: false, })); vi.mock("../werkstatt/git-exec.ts", () => ({ gitExec: vi.fn((cwd: string, args: string) => { gitCalls.calls.push(args); if (args === "stash pop" && gitCalls.failOnPop) { throw new Error("stash pop conflict"); } if (args.startsWith("push") && args.includes("origin") && gitCalls.failOnPush) { throw new Error("push failed"); } if (args === "symbolic-ref --short HEAD") return "master"; if (args === "rev-parse HEAD") return "abc123"; return ""; }), })); vi.mock("../mission/mission-git-commit.ts", () => ({ cacheCloneCommit: vi.fn(), })); import { commitAndPushBordbuch } from "./bordbuch-io.ts"; let testRoot: string; let systemDir: string; beforeEach(async () => { testRoot = await mkdtemp(join(tmpdir(), "stash-cleanup-test-")); systemDir = join(testRoot, "cache-clone"); await mkdir(systemDir, { recursive: true }); await mkdir(join(systemDir, "bordbuch"), { recursive: true }); await writeFile(join(systemDir, "bordbuch", "events.ndjson"), ""); gitCalls.calls = []; gitCalls.failOnPop = false; gitCalls.failOnPush = false; }); afterEach(async () => { await rm(testRoot, { recursive: true, force: true }); }); test("drops stash after failed stash pop", async () => { gitCalls.failOnPop = true; const result = await commitAndPushBordbuch(systemDir, "test commit"); expect(gitCalls.calls).toContain("stash push -m bordbuch-pull-rebase"); expect(gitCalls.calls).toContain("stash pop"); expect(gitCalls.calls).toContain("stash drop"); expect(result.pushed).toBe(true); }); test("returns error on push failure after successful stash pop", async () => { gitCalls.failOnPush = true; const result = await commitAndPushBordbuch(systemDir, "test commit"); expect(gitCalls.calls).toContain("stash push -m bordbuch-pull-rebase"); expect(gitCalls.calls).toContain("stash pop"); expect(result.pushed).toBe(false); expect(result.error).toContain("push failed"); }); const gitCallsNoStash = vi.hoisted(() => ({ calls: [] as string[], })); test("no stash drop when nothing was stashed", async () => { vi.mocked(await import("../werkstatt/git-exec.ts")).gitExec.mockImplementationOnce( async (cwd: string, args: string) => { gitCallsNoStash.calls.push(args); if (args === "stash push -m bordbuch-pull-rebase") { throw new Error("no changes to stash"); } if (args === "symbolic-ref --short HEAD") return "master"; if (args === "rev-parse HEAD") return "abc123"; return ""; }, ); const result = await commitAndPushBordbuch(systemDir, "test commit"); // stash drop should NOT appear since nothing was stashed expect(gitCallsNoStash.calls).not.toContain("stash drop"); expect(result.pushed).toBe(true); });