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
158 lines
6.8 KiB
TypeScript
158 lines
6.8 KiB
TypeScript
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
|
|
import {
|
|
writePublished,
|
|
ingestText,
|
|
scanInbox,
|
|
listPublished,
|
|
serializePublishedRecord,
|
|
parsePublishedRecord,
|
|
type PublishedRecord,
|
|
} from "../src/ingest.js";
|
|
import { mintContentId } from "../src/id.js";
|
|
|
|
const CAPTURED = "2026-06-23";
|
|
|
|
function publishedDir(root: string): string {
|
|
return join(root, "ingest", "published");
|
|
}
|
|
function inboxDir(root: string): string {
|
|
return join(root, "ingest", "inbox");
|
|
}
|
|
function mdFiles(dir: string): string[] {
|
|
return existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith(".md")) : [];
|
|
}
|
|
|
|
describe("ingest IO — writePublished / ingestText / scanInbox / listPublished (SC1/SC3/SC4)", () => {
|
|
let root: string;
|
|
const prevEnv = process.env.LINKEDIN_STUDIO_DATA;
|
|
|
|
beforeEach(() => {
|
|
root = mkdtempSync(join(tmpdir(), "brain-ingest-"));
|
|
process.env.LINKEDIN_STUDIO_DATA = root;
|
|
});
|
|
afterEach(() => {
|
|
if (prevEnv === undefined) delete process.env.LINKEDIN_STUDIO_DATA;
|
|
else process.env.LINKEDIN_STUDIO_DATA = prevEnv;
|
|
rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
test("create-on-demand: ingestText writes ingest/published/<id>.md with no prior init (SC1)", () => {
|
|
const body = "Min første publiserte post.\n\nMed avsnitt.";
|
|
const res = ingestText({ body, captured_at: CAPTURED });
|
|
assert.equal(res.written, true);
|
|
const path = join(publishedDir(root), `${mintContentId(body)}.md`);
|
|
assert.ok(existsSync(path), "record file created at runtime data-path");
|
|
const rec = parsePublishedRecord(readFileSync(path, "utf8"));
|
|
assert.equal(rec.provenance, "published");
|
|
assert.equal(rec.body, body, "verbatim body preserved");
|
|
});
|
|
|
|
test("published_date defaults to captured_at when not supplied", () => {
|
|
const res = ingestText({ body: "no date given", captured_at: CAPTURED });
|
|
assert.equal(res.record.published_date, CAPTURED);
|
|
});
|
|
|
|
test("published_date is kept when supplied", () => {
|
|
const res = ingestText({ body: "dated", captured_at: CAPTURED, published_date: "2026-05-26" });
|
|
assert.equal(res.record.published_date, "2026-05-26");
|
|
});
|
|
|
|
test("re-ingesting an identical body is a no-op (SC3 dedup): {written:false}, no duplicate file", () => {
|
|
const body = "same exact text";
|
|
ingestText({ body, captured_at: CAPTURED });
|
|
const res2 = ingestText({ body, captured_at: "2026-07-01" }); // different day, same body
|
|
assert.equal(res2.written, false);
|
|
assert.equal(mdFiles(publishedDir(root)).length, 1, "still exactly one record");
|
|
});
|
|
|
|
test("id-collision with a DIFFERING body disambiguates to <id>-2.md — never silent data loss (B2)", () => {
|
|
const body = "the real body";
|
|
const id = mintContentId(body);
|
|
// Pre-plant a file at <id>.md whose body is DIFFERENT (simulates a hash collision).
|
|
mkdirSync(publishedDir(root), { recursive: true });
|
|
const squatter: PublishedRecord = {
|
|
id, provenance: "published", published_date: CAPTURED, captured_at: CAPTURED,
|
|
source: "manual", specifics: [], trends: [], body: "a DIFFERENT body that happens to share the id",
|
|
};
|
|
writeFileSync(join(publishedDir(root), `${id}.md`), serializePublishedRecord(squatter), "utf8");
|
|
|
|
const res = writePublished({ ...squatter, body });
|
|
assert.equal(res.written, true);
|
|
assert.equal(res.collision, true);
|
|
assert.match(res.path, /-2\.md$/);
|
|
assert.equal(mdFiles(publishedDir(root)).length, 2, "both records kept — no data loss");
|
|
});
|
|
|
|
test("scanInbox processes top-level *.md only, skipping dotfiles like .DS_Store (SC4)", () => {
|
|
mkdirSync(inboxDir(root), { recursive: true });
|
|
writeFileSync(join(inboxDir(root), "post-a.md"), "Post A body", "utf8");
|
|
writeFileSync(join(inboxDir(root), "post-b.md"), "Post B body", "utf8");
|
|
writeFileSync(join(inboxDir(root), ".DS_Store"), "junk", "utf8");
|
|
writeFileSync(join(inboxDir(root), "notes.txt"), "not markdown", "utf8");
|
|
|
|
const res = scanInbox({ captured_at: CAPTURED });
|
|
assert.equal(res.processed.length, 2, "only the two .md files processed");
|
|
assert.equal(mdFiles(publishedDir(root)).length, 2);
|
|
});
|
|
|
|
test("scanInbox on an empty/absent inbox is a clean no-op (SC4)", () => {
|
|
const res = scanInbox({ captured_at: CAPTURED });
|
|
assert.deepEqual(res, { processed: [], skipped: [] });
|
|
});
|
|
|
|
test("re-running scanInbox re-skips already-published records (idempotent)", () => {
|
|
mkdirSync(inboxDir(root), { recursive: true });
|
|
writeFileSync(join(inboxDir(root), "post.md"), "once", "utf8");
|
|
scanInbox({ captured_at: CAPTURED });
|
|
const res2 = scanInbox({ captured_at: CAPTURED });
|
|
assert.equal(res2.processed.length, 0);
|
|
assert.equal(res2.skipped.length, 1);
|
|
assert.equal(mdFiles(publishedDir(root)).length, 1, "no duplicate");
|
|
});
|
|
|
|
test("listPublished surfaces provenance and skips a malformed file without throwing", () => {
|
|
ingestText({ body: "good record", captured_at: CAPTURED });
|
|
mkdirSync(publishedDir(root), { recursive: true });
|
|
writeFileSync(join(publishedDir(root), "broken.md"), "not a valid record at all", "utf8");
|
|
|
|
const res = listPublished();
|
|
assert.equal(res.records.length, 1, "the one valid record listed");
|
|
assert.equal(res.records[0].provenance, "published");
|
|
assert.equal(res.skipped, 1, "the malformed file counted as skipped, not a crash");
|
|
});
|
|
|
|
// SC4 — ingestText threads specifics/trends onto the written record; absent → [].
|
|
test("SC4: ingestText threads specifics/trends; round-trips off disk", () => {
|
|
const spec = "aaaaaaaaaaaa";
|
|
const trend = "bbbbbbbbbbbb";
|
|
const res = ingestText({ body: "tagged post body", captured_at: CAPTURED, specifics: [spec], trends: [trend] });
|
|
assert.deepEqual(res.record.specifics, [spec]);
|
|
assert.deepEqual(res.record.trends, [trend]);
|
|
const onDisk = parsePublishedRecord(readFileSync(res.path, "utf8"));
|
|
assert.deepEqual(onDisk.specifics, [spec]);
|
|
assert.deepEqual(onDisk.trends, [trend]);
|
|
});
|
|
|
|
test("SC4: ingestText with no specifics/trends keeps them empty (regression)", () => {
|
|
const res = ingestText({ body: "plain post", captured_at: CAPTURED });
|
|
assert.deepEqual(res.record.specifics, []);
|
|
assert.deepEqual(res.record.trends, []);
|
|
});
|
|
|
|
// SC3 (producer side) — a malformed id thrown fast at the producer boundary.
|
|
test("SC3: ingestText with a non-12-hex specifics id throws at the producer", () => {
|
|
assert.throws(
|
|
() => ingestText({ body: "x", captured_at: CAPTURED, specifics: ["NOTAHEX"] }),
|
|
/bad specifics id/i,
|
|
);
|
|
assert.throws(
|
|
() => ingestText({ body: "y", captured_at: CAPTURED, trends: ["zzz"] }),
|
|
/bad trends id/i,
|
|
);
|
|
});
|
|
});
|