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 /trends/morning-brief/ — 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'); }); });