import { describe, test } from "node:test"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; // Resolve the package root (scripts/editions) so the subprocess `src/cli.ts` path + // the `tsx` loader resolve regardless of the runner's cwd. const editionsDir = fileURLToPath(new URL("..", import.meta.url)); function run(args: string[]): { status: number | null; stdout: string; stderr: string } { const res = spawnSync("node", ["--import", "tsx", "src/cli.ts", ...args], { encoding: "utf8", cwd: editionsDir, }); return { status: res.status, stdout: res.stdout, stderr: res.stderr }; } const tmp = () => mkdtempSync(join(tmpdir(), "editions-cli-")); const EXTRACT = { editionId: "05", title: "Da pipelinen brøt sammen", lockedAt: "2026-07-20", anecdotes: ["Jeg brukte tre uker på å migrere pipeline-en, og halvparten av tiden gikk til testdata."], arguments: ["Modellen var ikke problemet — det var dataene ingen hadde ryddet på fem år."], hooks: ["De fleste AI-prosjekter dør ikke av dårlig modell. De dør av dårlig data."], }; describe("editions CLI — distil-append / distil-check round-trip (N11 AC)", () => { test("append -> check round-trips through a real file and flags the reuse", () => { const dir = tmp(); try { const distillate = join(dir, "linkedin", "series-distillate.json"); const extract = join(dir, "extract.json"); const skeleton = join(dir, "skeleton.json"); writeFileSync(extract, JSON.stringify(EXTRACT), "utf8"); // 1. lock writes the distillate (file did not exist) const appended = run(["distil-append", "--distillate", distillate, "--series", "seres", "--extract", extract]); assert.equal(appended.status, 0, appended.stderr); assert.ok(existsSync(distillate), "distil-append must create the distillate file"); const onDisk = JSON.parse(readFileSync(distillate, "utf8")); assert.equal(onDisk.series, "seres"); assert.equal(onDisk.editions.length, 1); assert.equal(onDisk.editions[0].editionId, "05"); // 2. a later skeleton reusing the same hook is flagged, with attribution writeFileSync(skeleton, JSON.stringify({ hooks: [EXTRACT.hooks[0]] }), "utf8"); const checked = run(["distil-check", "--distillate", distillate, "--skeleton", skeleton, "--json"]); assert.equal(checked.status, 0, checked.stderr); const report = JSON.parse(checked.stdout); assert.equal(report.verdict, "REUSE"); assert.equal(report.hits[0].editionId, "05"); assert.equal(report.hits[0].kind, "hook"); // 3. fresh material on the same distillate is CLEAR writeFileSync( skeleton, JSON.stringify({ hooks: ["Innkjøpsavdelingen krevde en garanti vi ikke kunne gi uten revisjon."] }), "utf8", ); const clear = run(["distil-check", "--distillate", distillate, "--skeleton", skeleton, "--json"]); assert.equal(clear.status, 0, clear.stderr); assert.equal(JSON.parse(clear.stdout).verdict, "CLEAR"); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("re-appending the same edition replaces it — a pivot re-lock does not duplicate", () => { const dir = tmp(); try { const distillate = join(dir, "series-distillate.json"); const extract = join(dir, "extract.json"); writeFileSync(extract, JSON.stringify(EXTRACT), "utf8"); run(["distil-append", "--distillate", distillate, "--series", "seres", "--extract", extract]); writeFileSync(extract, JSON.stringify({ ...EXTRACT, title: "Revidert" }), "utf8"); const again = run(["distil-append", "--distillate", distillate, "--series", "seres", "--extract", extract]); assert.equal(again.status, 0, again.stderr); const onDisk = JSON.parse(readFileSync(distillate, "utf8")); assert.equal(onDisk.editions.length, 1); assert.equal(onDisk.editions[0].title, "Revidert"); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("distil-check against a distillate that does not exist yet is CLEAR, not an error", () => { const dir = tmp(); try { const skeleton = join(dir, "skeleton.json"); writeFileSync(skeleton, JSON.stringify({ hooks: ["første utgave i serien"] }), "utf8"); const res = run([ "distil-check", "--distillate", join(dir, "absent.json"), "--skeleton", skeleton, "--json", ]); assert.equal(res.status, 0, res.stderr); const report = JSON.parse(res.stdout); assert.equal(report.verdict, "CLEAR"); assert.equal(report.comparedAgainst, 0); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("human-readable output names the reused unit and its edition", () => { const dir = tmp(); try { const distillate = join(dir, "d.json"); const extract = join(dir, "extract.json"); const skeleton = join(dir, "skeleton.json"); writeFileSync(extract, JSON.stringify(EXTRACT), "utf8"); run(["distil-append", "--distillate", distillate, "--series", "seres", "--extract", extract]); writeFileSync(skeleton, JSON.stringify({ hooks: [EXTRACT.hooks[0]] }), "utf8"); const res = run(["distil-check", "--distillate", distillate, "--skeleton", skeleton]); assert.equal(res.status, 0, res.stderr); assert.match(res.stdout, /REUSE/); assert.match(res.stdout, /05/); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("bad invocations exit 2 with usage on stderr", () => { const dir = tmp(); try { assert.equal(run(["distil-append", "--series", "seres"]).status, 2); // no --extract/--distillate assert.equal(run(["distil-check"]).status, 2); // no --skeleton assert.equal(run(["nonsense"]).status, 2); assert.equal(run([]).status, 2); // an extract missing required fields is a usage error, not a silent empty entry const bad = join(dir, "bad.json"); writeFileSync(bad, JSON.stringify({ title: "no id" }), "utf8"); const res = run(["distil-append", "--distillate", join(dir, "d.json"), "--series", "s", "--extract", bad]); assert.equal(res.status, 2); assert.match(res.stderr, /editionId/); } finally { rmSync(dir, { recursive: true, force: true }); } }); });