linkedin-studio/scripts/trends/tests/brief.test.ts
Kjell Tore Guttormsen 5b51b4baeb feat(linkedin-studio): RE-R3e — brief history + day-over-day diff (surfaced: frontmatter + Nytt siden sist) [skip-docs]
Closes hull #7 ("ingen brief-historikk"). Each morning brief now records the
trend ids it showed into its own YAML frontmatter (surfaced: <id-csv> =
surfacedIds(ranking)) and renders a day-over-day diff against the most recent
prior brief — a "## Nytt siden sist (<prior-date>)" section that leads the
ranked list, plus a " N nye siden sist." marker on the one-line summary the
SessionStart hook surfaces (no hook change).

- brief.ts: BRIEF_SCHEMA_VERSION 1->2 (artifact frontmatter gained surfaced:;
  the store's SCHEMA_VERSION stays 4 — no store field). Three PURE helpers
  (diffSurfaced / parseSurfacedFrontmatter / selectPriorBriefFile) + the
  surfaced: emit + the section + the summary marker. No fs/clock in brief.ts.
- cli.ts: the brief handler discovers the prior dated file (existsSync-guarded
  readdirSync -> selectPriorBriefFile, strict < today so a same-day re-run is
  byte-identical), parses its surfaced: line, computes the diff, threads it into
  renderBrief AND the shared briefSummary(ranking, diff) (one-source: file
  frontmatter == --json summary, cli.test one-source invariant). --json gains a
  diff:{priorDate,added,carried,dropped} counts object; the console line appends
  the delta. Any fs error degrades to the empty-prior (first-brief) path.

TDD two-phase: stubs -> 17 value-RED (no module-not-found) -> GREEN. Trends suite
216 -> 245 (brief +27, cli +2), 0 fail. New unconditional gate Section 16n (6
checks); ASSERT_BASELINE_FLOOR 117 -> 123; TRENDS_TESTS_FLOOR -> 245. Full gate
FAIL=0; hook suite 139/139 + R3c schedule/run-daily green untouched. Behavioural:
real two-day rename-real-write diff + same-day byte-identity confirmed. Counts
29/19/27 unchanged; no version bump (additive, v0.5.2 dev).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vmzxpsFpc8q19LaogAWLD
2026-06-26 14:40:09 +02:00

734 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { join } from "node:path";
import {
rankForBrief,
renderBrief,
briefSummary,
defaultBriefDir,
surfacedIds,
diffSurfaced,
parseSurfacedFrontmatter,
selectPriorBriefFile,
temporalSignal,
BRIEF_SCHEMA_VERSION,
} from "../src/brief.js";
import { SCHEMA_VERSION } from "../src/types.js";
import type { TrendRecord, TrendStore } from "../src/types.js";
const TODAY = "2026-06-24";
type TestScore = {
mode: "kortform" | "long-form";
dimensions: Record<string, number>;
composite: number;
priority: "Immediate" | "High" | "Medium" | "Low" | "Skip";
};
function mkTrend(
p: {
title: string;
url: string;
topics: string[];
capturedAt: string;
publishedAt?: string;
source?: string;
summary?: string;
score?: TestScore;
status?: "new" | "acted" | "skipped";
surfacedCount?: number;
lastSurfacedAt?: 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 } : {}),
...(p.score !== undefined ? { score: p.score } : {}),
...(p.status !== undefined ? { status: p.status } : {}),
...(p.surfacedCount !== undefined ? { surfacedCount: p.surfacedCount } : {}),
...(p.lastSurfacedAt !== undefined ? { lastSurfacedAt: p.lastSurfacedAt } : {}),
};
}
/** A composite-bearing score for the rank tests (mode/priority kept consistent for render asserts). */
function mkScore(composite: number, priority: TestScore["priority"], mode: TestScore["mode"] = "kortform"): TestScore {
return { mode, dimensions: { pillar: 5, audience: 5, timing: 5, angle: 5, authority: 5 }, composite, priority };
}
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 (both neutral so the RE-R3d temporal key ties)", () => {
// RE-R3d: both entries are neutral (surfaced 0, ageDays 4-6 > firstMoverDays 2), so the temporal
// key ties and effectiveDate is the deciding key; titles disagree with date to isolate effectiveDate.
const store = mkStore([
mkTrend({ title: "Alpha", url: "https://e/a1", topics: ["a", "b"], publishedAt: "2026-06-18", capturedAt: "2026-06-18" }),
mkTrend({ title: "Bravo", url: "https://e/b1", topics: ["a", "b"], publishedAt: "2026-06-20", capturedAt: "2026-06-20" }),
]);
const r = rankForBrief(store, ["a", "b"], TODAY);
assert.deepEqual(r.topMatches.map((e) => e.trend.title), ["Bravo", "Alpha"]);
});
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("rankForBrief — composite primary within bucket (RE-R3a / SC5)", () => {
const pillars = ["a", "b"];
test("higher composite sorts first at the same overlap + freshness", () => {
const store = mkStore([
mkTrend({ title: "Low", url: "https://e/low", topics: ["a", "b"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", score: mkScore(6.0, "High") }),
mkTrend({ title: "High", url: "https://e/high", topics: ["a", "b"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", score: mkScore(9.0, "Immediate") }),
]);
const r = rankForBrief(store, pillars, TODAY);
assert.deepEqual(r.topMatches.map((e) => e.trend.title), ["High", "Low"], "composite 9 before composite 6");
});
test("an unscored record sorts after every scored record in its bucket (sentinel -1)", () => {
const store = mkStore([
mkTrend({ title: "Unscored", url: "https://e/u", topics: ["a", "b"], publishedAt: "2026-06-22", capturedAt: "2026-06-22" }),
mkTrend({ title: "Scored low", url: "https://e/s", topics: ["a", "b"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", score: mkScore(2.0, "Low") }),
]);
const r = rankForBrief(store, pillars, TODAY);
assert.deepEqual(r.topMatches.map((e) => e.trend.title), ["Scored low", "Unscored"], "any scored beats unscored");
});
test("both-unscored same-title/diff-url pair falls back to url asc (total order intact)", () => {
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, pillars, TODAY);
assert.deepEqual(r.topMatches.map((e) => e.trend.url), ["https://e/aaa", "https://e/zzz"]);
});
test("composite overrides effectiveDate within the bucket (composite is the leading key)", () => {
const store = mkStore([
mkTrend({ title: "Fresher lower", url: "https://e/fl", topics: ["a", "b"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", score: mkScore(5.0, "Medium") }),
mkTrend({ title: "Older higher", url: "https://e/oh", topics: ["a", "b"], publishedAt: "2026-06-20", capturedAt: "2026-06-20", score: mkScore(9.0, "Immediate") }),
]);
const r = rankForBrief(store, pillars, TODAY);
assert.deepEqual(r.topMatches.map((e) => e.trend.title), ["Older higher", "Fresher lower"]);
});
test("deterministic: identical scored input -> identical ranking order", () => {
const trends = [
mkTrend({ title: "A", url: "https://e/a", topics: ["a", "b"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", score: mkScore(9.0, "Immediate") }),
mkTrend({ title: "B", url: "https://e/b", topics: ["a", "b"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", score: mkScore(6.0, "High") }),
];
const r1 = rankForBrief(mkStore(trends), pillars, TODAY);
const r2 = rankForBrief(mkStore(trends), pillars, TODAY);
assert.deepEqual(r1.topMatches.map((e) => e.trend.title), r2.topMatches.map((e) => e.trend.title));
});
});
describe("renderBrief — band + mode surfacing (RE-R3a / SC6)", () => {
const pillars = ["AI", "gov"];
test("scored top-entry meta line is the full pinned shape (· <priority> (<mode>) between age and Pillarer)", () => {
// neutral age (4d > firstMoverDays 2) so the RE-R3d temporal overlay adds no badge — isolates the score token.
const store = mkStore([
mkTrend({ title: "Alpha", url: "https://e/a", topics: ["ai", "gov"], publishedAt: "2026-06-20", capturedAt: "2026-06-20", score: mkScore(9.0, "Immediate") }),
]);
const md = renderBrief(rankForBrief(store, pillars, TODAY));
assert.ok(
md.includes("- Kilde: tavily · Publisert: 2026-06-20 (4d) · Immediate (kortform) · Pillarer: AI, gov"),
"scored top-entry meta line must carry · <priority> (<mode>) between (<age>d) and · Pillarer",
);
});
test("unscored top-entry meta line is UNCHANGED (no token)", () => {
const store = mkStore([
mkTrend({ title: "Alpha", url: "https://e/a", topics: ["ai", "gov"], publishedAt: "2026-06-20", capturedAt: "2026-06-20" }),
]);
const md = renderBrief(rankForBrief(store, pillars, TODAY));
assert.ok(
md.includes("- Kilde: tavily · Publisert: 2026-06-20 (4d) · Pillarer: AI, gov"),
"unscored neutral top-entry meta line carries no score and no temporal token",
);
});
test("scored bullet (single match) is the full pinned shape (· <priority> (<mode>) before · 🔗)", () => {
const store = mkStore([
mkTrend({ title: "Beta", url: "https://e/b", topics: ["ai"], publishedAt: "2026-06-20", capturedAt: "2026-06-20", score: mkScore(6.0, "High") }),
]);
const md = renderBrief(rankForBrief(store, pillars, TODAY));
assert.ok(
md.includes("- **Beta** — «AI» · 2026-06-20 (4d) · High (kortform) · 🔗 https://e/b"),
"scored bullet must carry · <priority> (<mode>) before · 🔗",
);
});
test("unscored bullet (single match) is UNCHANGED (no token)", () => {
const store = mkStore([
mkTrend({ title: "Beta", url: "https://e/b", topics: ["ai"], publishedAt: "2026-06-20", capturedAt: "2026-06-20" }),
]);
const md = renderBrief(rankForBrief(store, pillars, TODAY));
assert.ok(
md.includes("- **Beta** — «AI» · 2026-06-20 (4d) · 🔗 https://e/b"),
"unscored bullet must be unchanged",
);
});
test("briefSummary names the band (no mode) on a scored top", () => {
// neutral age (4d) so the RE-R3d first-mover marker stays off — isolates the band token.
const store = mkStore([
mkTrend({ title: "Alpha", url: "https://e/a", topics: ["ai", "gov"], publishedAt: "2026-06-20", capturedAt: "2026-06-20", score: mkScore(9.0, "Immediate") }),
]);
const s = briefSummary(rankForBrief(store, pillars, TODAY));
assert.ok(s.includes("Topp: «Alpha» (AI · Immediate · 4d)."), `summary should carry the band: ${s}`);
assert.ok(!s.includes("kortform"), "summary must not carry the mode");
});
test("briefSummary omits the band token on an unscored top", () => {
const store = mkStore([
mkTrend({ title: "Alpha", url: "https://e/a", topics: ["ai", "gov"], publishedAt: "2026-06-20", capturedAt: "2026-06-20" }),
]);
const s = briefSummary(rankForBrief(store, pillars, TODAY));
assert.ok(s.includes("Topp: «Alpha» (AI · 4d)."), `unscored summary should omit the band: ${s}`);
});
test("briefSummary stays one line, no double-quote, even when the top title contains a guillemet", () => {
const store = mkStore([
mkTrend({ title: "«Quoted» take", url: "https://e/q", topics: ["ai", "gov"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", score: mkScore(9.0, "Immediate") }),
]);
const s = briefSummary(rankForBrief(store, pillars, TODAY));
assert.ok(!s.includes('"'), "summary must not contain a double-quote");
assert.ok(!s.includes("\n"), "summary must be a single line");
assert.ok(s.includes("Immediate"), "summary still carries the band");
});
test("single-pillar unscored top -> summary renders with no · <priority> token, one line", () => {
const store = mkStore([
mkTrend({ title: "Solo", url: "https://e/solo", topics: ["ai"], publishedAt: "2026-06-20", capturedAt: "2026-06-20" }),
]);
const s = briefSummary(rankForBrief(store, ["AI"], TODAY));
assert.ok(!s.includes("· ·"), "no empty priority slot");
assert.ok(s.includes("Topp: «Solo» (AI · 4d)."), `single-pillar unscored summary: ${s}`);
assert.ok(!s.includes("\n"));
});
test("ranking: descriptor equals the exact pinned string", () => {
const store = mkStore([
mkTrend({ title: "Alpha", url: "https://e/a", topics: ["ai", "gov"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", score: mkScore(9.0, "Immediate") }),
]);
const md = renderBrief(rankForBrief(store, pillars, TODAY, { freshDays: 7 }));
assert.ok(
md.includes("ranking: composite desc, then pillar-overlap desc, then temporal (first-mover↑/saturated↓), then publishedAt desc (capturedAt fallback); freshDays 7"),
"the ranking descriptor must carry the RE-R3d temporal key (RE-R3a base + RE-R3d)",
);
});
test("deterministic: identical scored input -> identical bytes", () => {
const store = mkStore([
mkTrend({ title: "Alpha", url: "https://e/a", topics: ["ai", "gov"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", score: mkScore(9.0, "Immediate") }),
mkTrend({ title: "Beta", url: "https://e/b", topics: ["ai"], publishedAt: "2026-06-20", capturedAt: "2026-06-20", score: mkScore(6.0, "High") }),
]);
const r = rankForBrief(store, pillars, TODAY);
assert.equal(renderBrief(r), renderBrief(rankForBrief(store, pillars, TODAY)));
});
});
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;
}
});
});
describe("RE-R3b — exclude acted/skipped (A3)", () => {
const pillars = ["AI", "gov"];
const store = mkStore([
mkTrend({ title: "New top", url: "https://e/n", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23" }),
mkTrend({ title: "Acted top", url: "https://e/a", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", status: "acted" }),
mkTrend({ title: "Skipped single", url: "https://e/s", topics: ["ai"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", status: "skipped" }),
]);
const r = rankForBrief(store, pillars, TODAY);
test("RED: acted/skipped are dropped from every bucket", () => {
assert.deepEqual(r.topMatches.map((e) => e.trend.title), ["New top"]);
assert.deepEqual(r.singleMatches.map((e) => e.trend.title), []);
assert.deepEqual(r.olderMatched.map((e) => e.trend.title), []);
});
test("RED: totals.trends counts the full inventory (incl. handled); matched is post-filter", () => {
assert.equal(r.totals.trends, 3, "full store count");
assert.equal(r.totals.matched, 1, "only the new record is matched");
});
test("RED: a store whose only matches are handled → no fresh + the empty summary", () => {
const s = mkStore([
mkTrend({ title: "A", url: "https://e/aa", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", status: "acted" }),
mkTrend({ title: "B", url: "https://e/bb", topics: ["ai"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", status: "skipped" }),
]);
const rr = rankForBrief(s, pillars, TODAY);
assert.equal(rr.totals.fresh, 0);
assert.match(briefSummary(rr), /Ingen ferske tema-signaler/);
});
});
describe("RE-R3b — render id + surfaced marker + descriptor (D4/D5)", () => {
const pillars = ["AI", "gov"];
test("RED: a top entry carries the id in backticks (copy-paste-ready for act/skip)", () => {
const s = mkStore([mkTrend({ title: "Top", url: "https://e/t", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23" })]);
const md = renderBrief(rankForBrief(s, pillars, TODAY));
assert.ok(md.includes("· `Top|https://e/t`"), "top entry meta line must end with the id in backticks");
});
test("RED: a single-match bullet carries the id in backticks", () => {
const s = mkStore([mkTrend({ title: "Single", url: "https://e/sg", topics: ["ai"], publishedAt: "2026-06-22", capturedAt: "2026-06-22" })]);
const md = renderBrief(rankForBrief(s, pillars, TODAY));
assert.ok(md.includes("· `Single|https://e/sg`"), "bullet must end with the id in backticks");
});
test("· sett Nx appears only when surfacedCount >= 2 (warming badge, RE-R3b contract preserved by RE-R3d)", () => {
const s = mkStore([
mkTrend({ title: "Seen", url: "https://e/seen", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", surfacedCount: 2 }),
mkTrend({ title: "Once", url: "https://e/once", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", surfacedCount: 1 }),
]);
const md = renderBrief(rankForBrief(s, pillars, TODAY));
assert.ok(md.includes("· sett 2x"), "surfacedCount 2 → warming → · sett 2x");
assert.ok(!md.includes("sett 1x"), "surfacedCount 1 → no marker (the preserved ≥2 gate)");
});
test("RED: the ranking: descriptor ends with '; excludes acted/skipped'", () => {
const md = renderBrief(rankForBrief(mkStore([]), pillars, TODAY));
assert.match(
md,
/\nranking: composite desc, then pillar-overlap desc, then temporal \(first-mover↑\/saturated↓\), then publishedAt desc \(capturedAt fallback\); freshDays 7; excludes acted\/skipped\n/,
);
});
});
describe("RE-R3b — surfacedIds (D7, Phase B)", () => {
test("RED: surfacedIds = topMatches singleMatches olderMatched.slice(0,5)", () => {
const olders = Array.from({ length: 7 }, (_, i) =>
mkTrend({ title: "O" + i, url: "https://e/o" + i, topics: ["ai", "gov"], publishedAt: "2026-05-01", capturedAt: "2026-05-01" }),
);
const s = mkStore([
mkTrend({ title: "Top", url: "https://e/t", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23" }),
mkTrend({ title: "Sg", url: "https://e/sg", topics: ["ai"], publishedAt: "2026-06-22", capturedAt: "2026-06-22" }),
...olders,
]);
const r = rankForBrief(s, ["AI", "gov"], TODAY);
const expected = [...r.topMatches, ...r.singleMatches, ...r.olderMatched.slice(0, 5)].map((e) => e.trend.id);
assert.deepEqual(surfacedIds(r), expected);
assert.equal(surfacedIds(r).length, 1 + 1 + 5, "older capped at 5");
});
});
// ── RE-R3d: temporal overlay (first-mover + saturation) ──
describe("temporalSignal — first-mover detection (RE-R3d / SC1)", () => {
const opts = { firstMoverDays: 2, saturationAt: 3 };
test("recent + unsurfaced is first-mover (rank 3); undefined surfacedCount counts as 0", () => {
for (const a of [0, 1, 2]) {
const s = temporalSignal(a, 0, opts);
assert.equal(s.tier, "first-mover", `ageDays ${a} surfaced 0`);
assert.equal(s.firstMover, true);
assert.equal(s.rank, 3);
}
assert.equal(temporalSignal(1, undefined, opts).tier, "first-mover");
});
test("past the window is neutral (rank 2)", () => {
const s = temporalSignal(3, 0, opts);
assert.equal(s.tier, "neutral");
assert.equal(s.firstMover, false);
assert.equal(s.rank, 2);
});
test("recent but already surfaced is NOT first-mover (warming)", () => {
const s = temporalSignal(1, 1, opts);
assert.equal(s.tier, "warming");
assert.equal(s.firstMover, false);
});
test("future publishedAt (negative ageDays) is NOT first-mover (>=0 guard)", () => {
const s = temporalSignal(-1, 0, opts);
assert.equal(s.tier, "neutral");
assert.equal(s.firstMover, false);
});
});
describe("temporalSignal — saturation grading + clamp (RE-R3d / SC2, SC9)", () => {
const opts = { firstMoverDays: 2, saturationAt: 3 };
test("surfacings >= saturationAt is saturated (rank 0, inclusive)", () => {
for (const c of [3, 4]) {
const s = temporalSignal(5, c, opts);
assert.equal(s.tier, "saturated", `surfaced ${c}`);
assert.equal(s.rank, 0);
assert.equal(s.surfacings, c);
}
});
test("1..saturationAt-1 is warming (rank 1)", () => {
for (const c of [1, 2]) {
const s = temporalSignal(5, c, opts);
assert.equal(s.tier, "warming", `surfaced ${c}`);
assert.equal(s.rank, 1);
}
});
test("surfaced 0 (not recent) is neutral (rank 2)", () => {
assert.equal(temporalSignal(5, 0, opts).tier, "neutral");
});
test("defensive clamp: saturationAt 0 does NOT mark every non-first-mover saturated", () => {
assert.equal(temporalSignal(5, 5, { firstMoverDays: 2, saturationAt: 0 }).tier, "saturated");
assert.equal(temporalSignal(5, 0, { firstMoverDays: 2, saturationAt: 0 }).tier, "neutral");
});
test("pure: same inputs -> same output", () => {
assert.deepEqual(temporalSignal(2, 1, opts), temporalSignal(2, 1, opts));
});
});
describe("rankForBrief — temporal overlay re-orders within tier, composite dominates (RE-R3d / SC3)", () => {
const pillars = ["a", "b"];
// DISAGREEMENT fixture: temporal.rank and effectiveDate-desc disagree, so the new key is what decides.
// (surfacedCount correlates with age, so a naive first-mover-vs-saturated fixture would already be ordered
// correctly by the existing effectiveDate key — that test would pass WITHOUT the feature.)
const store = mkStore([
// A: neutral (surfaced 0, ageDays 5 > firstMoverDays), OLDER date, composite 7.0
mkTrend({ title: "A", url: "https://e/a", topics: ["a", "b"], publishedAt: "2026-06-19", capturedAt: "2026-06-19", score: mkScore(7.0, "High") }),
// B: warming (surfaced 2), NEWER date, composite 7.0
mkTrend({ title: "B", url: "https://e/b", topics: ["a", "b"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", surfacedCount: 2, score: mkScore(7.0, "High") }),
// Z: saturated (surfaced 4) but HIGHER composite 8.5
mkTrend({ title: "Z", url: "https://e/z", topics: ["a", "b"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", surfacedCount: 4, score: mkScore(8.5, "Immediate") }),
]);
test("[Z, A, B]: composite primary (Z); then temporal (A neutral > B warming) over newer-date B", () => {
const r = rankForBrief(store, pillars, TODAY);
assert.deepEqual(r.topMatches.map((e) => e.trend.title), ["Z", "A", "B"]);
});
test("total order: deterministic regardless of store insertion order", () => {
const reordered = mkStore([store.trends[2], store.trends[0], store.trends[1]]);
assert.deepEqual(rankForBrief(reordered, pillars, TODAY).topMatches.map((e) => e.trend.title), ["Z", "A", "B"]);
});
});
describe("renderBrief — temporal badges + the >=2 boundary (RE-R3d / SC4)", () => {
const pillars = ["AI", "gov"];
const render = (p: Parameters<typeof mkTrend>[0]): string => renderBrief(rankForBrief(mkStore([mkTrend(p)]), pillars, TODAY));
test("first-mover entry carries · 🥇 først ute", () => {
const md = render({ title: "FM", url: "https://e/fm", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23" });
assert.ok(md.includes("· 🥇 først ute"), "ageDays 1 + unsurfaced -> first-mover badge");
});
test("saturated entry carries · 🔁 mettet (3x)", () => {
const md = render({ title: "Sat", url: "https://e/sat", topics: ["ai", "gov"], publishedAt: "2026-06-20", capturedAt: "2026-06-20", surfacedCount: 3 });
assert.ok(md.includes("· 🔁 mettet (3x)"), "surfacedCount 3 -> saturated badge");
});
test("warming entry with surfacedCount 2 carries · sett 2x", () => {
const md = render({ title: "Warm", url: "https://e/warm", topics: ["ai", "gov"], publishedAt: "2026-06-20", capturedAt: "2026-06-20", surfacedCount: 2 });
assert.ok(md.includes("· sett 2x"), "surfacedCount 2 -> warming badge (>=2)");
});
test("warming entry with surfacedCount 1 carries NO badge (preserves the R3b >=2 contract)", () => {
const md = render({ title: "Once", url: "https://e/once", topics: ["ai", "gov"], publishedAt: "2026-06-20", capturedAt: "2026-06-20", surfacedCount: 1 });
assert.ok(!md.includes("sett 1x"), "surfacedCount 1 -> no marker");
assert.ok(!md.includes("mettet"), "surfacedCount 1 is not saturated");
});
test("neutral entry carries none of the temporal badges", () => {
const md = render({ title: "Neu", url: "https://e/neu", topics: ["ai", "gov"], publishedAt: "2026-06-20", capturedAt: "2026-06-20" });
assert.ok(!md.includes("først ute") && !md.includes("mettet") && !md.includes("· sett "), "ageDays 4 + unsurfaced -> neutral");
});
});
describe("briefSummary — first-mover marker (RE-R3d / SC5)", () => {
const pillars = ["AI", "gov"];
test("first-mover top carries · 🥇 først ute and stays regex-safe", () => {
const s = briefSummary(rankForBrief(mkStore([
mkTrend({ title: "Fresh", url: "https://e/f", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", score: mkScore(9.0, "Immediate") }),
]), pillars, TODAY));
assert.ok(s.includes("· 🥇 først ute"), `first-mover summary marker: ${s}`);
assert.ok(!s.includes('"') && !s.includes("\n"), "summary stays single-line, no double-quote");
});
test("non-first-mover top omits the marker", () => {
const s = briefSummary(rankForBrief(mkStore([
mkTrend({ title: "Old", url: "https://e/o", topics: ["ai", "gov"], publishedAt: "2026-06-20", capturedAt: "2026-06-20", score: mkScore(9.0, "Immediate") }),
]), pillars, TODAY));
assert.ok(!s.includes("først ute"), `neutral top, no marker: ${s}`);
});
});
describe("rankForBrief — no schema/score mutation (RE-R3d / SC8)", () => {
test("ranking does not mutate score.composite; schema versions unchanged", () => {
const t = mkTrend({ title: "X", url: "https://e/x", topics: ["ai"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", score: mkScore(8.0, "Immediate") });
const before = t.score!.composite;
rankForBrief(mkStore([t]), ["AI"], TODAY);
assert.equal(t.score!.composite, before, "rankForBrief must not mutate the stored composite");
assert.equal(BRIEF_SCHEMA_VERSION, 2); // RE-R3e: artifact frontmatter gained surfaced: (1->2); store SCHEMA_VERSION stays 4
assert.equal(SCHEMA_VERSION, 4);
});
});
describe("rankForBrief — prior-day surfacings exclude today (RE-R3d / same-day re-run idempotency)", () => {
const pillars = ["AI", "gov"];
test("a first-mover trend already surfaced TODAY stays first-mover (today excluded from the prior count)", () => {
// surfacedCount 1 but lastSurfacedAt === today -> prior-day count 0 -> still first-mover, so a
// same-day re-render (which loads the post-mark count) is byte-identical to the first run (RE-R3c SC7).
const s = mkStore([
mkTrend({ title: "FM", url: "https://e/fm", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", surfacedCount: 1, lastSurfacedAt: TODAY }),
]);
const e = rankForBrief(s, pillars, TODAY).topMatches[0];
assert.equal(e.temporal.tier, "first-mover", "today's own surfacing must not demote it out of first-mover");
assert.equal(e.temporal.surfacings, 0, "prior-day surfacings exclude today");
});
test("the same count surfaced on a PRIOR day is warming (today not excluded)", () => {
const s = mkStore([
mkTrend({ title: "W", url: "https://e/w", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", surfacedCount: 1, lastSurfacedAt: "2026-06-23" }),
]);
const e = rankForBrief(s, pillars, TODAY).topMatches[0];
assert.equal(e.temporal.tier, "warming", "a prior-day surfacing counts");
assert.equal(e.temporal.surfacings, 1);
});
});
// --- RE-R3e: brief history + day-over-day diff ---
describe("diffSurfaced — partitions + order + empty prior (RE-R3e / SC1)", () => {
test("symmetric set difference, order-stable", () => {
const d = diffSurfaced(["a", "b", "c"], ["b", "c", "d"], "2026-06-25");
assert.deepEqual(d, { priorDate: "2026-06-25", added: ["a"], carried: ["b", "c"], dropped: ["d"] });
});
test("empty prior ⇒ everything added, nothing dropped", () => {
const d = diffSurfaced(["a", "b"], [], null);
assert.deepEqual(d, { priorDate: null, added: ["a", "b"], carried: [], dropped: [] });
});
test("the three partitions are mutually disjoint (cross-partition exclusivity)", () => {
const d = diffSurfaced(["a", "b", "c"], ["b", "c", "d"], "2026-06-25");
const all = [...d.added, ...d.carried, ...d.dropped];
assert.equal(new Set(all).size, all.length, "no id appears in two buckets");
});
test("pure: same inputs → same output", () => {
assert.deepEqual(diffSurfaced(["x"], ["y"], "2026-06-20"), diffSurfaced(["x"], ["y"], "2026-06-20"));
});
});
describe("parseSurfacedFrontmatter — read + degrade (RE-R3e / SC2)", () => {
const block = (surfaced: string) =>
`---\ndate: 2026-06-26\nsummary: hi\nsurfaced: ${surfaced}\nstore: { trends: 3 }\nschemaVersion: 2\n---\nbody`;
test("parses a csv of ids", () => {
assert.deepEqual(parseSurfacedFrontmatter(block("1a2b,3c4d,5e6f")), ["1a2b", "3c4d", "5e6f"]);
});
test("trims whitespace around ids", () => {
assert.deepEqual(parseSurfacedFrontmatter(block(" a , b ,c ")), ["a", "b", "c"]);
});
test("a blank surfaced: line → []", () => {
assert.deepEqual(parseSurfacedFrontmatter(block("")), []);
});
test("an absent surfaced: line (a pre-R3e brief) → []", () => {
assert.deepEqual(parseSurfacedFrontmatter(`---\ndate: 2026-06-26\nsummary: hi\nschemaVersion: 1\n---\nbody`), []);
});
test("line-anchored: a summary: with commas does not leak in", () => {
assert.deepEqual(parseSurfacedFrontmatter(`---\ndate: 2026-06-26\nsummary: a,b,c\nschemaVersion: 1\n---`), []);
});
test("never throws on malformed input", () => {
assert.doesNotThrow(() => parseSurfacedFrontmatter("not a brief at all"));
});
});
describe("selectPriorBriefFile — strict-prior selection (RE-R3e / SC3)", () => {
const files = ["2026-06-24.md", "2026-06-25.md", "2026-06-26.md", "README.md", "2026-06-30.md"];
test("greatest dated file strictly < today (excludes today + future, ignores non-dated)", () => {
assert.equal(selectPriorBriefFile(files, "2026-06-26"), "2026-06-25.md");
});
test("no file < today → null", () => {
assert.equal(selectPriorBriefFile(["2026-06-26.md", "2026-06-30.md"], "2026-06-26"), null);
});
test("empty list → null", () => {
assert.equal(selectPriorBriefFile([], "2026-06-26"), null);
});
});
describe("renderBrief/briefSummary — surfaced: + diff section + marker (RE-R3e / SC4SC8)", () => {
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", score: mkScore(9.0, "Immediate") }),
mkTrend({ title: "Beta", url: "https://e/b", topics: ["ai"], publishedAt: "2026-06-20", capturedAt: "2026-06-20", score: mkScore(7.0, "High") }),
]);
const r = rankForBrief(store, pillars, TODAY);
const ids = surfacedIds(r);
const emptyDiff = { priorDate: null, added: [], carried: [], dropped: [] };
test("SC4: one surfaced: line = surfacedIds(r).join(','), before schemaVersion: 2", () => {
const md = renderBrief(r, emptyDiff);
const m = md.match(/^surfaced: (.*)$/m);
assert.ok(m, "a surfaced: frontmatter line exists");
assert.equal(m![1], ids.join(","));
assert.ok(md.indexOf("\nsurfaced:") < md.indexOf("\nschemaVersion:"), "surfaced: precedes schemaVersion:");
assert.match(md, /\nschemaVersion: 2\n/);
});
test("SC4: round-trips via parseSurfacedFrontmatter", () => {
assert.deepEqual(parseSurfacedFrontmatter(renderBrief(r, emptyDiff)), ids);
});
test("SC4: an empty store → a blank surfaced: line", () => {
const empty = rankForBrief(mkStore([]), pillars, TODAY);
assert.match(renderBrief(empty, emptyDiff), /\nsurfaced: \n/);
});
test("SC5: section header present + precedes Topp-treff (delta leads)", () => {
const md = renderBrief(r, emptyDiff);
assert.ok(md.includes("## 🆕 Nytt siden sist"), "section header present");
assert.ok(md.indexOf("## 🆕 Nytt siden sist") < md.indexOf("## 🎯 Topp-treff"), "delta precedes the ranked list");
});
test("SC5: first brief with added → 'Første brief — alt nedenfor er nytt'", () => {
assert.ok(renderBrief(r, { priorDate: null, added: ids, carried: [], dropped: [] }).includes("Første brief — alt nedenfor er nytt"));
});
test("SC5: empty first brief → 'Første brief.'", () => {
assert.ok(renderBrief(r, emptyDiff).includes("_Første brief._"));
});
test("SC5: prior + added → the added title + the carried/dropped tally", () => {
const md = renderBrief(r, { priorDate: "2026-06-23", added: [ids[0]], carried: [ids[1]], dropped: ["gone"] });
assert.ok(md.includes("Alpha"), "the added entry's title is rendered");
assert.ok(md.includes("1 båret over, 1 ikke vist i dag"), "the carried/dropped tally");
});
test("SC5: prior + no added → 'Ingenting nytt siden <date>' + tally", () => {
const md = renderBrief(r, { priorDate: "2026-06-23", added: [], carried: ids, dropped: [] });
assert.ok(md.includes("Ingenting nytt siden 2026-06-23"));
assert.ok(md.includes("båret over"));
});
test("SC6: marker ' N nye siden sist.' when prior + added", () => {
const s = briefSummary(r, { priorDate: "2026-06-25", added: ["x", "y"], carried: [], dropped: [] });
assert.ok(s.endsWith(" 2 nye siden sist."), `marker appended: ${s}`);
});
test("SC6: no marker on the first brief or when nothing new", () => {
assert.ok(!briefSummary(r, { priorDate: null, added: ["x"], carried: [], dropped: [] }).includes("nye siden sist"));
assert.ok(!briefSummary(r, { priorDate: "2026-06-25", added: [], carried: [], dropped: [] }).includes("nye siden sist"));
});
test("SC6: briefSummary(r) === briefSummary(r, emptyDiff) (keeps the :166 summary-equality test green)", () => {
assert.equal(briefSummary(r), briefSummary(r, emptyDiff));
});
test("SC6: the marker carries no quote/newline", () => {
const s = briefSummary(r, { priorDate: "2026-06-25", added: ["x"], carried: [], dropped: [] });
assert.ok(!s.includes('"') && !s.includes("\n"));
});
test("SC7: BRIEF_SCHEMA_VERSION === 2; SCHEMA_VERSION === 4", () => {
assert.equal(BRIEF_SCHEMA_VERSION, 2);
assert.equal(SCHEMA_VERSION, 4);
});
test("SC8: same (r, diff) → byte-identical render", () => {
const d = { priorDate: "2026-06-23", added: [ids[0]], carried: [ids[1]], dropped: [] };
assert.equal(renderBrief(r, d), renderBrief(r, d));
});
});