diff --git a/packages/amico-run/test/coordination-ledger.contract.test.ts b/packages/amico-run/test/coordination-ledger.contract.test.ts index f127e3d2..98629cb5 100644 --- a/packages/amico-run/test/coordination-ledger.contract.test.ts +++ b/packages/amico-run/test/coordination-ledger.contract.test.ts @@ -1,10 +1,50 @@ // Contract suite for coordination ledger (spec #318) — runs against both cloud and sqlite ref -import { describe, it, expect } from "vitest"; +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { homedir, tmpdir } from "node:os"; +import { join } from "node:path"; import { coordinationService, SqliteCoordinationService, workId } from "../src/coordination_ledger.js"; import { degradedStamp } from "../src/coordination_ledger.js"; const wid = (s: string) => workId({ structure_hash: s, goal: "CZ", N: 100, T: 30 }); +// ── isolation bridge (#642) ───────────────────────────────────────────────── +// Claim appends made by this suite must land in a per-run tmp partition and +// NEVER in the production claims ledger (~/.amico/ledger/claims.jsonl) — the +// Prova simulated-ledger-partition pattern from docs/ledger.md, via the +// $AMICO_CLAIMS_FILE seam the service reads per append. Before the bridge, the +// suite constructed the service bare with the env unset, so every contract run +// appended fixture rows to the real ledger: 176 rows before ops archived it as +// claims.jsonl.archive-20260830-test-pollution (#642). +const PRODUCTION_CLAIMS = join(homedir(), ".amico", "ledger", "claims.jsonl"); +// Snapshot at module load — before any test in this file has run. +const productionSnapshot = existsSync(PRODUCTION_CLAIMS) + ? readFileSync(PRODUCTION_CLAIMS).toString("base64") + : null; +const ISO_ROOT = mkdtempSync(join(tmpdir(), "amico-claims-iso-")); +const ISOLATED_CLAIMS = join(ISO_ROOT, "claims.jsonl"); + +const productionNow = (): string | null => + existsSync(PRODUCTION_CLAIMS) ? readFileSync(PRODUCTION_CLAIMS).toString("base64") : null; + +// The bridge itself: route EVERY service construction in this file at the +// isolated partition. The env is the seam claimsFile() reads per append, so +// bare constructions and future ones are covered equally; production callers +// never set it and keep their default path. +const prevClaimsEnv = process.env.AMICO_CLAIMS_FILE; +beforeAll(() => { + process.env.AMICO_CLAIMS_FILE = ISOLATED_CLAIMS; +}); +afterAll(() => { + // Suite-wide guard, order-robust: after every test has run, the production + // ledger is still pristine (byte-identical, or still absent — CI and local). + expect(productionNow()).toBe(productionSnapshot); + // Never leak process state across suites (vitest reuses forked workers). + if (prevClaimsEnv === undefined) delete process.env.AMICO_CLAIMS_FILE; + else process.env.AMICO_CLAIMS_FILE = prevClaimsEnv; + rmSync(ISO_ROOT, { recursive: true, force: true }); +}); + describe("coordination ledger — claim serialization (spec §3)", () => { it("simultaneous claims serialize by receipt order; loser gets holder", async () => { const svc = new SqliteCoordinationService(); @@ -52,3 +92,16 @@ describe("coordination ledger — claim serialization (spec §3)", () => { expect(list.some(s => s.user === "alice" && s.host === "h1")).toBe(true); }); }); + +describe("coordination ledger — test isolation (#642)", () => { + it("claim appends land in the per-run tmp partition, never the production claims ledger", async () => { + const svc = new SqliteCoordinationService(); + const r = await svc.preflight({ work_id: wid("iso-guard"), agent_id: "iso", user: "iso", org: "iso", host: "iso" }); + expect(r.ok).toBe(true); + // the durable append still works — the claim line landed in the isolated partition + const written = existsSync(ISOLATED_CLAIMS) ? readFileSync(ISOLATED_CLAIMS, "utf8") : ""; + expect(written).toContain(wid("iso-guard")); + // and the production ledger is byte-identical to its pre-suite snapshot (or still absent) + expect(productionNow()).toBe(productionSnapshot); + }); +}); diff --git a/packages/amico-run/test/setup.ts b/packages/amico-run/test/setup.ts index 4a2596a8..cc6efffe 100644 --- a/packages/amico-run/test/setup.ts +++ b/packages/amico-run/test/setup.ts @@ -21,3 +21,13 @@ process.env.AMICO_CRITIC_BIN = "/nonexistent/amico-test-guard/no-real-model-call if (!process.env.AMICO_LEDGER) { process.env.AMICO_LEDGER = "/nonexistent/amico-test-guard/ledger.jsonl"; } + +// And for the coordination claims ledger ($AMICO_CLAIMS_FILE → ~/.amico/ledger/claims.jsonl): +// the coordination-ledger contract suite constructed its service bare, and every run appended +// fixture rows to the real claims ledger — 176 rows before ops archived it 2026-08-30 as +// claims.jsonl.archive-20260830-test-pollution (#642; no in-repo action needed beyond this +// guard). The suite now routes its writes through its own per-run tmp partition; this backstop +// fails closed for the next suite that forgets. +if (!process.env.AMICO_CLAIMS_FILE) { + process.env.AMICO_CLAIMS_FILE = "/nonexistent/amico-test-guard/claims.jsonl"; +}