import { describe, test, beforeEach, afterEach } from "node:test"; import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { mkdtempSync, rmSync, existsSync, writeFileSync, readFileSync, readdirSync, 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"); /** Run the brain CLI in a child process with LINKEDIN_STUDIO_DATA pointed at `root`. */ 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 }; } } describe("brain CLI dispatch (SB-S1)", () => { let root: string; beforeEach(() => { root = mkdtempSync(join(tmpdir(), "brain-cli-")); }); afterEach(() => { rmSync(root, { recursive: true, force: true }); }); test("`init` still works after the CLI refactor (regression)", () => { const { stdout, code } = runCli(root, ["init"]); assert.equal(code, 0); assert.ok(existsSync(join(root, "brain", "profile.md")), "init scaffolded brain/"); assert.match(stdout, /Created|Brain root/); }); test("`ingest --file` writes a published record", () => { const f = join(root, "post.md"); writeFileSync(f, "A published post body.", "utf8"); const { stdout, code } = runCli(root, ["ingest", "--file", f]); assert.equal(code, 0); assert.match(stdout, /Wrote ingest\/published\/[0-9a-f]{12}\.md/); }); test("`ingest --file` twice on the same body reports a duplicate", () => { const f = join(root, "post.md"); writeFileSync(f, "dup body", "utf8"); runCli(root, ["ingest", "--file", f]); const { stdout } = runCli(root, ["ingest", "--file", f]); assert.match(stdout, /Duplicate/); }); test("`ingest --scan-inbox` processes the inbox", () => { runCli(root, ["init"]); writeFileSync(join(root, "ingest", "inbox", "a.md"), "inbox post", "utf8"); const { stdout, code } = runCli(root, ["ingest", "--scan-inbox"]); assert.equal(code, 0); assert.match(stdout, /1 new/); }); test("`published list` lists ingested records with provenance", () => { const f = join(root, "post.md"); writeFileSync(f, "listed post", "utf8"); runCli(root, ["ingest", "--file", f]); const { stdout, code } = runCli(root, ["published", "list"]); assert.equal(code, 0); assert.match(stdout, /published/); }); test("unknown command exits 2 (usage error)", () => { const { code } = runCli(root, ["bogus"]); assert.equal(code, 2); }); // SC5 — repeatable producer flags collect into arrays. test("SC5: `ingest --specific a --specific c --trend b` tags the record [a,c]/[b]", () => { const f = join(root, "post.md"); writeFileSync(f, "A tagged published post body.", "utf8"); const { stdout, code } = runCli(root, [ "ingest", "--file", f, "--specific", "aaaaaaaaaaaa", "--specific", "cccccccccccc", "--trend", "bbbbbbbbbbbb", ]); assert.equal(code, 0); const idMatch = stdout.match(/published\/([0-9a-f]{12})\.md/); assert.ok(idMatch, "wrote a record"); const recText = readFileSync(join(root, "ingest", "published", `${idMatch![1]}.md`), "utf8"); assert.match(recText, /specifics: aaaaaaaaaaaa,cccccccccccc/); assert.match(recText, /trends: bbbbbbbbbbbb/); }); // SC12 — single-value flags unregressed by the repeatable-flag change. test("SC12: single-value `--source` + boolean `--scan-inbox` still parse as today", () => { const f = join(root, "p.md"); writeFileSync(f, "single-flag body", "utf8"); runCli(root, ["ingest", "--file", f, "--source", "connector-x"]); const recDir = join(root, "ingest", "published"); const recFile = readFileSync(join(recDir, readdirSync(recDir)[0]), "utf8"); assert.match(recFile, /source: connector-x/); const { code } = runCli(root, ["ingest", "--scan-inbox"]); assert.equal(code, 0); }); // SC9 — read-only `assemble` prints the join and writes nothing. test("SC9: `assemble` joins post↔analytics and writes nothing", () => { const f = join(root, "post.md"); const body = "Jeg lærte noe viktig om dømmekraft i dag. Del 1."; writeFileSync(f, body, "utf8"); runCli(root, ["ingest", "--file", f, "--date", "2026-05-26", "--specific", "aaaaaaaaaaaa"]); // Seed an analytics batch JSON (raw shape; assemble inlines the read). const postsDir = join(root, "analytics", "posts"); mkdirSync(postsDir, { recursive: true }); writeFileSync( join(postsDir, "2026-05-26-batch.json"), JSON.stringify({ posts: [{ title: "Jeg lærte noe viktig om dømmekraft i dag.", publishedDate: "2026-05-26", metrics: { engagementRate: 5.1 } }] }), "utf8", ); const { stdout, code } = runCli(root, ["assemble"]); assert.equal(code, 0); assert.match(stdout, /aaaaaaaaaaaa/, "surfaces the specific id"); assert.match(stdout, /high/i, "shows the high-confidence analytics match"); assert.ok(!existsSync(join(root, "brain", "profile.md")), "assemble wrote no profile.md"); }); test("SC9: `assemble` on an empty root degrades cleanly (no crash)", () => { const { code } = runCli(root, ["assemble"]); assert.equal(code, 0); }); });