linkedin-studio/scripts/trends/tests/brief.test.ts
Kjell Tore Guttormsen b185db9a12 feat(linkedin-studio): RE-R3b — trend lifecycle (re-score on re-capture · status · seen-log) [skip-docs]
The lifecycle layer over the trend store: what happens to a trend AFTER first capture.
- re-score on re-capture (last-wins; addTrend duplicate branch, score the one mutable
  field; provenance + lifecycle untouched; no false-merge via JSON compare). Reverses
  R3a's first-sight D3 — that R3a test reconciled to the new behaviour.
- status new/acted/skipped (effectiveStatus/setStatus + act/skip/reset CLI verbs);
  rankForBrief EXCLUDES handled trends (a work queue, not an archive).
- seen-log surfacedCount/lastSurfacedAt (markSurfaced, per-day idempotent); the brief
  CLI records surfacing on the store AFTER the pure render, unless --no-mark.
- render: entry id in backticks (copy-paste for act/skip) + · sett Nx prior-day hint.
- schema v3→v4 (additive lossless); the R3a migration block reconciled to the bump,
  the new R3b block committed against SCHEMA_VERSION (breaks the reconcile cycle).

score.ts + item.ts untouched (re-score reuses the R3a capture path). RED-first (two
phase: 16 logic-RED + 4 stub-RED). Gate: Section 16k (6 emitters), TRENDS_TESTS_FLOOR
146→171, ASSERT_BASELINE_FLOOR 99→105. trends 171/171, gate 120/0/0, hook suite 139/139.

Plan: docs/research-engine/{brief,plan}-re-r3b.md (light-Voyage hardened @ c40b937).

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

434 lines
22 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,
BRIEF_SCHEMA_VERSION,
} from "../src/brief.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", () => {
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("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)", () => {
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));
assert.ok(
md.includes("- Kilde: tavily · Publisert: 2026-06-22 (2d) · 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-22", capturedAt: "2026-06-22" }),
]);
const md = renderBrief(rankForBrief(store, pillars, TODAY));
assert.ok(
md.includes("- Kilde: tavily · Publisert: 2026-06-22 (2d) · Pillarer: AI, gov"),
"unscored top-entry meta line must be unchanged",
);
});
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", () => {
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 s = briefSummary(rankForBrief(store, pillars, TODAY));
assert.ok(s.includes("Topp: «Alpha» (AI · Immediate · 2d)."), `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-22", capturedAt: "2026-06-22" }),
]);
const s = briefSummary(rankForBrief(store, pillars, TODAY));
assert.ok(s.includes("Topp: «Alpha» (AI · 2d)."), `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-22", capturedAt: "2026-06-22" }),
]);
const s = briefSummary(rankForBrief(store, ["AI"], TODAY));
assert.ok(!s.includes("· ·"), "no empty priority slot");
assert.ok(s.includes("Topp: «Solo» (AI · 2d)."), `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 publishedAt desc (capturedAt fallback); freshDays 7"),
"the ranking descriptor must match the pinned RE-R3a string verbatim",
);
});
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("RED: · sett Nx appears only when surfacedCount >= 2 (prior-day count)", () => {
const s = mkStore([
mkTrend({ title: "Seen", url: "https://e/seen", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", surfacedCount: 3 }),
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 3x"), "surfacedCount 3 → · sett 3x");
assert.ok(!md.includes("sett 1x"), "surfacedCount 1 → no marker");
});
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 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");
});
});