linkedin-studio/scripts/trends/tests/brief.test.ts
Kjell Tore Guttormsen fa7551070e feat(linkedin-studio): RE-R2b — dated morning-brief artifact + session-start surfacing [skip-docs]
The visible layer of R2. Pure brief.ts: rankForBrief (pillar-overlap -> recency over
the store; publishedAt ?? capturedAt freshness, 7d window; total-order sort), renderBrief
(dated Markdown + hook-surfaceable summary frontmatter), briefSummary (one summary source),
defaultBriefDir (derived from defaultStorePath). CLI `brief` writes
<data>/trends/morning-brief/YYYY-MM-DD.md; session-start surfaces the latest zero-tsx
(latestMorningBrief). Wired into trend-spotter Step 4.6 (scan->capture->brief->surfaced).
No store-schema/scoring change; no scheduler (R3).

25 new trends tests (21 brief.test + 4 cli brief, RED-first) + 3 hook tests (morning-brief
surfacing). trends 104/104 (floor 104), hook-suite 139/139, gate FAIL=0 (ASSERT floor 94,
Section 16i: cli brief-handler + trend-spotter brief-pointer + session-start surfacing
greps), tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 13:12:54 +02:00

173 lines
8.3 KiB
TypeScript

import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { join } from "node:path";
import {
rankForBrief,
renderBrief,
briefSummary,
defaultBriefDir,
BRIEF_SCHEMA_VERSION,
} from "../src/brief.js";
import type { TrendRecord, TrendStore } from "../src/types.js";
const TODAY = "2026-06-24";
function mkTrend(
p: { title: string; url: string; topics: string[]; capturedAt: string; publishedAt?: string; source?: string; summary?: string },
): TrendRecord {
return {
id: p.title + "|" + p.url,
title: p.title,
url: p.url,
source: p.source ?? "tavily",
capturedAt: p.capturedAt,
...(p.publishedAt !== undefined ? { publishedAt: p.publishedAt } : {}),
topics: p.topics,
...(p.summary !== undefined ? { summary: p.summary } : {}),
};
}
function mkStore(trends: TrendRecord[]): TrendStore {
return { schemaVersion: 2, trends };
}
describe("rankForBrief — grouping (SC1)", () => {
const pillars = ["AI", "gov"];
const store = mkStore([
mkTrend({ title: "T1 top", url: "https://e/1", topics: ["ai", "gov", "x"], publishedAt: "2026-06-22", capturedAt: "2026-06-23" }),
mkTrend({ title: "T2 single", url: "https://e/2", topics: ["AI"], publishedAt: "2026-06-20", capturedAt: "2026-06-20" }),
mkTrend({ title: "T3 older1", url: "https://e/3", topics: ["gov"], publishedAt: "2026-06-01", capturedAt: "2026-06-01" }),
mkTrend({ title: "T4 noise", url: "https://e/4", topics: ["unrelated"], publishedAt: "2026-06-23", capturedAt: "2026-06-23" }),
mkTrend({ title: "T5 older2", url: "https://e/5", topics: ["ai", "gov"], publishedAt: "2026-05-01", capturedAt: "2026-05-01" }),
]);
const r = rankForBrief(store, pillars, TODAY);
test("topMatches = overlap>=2 & fresh only", () => {
assert.deepEqual(r.topMatches.map((e) => e.trend.title), ["T1 top"]);
});
test("singleMatches = overlap===1 & fresh only", () => {
assert.deepEqual(r.singleMatches.map((e) => e.trend.title), ["T2 single"]);
});
test("olderMatched = overlap>=1 & stale, sorted overlap desc", () => {
assert.deepEqual(r.olderMatched.map((e) => e.trend.title), ["T5 older2", "T3 older1"]);
});
test("overlap===0 excluded everywhere", () => {
const all = [...r.topMatches, ...r.singleMatches, ...r.olderMatched].map((e) => e.trend.title);
assert.ok(!all.includes("T4 noise"));
});
test("totals reflect trends/matched/fresh", () => {
assert.deepEqual(r.totals, { trends: 5, matched: 4, fresh: 2 });
});
test("matchedPillars preserve pillar case (case-insensitive match)", () => {
assert.deepEqual(r.topMatches[0].matchedPillars, ["AI", "gov"]);
});
});
describe("rankForBrief — within-group total order (SC1)", () => {
test("effectiveDate desc orders before title", () => {
const store = mkStore([
mkTrend({ title: "Bravo", url: "https://e/b1", topics: ["a", "b"], publishedAt: "2026-06-20", capturedAt: "2026-06-20" }),
mkTrend({ title: "Alpha", url: "https://e/a1", topics: ["a", "b"], publishedAt: "2026-06-22", capturedAt: "2026-06-22" }),
]);
const r = rankForBrief(store, ["a", "b"], TODAY);
assert.deepEqual(r.topMatches.map((e) => e.trend.title), ["Alpha", "Bravo"]);
});
test("same title+effectiveDate+overlap -> url asc tie-break (total order)", () => {
const store = mkStore([
mkTrend({ title: "Same", url: "https://e/zzz", topics: ["a", "b"], publishedAt: "2026-06-22", capturedAt: "2026-06-22" }),
mkTrend({ title: "Same", url: "https://e/aaa", topics: ["a", "b"], publishedAt: "2026-06-22", capturedAt: "2026-06-22" }),
]);
const r = rankForBrief(store, ["a", "b"], TODAY);
assert.deepEqual(r.topMatches.map((e) => e.trend.url), ["https://e/aaa", "https://e/zzz"]);
});
});
describe("rankForBrief — freshness (SC2)", () => {
const pillars = ["a"];
test("effectiveDate = publishedAt when present (fresh despite old capturedAt)", () => {
const r = rankForBrief(mkStore([mkTrend({ title: "P", url: "https://e/p", topics: ["a"], publishedAt: "2026-06-22", capturedAt: "2026-01-01" })]), pillars, TODAY);
assert.equal(r.singleMatches.length, 1);
assert.equal(r.singleMatches[0].effectiveDate, "2026-06-22");
assert.equal(r.olderMatched.length, 0);
});
test("fallback to capturedAt when publishedAt absent", () => {
const r = rankForBrief(mkStore([mkTrend({ title: "C", url: "https://e/c", topics: ["a"], capturedAt: "2026-06-22" })]), pillars, TODAY);
assert.equal(r.singleMatches.length, 1);
assert.equal(r.singleMatches[0].effectiveDate, "2026-06-22");
});
test("stale when capturedAt old and no publishedAt", () => {
const r = rankForBrief(mkStore([mkTrend({ title: "S", url: "https://e/s", topics: ["a"], capturedAt: "2026-01-01" })]), pillars, TODAY);
assert.equal(r.olderMatched.length, 1);
assert.equal(r.singleMatches.length, 0);
});
test("boundary: ageDays === freshDays is fresh (<=)", () => {
const r = rankForBrief(mkStore([mkTrend({ title: "B", url: "https://e/bd", topics: ["a"], publishedAt: "2026-06-17", capturedAt: "2026-06-17" })]), pillars, TODAY, { freshDays: 7 });
assert.equal(r.singleMatches.length, 1, "7d with freshDays 7 must be fresh");
assert.equal(r.singleMatches[0].ageDays, 7);
});
test("freshDays configurable: 10d fresh at 14, stale at 7", () => {
const t = mkTrend({ title: "X", url: "https://e/x", topics: ["a"], publishedAt: "2026-06-14", capturedAt: "2026-06-14" });
assert.equal(rankForBrief(mkStore([t]), pillars, TODAY, { freshDays: 14 }).singleMatches.length, 1);
assert.equal(rankForBrief(mkStore([t]), pillars, TODAY, { freshDays: 7 }).olderMatched.length, 1);
});
});
describe("renderBrief + briefSummary (SC3)", () => {
const pillars = ["AI", "gov"];
const store = mkStore([
mkTrend({ title: "Alpha", url: "https://e/a", topics: ["ai", "gov"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", summary: "A short summary" }),
mkTrend({ title: "Beta", url: "https://e/b", topics: ["ai"], publishedAt: "2026-06-20", capturedAt: "2026-06-20" }),
mkTrend({ title: "Gamma", url: "https://e/g", topics: ["gov"], publishedAt: "2026-05-01", capturedAt: "2026-05-01" }),
]);
const r = rankForBrief(store, pillars, TODAY);
const md = renderBrief(r);
test("starts with YAML frontmatter", () => {
assert.ok(md.startsWith("---\n"), "brief must open with YAML frontmatter");
});
test("frontmatter carries date, store, schemaVersion", () => {
assert.match(md, /\ndate: 2026-06-24\n/);
assert.match(md, new RegExp("\\nschemaVersion: " + BRIEF_SCHEMA_VERSION + "\\n"));
assert.match(md, /\nstore:/);
});
test("frontmatter summary === briefSummary(ranking); single line, no quote/newline", () => {
const summary = briefSummary(r);
assert.ok(!summary.includes('"'), "summary must not contain a double-quote");
assert.ok(!summary.includes("\n"), "summary must be a single line");
const m = md.match(/^summary: (.*)$/m);
assert.ok(m, "frontmatter has a summary line");
assert.equal(m![1], summary);
});
test("summary names the top entry when fresh matches exist", () => {
assert.ok(briefSummary(r).includes("Alpha"));
});
test("body has the three section markers", () => {
assert.ok(md.includes("Topp-treff"), "top section");
assert.ok(md.includes("Enkelt-treff"), "single section");
assert.ok(md.includes("Eldre i lager"), "older section");
});
test("deterministic: identical input -> identical bytes", () => {
assert.equal(renderBrief(r), renderBrief(rankForBrief(store, pillars, TODAY)));
});
test("empty ranking renders a valid no-fresh brief", () => {
const empty = rankForBrief(mkStore([]), pillars, TODAY);
const emd = renderBrief(empty);
assert.ok(emd.startsWith("---\n"));
assert.ok(briefSummary(empty).startsWith("Ingen ferske"), "no-fresh summary line");
const m = emd.match(/^summary: (.*)$/m);
assert.equal(m![1], briefSummary(empty));
});
});
describe("defaultBriefDir", () => {
test("ends with trends/morning-brief and honors LINKEDIN_STUDIO_DATA (derived from defaultStorePath)", () => {
const prev = process.env.LINKEDIN_STUDIO_DATA;
process.env.LINKEDIN_STUDIO_DATA = "/tmp/lis-brief-root";
try {
assert.equal(defaultBriefDir(), join("/tmp/lis-brief-root", "trends", "morning-brief"));
} finally {
if (prev === undefined) delete process.env.LINKEDIN_STUDIO_DATA;
else process.env.LINKEDIN_STUDIO_DATA = prev;
}
});
});