import { describe, test, beforeEach, afterEach } from "node:test"; import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { tmpdir } from "node:os"; const HERE = dirname(fileURLToPath(import.meta.url)); const CLI = join(HERE, "..", "src", "cli.ts"); function runCli(root: string, args: string[]): { stdout: string; code: number } { try { const stdout = execFileSync("node", ["--import", "tsx", CLI, ...args], { env: { ...process.env, LINKEDIN_STUDIO_DATA: root }, encoding: "utf8", }); return { stdout, code: 0 }; } catch (e: any) { return { stdout: (e.stdout ?? "") + (e.stderr ?? ""), code: e.status ?? 1 }; } } const candidatesFile = (root: string, items: unknown): string => { const p = join(root, "candidates.json"); writeFileSync(p, JSON.stringify(items), "utf8"); return p; }; const profilePath = (root: string) => join(root, "brain", "profile.md"); const pendingJson = (root: string) => join(root, "brain", "pending-diff.json"); const pendingMd = (root: string) => join(root, "brain", "pending-diff.md"); const validCand = { key: "topic", value: "AI safety", provenance: "published", source: "manual", observed_date: "2026-06-23" }; describe("brain consolidate CLI (SC5)", () => { let root: string; beforeEach(() => { root = mkdtempSync(join(tmpdir(), "brain-consol-")); runCli(root, ["init"]); // seed brain/profile.md }); afterEach(() => rmSync(root, { recursive: true, force: true })); test("--gather reads published bodies (filtered by last_run) and writes nothing", () => { runCli(root, ["ingest", "--file", (() => { const f = join(root, "p.md"); writeFileSync(f, "A post about AI safety governance.", "utf8"); return f; })()]); const before = readFileSync(profilePath(root), "utf8"); const { stdout, code } = runCli(root, ["consolidate", "--gather"]); assert.equal(code, 0); assert.match(stdout, /AI safety governance/, "gather surfaces the published body"); assert.equal(readFileSync(profilePath(root), "utf8"), before, "gather leaves profile.md unchanged"); }); test("--propose writes pending-diff.{md,json}, leaves profile.md unchanged", () => { const before = readFileSync(profilePath(root), "utf8"); const { code } = runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [validCand])]); assert.equal(code, 0); assert.ok(existsSync(pendingJson(root)), "pending-diff.json written"); assert.ok(existsSync(pendingMd(root)), "pending-diff.md written"); assert.equal(readFileSync(profilePath(root), "utf8"), before, "propose leaves profile.md unchanged"); const diff = JSON.parse(readFileSync(pendingJson(root), "utf8")); assert.equal(diff.additions.length, 1); }); test("--propose writes pending-diff.md in OKF-compatible form (type: PendingDiff)", () => { // The pending-diff is a transient operator-review artifact that lives in the // brain/ bundle, so it must carry a type or okf-check fails the whole bundle // mid-propose. (docs/okf-convergence-brief.md Stage-1 deferred item.) runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [validCand])]); const block = readFileSync(pendingMd(root), "utf8").match(/^---\n([\s\S]*?)\n---\n/); assert.ok(block, "pending-diff.md leads with a frontmatter block"); assert.match(block![1], /^type:\s*PendingDiff\s*$/m, "pending-diff.md is type PendingDiff"); }); test("--propose REJECTS a malformed candidate (missing field), no write", () => { const { code } = runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [{ key: "x", value: "y" }])]); assert.notEqual(code, 0, "non-zero exit on malformed candidate"); assert.ok(!existsSync(pendingJson(root)), "no diff written"); }); test("--propose REJECTS a candidate whose value contains a newline, no write", () => { const bad = { ...validCand, value: "line one\nline two" }; const { code } = runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [bad])]); assert.notEqual(code, 0, "non-zero exit on multiline value"); assert.ok(!existsSync(pendingJson(root)), "no diff written"); }); test("--apply --confirm writes profile.md + consolidation-state.json", () => { runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [validCand])]); const { code } = runCli(root, ["consolidate", "--apply", "--diff", pendingJson(root), "--confirm"]); assert.equal(code, 0); assert.match(readFileSync(profilePath(root), "utf8"), /AI safety/, "fact applied to profile.md"); assert.ok(existsSync(join(root, "brain", "consolidation-state.json")), "last-run sidecar written"); }); test("--apply WITHOUT --confirm refuses and writes nothing", () => { runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [validCand])]); const before = readFileSync(profilePath(root), "utf8"); const { code } = runCli(root, ["consolidate", "--apply", "--diff", pendingJson(root)]); assert.notEqual(code, 0, "refuses without --confirm"); assert.equal(readFileSync(profilePath(root), "utf8"), before, "profile.md untouched"); }); test("init/ingest/published still work (regression)", () => { assert.equal(runCli(root, ["init"]).code, 0); const f = join(root, "q.md"); writeFileSync(f, "another post", "utf8"); assert.equal(runCli(root, ["ingest", "--file", f]).code, 0); assert.equal(runCli(root, ["published", "list"]).code, 0); }); const supCand = { key: "topic", value: "AI governance", provenance: "published", source: "manual", observed_date: "2026-06-24", supersedes: "topic" }; test("S3b-SC6 — Supersessions section renders one line per entry; a plain diff renders none; gated apply writes superseded", () => { // plain propose → NO Supersessions section (byte-identity decoy) runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [validCand])]); assert.ok(!readFileSync(pendingMd(root), "utf8").includes("## Supersessions"), "no section when 0 supersessions"); // apply to seed the active fact, then propose a supersede of it runCli(root, ["consolidate", "--apply", "--diff", pendingJson(root), "--confirm"]); runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [supCand])]); const md = readFileSync(pendingMd(root), "utf8"); assert.match(md, /## Supersessions/, "section rendered when a supersession exists"); assert.match(md, /AI safety.*→.*AI governance/, "old → new line rendered"); const diff = JSON.parse(readFileSync(pendingJson(root), "utf8")); assert.equal(diff.supersedes.length, 1, "one supersede op in the JSON"); // the gated apply writes the superseded status to profile.md runCli(root, ["consolidate", "--apply", "--diff", pendingJson(root), "--confirm"]); assert.match(readFileSync(profilePath(root), "utf8"), /superseded/, "apply writes the superseded status"); }); test("S3b-SC10 — --gather excludes superseded facts from profileFacts", () => { runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [validCand])]); runCli(root, ["consolidate", "--apply", "--diff", pendingJson(root), "--confirm"]); runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [supCand])]); runCli(root, ["consolidate", "--apply", "--diff", pendingJson(root), "--confirm"]); // profile now holds an archival "AI safety" (superseded) + winner "AI governance" (active) const { stdout } = runCli(root, ["consolidate", "--gather", "--json"]); const out = JSON.parse(stdout); const values = out.profileFacts.map((f: any) => f.value); assert.ok(values.includes("AI governance"), "active winner present in gather"); assert.ok(!out.profileFacts.some((f: any) => f.status === "superseded"), "no superseded fact leaks into gather"); }); });