Hub-side design: the published record now carries the specifics/trends ids it was built from (additive, omit-empty → byte-backward-compatible), and a new pure assembler (scripts/brain/src/assemble.ts + `brain assemble`) joins post↔analytics by normalized title-prefix + date with honest confidence tiers (high/low/none). Answers the arc's north-star query: which raw material actually performs? (specific → post → measured analytics). All four tributaries untouched (analytics READ-only via inlined raw-JSON, no package import); profile.md grammar untouched (the fact→post link stays OUT — C-1). The repeatable --specific/--trend ingest flags collect via a new collectRepeated helper, leaving parseFlags untouched. TDD: 19 new brain tests (ingest 4 + publish 3 + assemble 8 + cli 4), all SC1–SC12. brain 113/113, gate 95/0/0, BRAIN_TESTS_FLOOR 94→113, ASSERT_BASELINE_FLOOR unchanged at 80. Light-Voyage hardened (brief-review 5 FIX · plan-critic 1 BLOCK+4 MAJOR+4 MINOR · scope-guardian ALIGNED). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
132 lines
5.5 KiB
TypeScript
132 lines
5.5 KiB
TypeScript
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);
|
|
});
|
|
});
|