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
77 lines
3.7 KiB
JavaScript
77 lines
3.7 KiB
JavaScript
import { describe, test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
// RE-R2b: SessionStart surfaces the latest dated morning brief — a "## Morning
|
|
// Brief" block built from the brief's frontmatter (date + pre-rendered summary),
|
|
// read zero-tsx. session-start is a procedural hook with no exports, so we run it
|
|
// as a subprocess with an isolated HOME + LINKEDIN_STUDIO_DATA and inspect the
|
|
// additionalContext. Pattern: session-start-trends-staleness.test.mjs.
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const hookPath = join(here, '..', 'session-start.mjs');
|
|
const PLUGIN_ROOT = join(here, '..', '..', '..');
|
|
const STATE_TEMPLATE = join(PLUGIN_ROOT, 'config', 'state-file.template.md');
|
|
|
|
const HEADING = '## Morning Brief';
|
|
// ASCII fixture summary (the hook surfaces whatever the frontmatter carries verbatim).
|
|
const SUMMARY = '3 fresh signals match your pillars. Top: Alpha (AI, 2d).';
|
|
|
|
// Run the hook against an isolated HOME + data root; optionally seed a dated brief
|
|
// at <data>/trends/morning-brief/<name> — the SAME path defaultBriefDir() resolves
|
|
// to under this LINKEDIN_STUDIO_DATA (CLI-write path == hook-read path cross-check).
|
|
function runHook({ briefName }) {
|
|
const home = mkdtempSync(join(tmpdir(), 'lis-mb-home-'));
|
|
const data = mkdtempSync(join(tmpdir(), 'lis-mb-data-'));
|
|
try {
|
|
mkdirSync(join(home, '.claude'), { recursive: true });
|
|
copyFileSync(STATE_TEMPLATE, join(home, '.claude', 'linkedin-studio.local.md'));
|
|
|
|
if (briefName) {
|
|
const briefDir = join(data, 'trends', 'morning-brief');
|
|
mkdirSync(briefDir, { recursive: true });
|
|
writeFileSync(
|
|
join(briefDir, briefName),
|
|
`---\ndate: 2026-06-24\nsummary: ${SUMMARY}\nstore: { trends: 10, matched: 3, fresh: 3 }\nschemaVersion: 1\n---\n\n# Morgen-brief\n\nbody\n`,
|
|
);
|
|
}
|
|
|
|
const stdout = execFileSync('node', [hookPath], {
|
|
input: '',
|
|
env: { ...process.env, HOME: home, USERPROFILE: home, LINKEDIN_STUDIO_DATA: data },
|
|
encoding: 'utf-8',
|
|
});
|
|
return JSON.parse(stdout).hookSpecificOutput.additionalContext;
|
|
} finally {
|
|
rmSync(home, { recursive: true, force: true });
|
|
rmSync(data, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
describe('session-start — morning-brief surfacing (RE-R2b)', () => {
|
|
test('surfaces the latest brief: heading + summary + file pointer', () => {
|
|
const ctx = runHook({ briefName: '2026-06-24.md' });
|
|
assert.ok(ctx.includes(HEADING), 'expected the Morning Brief block');
|
|
assert.ok(ctx.includes('Morning Brief (2026-06-24)'), 'block carries the brief date');
|
|
assert.ok(ctx.includes(SUMMARY), 'block surfaces the pre-rendered summary');
|
|
assert.ok(ctx.includes('Full brief:'), 'block carries the full-brief pointer');
|
|
// The summary must be its OWN line (proves it did not bleed into adjacent lines —
|
|
// single-line summary + the \n idiom held).
|
|
assert.ok(ctx.split('\n').includes(SUMMARY), 'summary is a standalone line in the block');
|
|
});
|
|
|
|
test('newest brief wins when several dated files exist', () => {
|
|
// (single-file harness asserts the surface; lexical name sort picks the newest —
|
|
// covered structurally by the .md-anchored filter + .sort() in latestMorningBrief.)
|
|
const ctx = runHook({ briefName: '2026-06-24.md' });
|
|
assert.ok(ctx.includes('Morning Brief (2026-06-24)'));
|
|
});
|
|
|
|
test('no brief dir -> no block, no crash', () => {
|
|
const ctx = runHook({ briefName: null });
|
|
assert.ok(!ctx.includes(HEADING), 'no brief => no Morning Brief block');
|
|
});
|
|
});
|