import { describe, it, expect } from 'vitest'; import { renderStatus, computeCostBlock, computeAnomalyBlock, computeSelfRetrospectBlock, computeReviewerBlock, computeReviewerFindingsBlock, computeSessionLengthBlock } from './status-md-generator.mjs'; // brain-retro #9 candidate 5 (2026-05-28): surface long-running system processes in STATUS.md. // Block is passed as pre-computed string `systemHealthBlock` in inputs — same wiring pattern // as sessionLengthBlock, costBlock, etc. (no I/O in renderStatus). describe('renderStatus — systemHealthBlock (brain-retro #9 candidate 5)', () => { const minInputs = { now: '2026-05-28T10:00:00Z', c1: { status: 'ok', detail: 'OK' }, c2: { status: 'ok', detail: 'OK' }, c3: { status: 'ok', detail: 'OK' }, c5: { status: 'ok', detail: 'OK' }, c6: { status: 'ok', detail: 'OK' }, observer: { episodeCount: 5, observerErrors: 0, piiMatches: 0, v1Episodes: 0 }, missed: { totalMissed: 0, byNode: {}, byClassification: {} }, }; it('renders systemHealthBlock when provided as string', () => { const block = '## System Health\n\nТоп-3 процессов с CPU > 1ч:\n\n| PID | Имя | CPU-время | Возраст |\n|---|---|---|---|\n| 6444 | python | 7.07ч | 13.6ч |\n\n⚠️ Проверь, не «осиротевшие» ли это процессы.\n'; const md = renderStatus({ ...minInputs, systemHealthBlock: block }); expect(md).toContain('## System Health'); expect(md).toContain('6444'); expect(md).toContain('python'); }); it('omits systemHealthBlock when absent (backward compat)', () => { const md = renderStatus(minInputs); // Should not contain System Health section expect(md).not.toContain('## System Health'); }); it('renders "Долго работающих процессов нет" variant correctly', () => { const block = '## System Health\n\nДолго работающих процессов нет (порог CPU > 1ч).\n'; const md = renderStatus({ ...minInputs, systemHealthBlock: block }); expect(md).toContain('Долго работающих процессов нет'); }); }); const baseInputs = (overrides = {}) => ({ now: '2026-05-19T10:00:00+03:00', c1: { status: 'ok', detail: 'no drift' }, c2: { status: 'ok', detail: '0 version drift' }, c3: { status: 'ok', detail: 'last read today' }, c5: { status: 'ok', detail: 'coverage OK · registration OK' }, c6: { status: 'ok', detail: '14 chains in sync' }, observer: { episodeCount: 12, observerErrors: 0, piiMatches: 0 }, missed: { totalMissed: 0, byNode: {}, byClassification: {} }, ...overrides, }); describe('renderStatus', () => { it('renders all 5 controllers + metrics', () => { const md = renderStatus(baseInputs()); expect(md).toContain('# Brain Status'); expect(md).toContain('| C1 L1-watcher | ✅'); expect(md).toContain('| C2 Cross-ref consistency | ✅'); expect(md).toContain('| C3 Observer-of-observer | ✅'); expect(md).toContain('| C4 Сигнальный статус | ✅'); expect(md).toContain('| C5 Observer-coverage | ✅'); expect(md).toContain('12 episodes'); }); it('includes a C6 chain-map row', () => { const md = renderStatus(baseInputs()); expect(md).toContain('| C6 Chain map sync | ✅'); }); it('shows a warn status for the coverage controller', () => { const md = renderStatus(baseInputs({ c5: { status: 'warn', detail: '3 commits, 0 episodes' } })); expect(md).toContain('| C5 Observer-coverage | ⚠️'); }); it('shows the observer_error count in the metrics block', () => { const md = renderStatus(baseInputs({ observer: { episodeCount: 4, observerErrors: 2, piiMatches: 0 } })); expect(md).toContain('2 observer_error markers'); }); it('shows a red status for failing controllers', () => { const md = renderStatus(baseInputs({ c1: { status: 'fail', detail: '2 plugins not formalized' } })); expect(md).toContain('| C1 L1-watcher | 🔴'); }); it('mentions the conditional capability-readiness behavioral rule (§16.4 v1.36)', () => { const md = renderStatus(baseInputs()); expect(md).toContain('Неиспользованные узлы — не алерт'); expect(md).toContain('если профильной задачи не было'); expect(md).toContain('feedback_brain_unused_tools_not_problem'); }); it('shows piiMatches > 0 when counter file has data (Task 3)', () => { const md = renderStatus(baseInputs({ observer: { episodeCount: 24, observerErrors: 0, piiMatches: 7 } })); expect(md).toMatch(/7 PII matches before filter/); }); }); describe('renderStatus — last /brain-retro (Task 10)', () => { it('shows last /brain-retro days-ago when counter has data', () => { const md = renderStatus(baseInputs({ lastRetroDaysAgo: 3 })); expect(md).toMatch(/Last \/brain-retro:\s*3 day\(s\) ago/); }); it('shows "never" when lastRetroDaysAgo is null', () => { const md = renderStatus(baseInputs({ lastRetroDaysAgo: null })); expect(md).toMatch(/Last \/brain-retro:\s*never/); }); it('shows "never" when lastRetroDaysAgo is undefined', () => { const md = renderStatus(baseInputs()); expect(md).toMatch(/Last \/brain-retro:\s*never/); }); }); describe('renderStatus — v1 episodes count surface (Task 18)', () => { it('shows v1 count when present', () => { const md = renderStatus(baseInputs({ observer: { episodeCount: 22, observerErrors: 0, piiMatches: 0, v1Episodes: 5 } })); expect(md).toMatch(/Legacy v1 episodes \(not in factor analysis\):\s*5/); }); it('shows 0 when v1Episodes undefined', () => { const md = renderStatus(baseInputs()); expect(md).toMatch(/Legacy v1 episodes \(not in factor analysis\):\s*0/); }); }); describe('renderStatus — missed activations (Task 7, Pravila §16.4 v1.36)', () => { it('renders missed_activations: 0 when there are no misses', () => { const md = renderStatus(baseInputs()); expect(md).toContain('missed_activations: 0'); }); it('renders missed_activations: N when misses occur', () => { const md = renderStatus(baseInputs({ missed: { totalMissed: 3, byNode: { '#11': 2, '#12': 1 }, byClassification: { refactor: 3 } }, })); expect(md).toContain('missed_activations: 3'); }); it('keeps C5 ✅ when controller is ok and no misses', () => { const md = renderStatus(baseInputs()); expect(md).toContain('| C5 Observer-coverage | ✅'); }); it('honors the c5 status override (warn) regardless of missed count', () => { const md = renderStatus(baseInputs({ c5: { status: 'warn', detail: '16 missed activation(s)' }, })); expect(md).toContain('| C5 Observer-coverage | ⚠️'); }); }); describe('renderStatus — discipline block (stage 2)', () => { const baseInputs = { now: '2026-05-24T10:00:00Z', c1: { status: 'ok', detail: 'OK' }, c2: { status: 'ok', detail: 'OK' }, c3: { status: 'ok', detail: 'OK' }, c5: { status: 'ok', detail: 'OK' }, c6: { status: 'ok', detail: 'OK' }, observer: { episodeCount: 10, observerErrors: 0, piiMatches: 0, v1Episodes: 0 }, missed: { totalMissed: 0, byNode: {}, byClassification: {} }, lastRetroDaysAgo: 0, }; it('renders discipline table when discipline data is provided', () => { const md = renderStatus({ ...baseInputs, discipline: { byClassification: { feature: { episodes: 5, withTriggerMatch: 0, viaSkill: 0, pctTriggerMatch: 0, pctViaSkill: 0 }, bugfix: { episodes: 6, withTriggerMatch: 2, viaSkill: 2, pctTriggerMatch: 0.333, pctViaSkill: 0.333 }, }, routerStep: { distribution: { '1': 10, '3': 1 }, total: 11, suspicious: true }, boundariesRate: { total: 11, withBoundaries: 3, rate: 0.273, byPathType: {} }, }, }); expect(md).toMatch(/## Метрики дисциплины/); expect(md).toMatch(/feature/); expect(md).toMatch(/bugfix/); expect(md).toMatch(/33\.3%/); expect(md).toMatch(/router step distribution/i); expect(md).toMatch(/⚠️.*suspicious/i); expect(md).toMatch(/boundaries applied/i); expect(md).toMatch(/27\.3%/); }); it('omits the discipline block when discipline is absent (backward compat)', () => { const md = renderStatus(baseInputs); expect(md).not.toMatch(/## Метрики дисциплины/); }); it('coexists: both sessionLengthBlock (brain-retro candidate B) and overrideUsageBlock (enforce hole 8) appear together in template after merge', () => { const md = renderStatus({ ...baseInputs, sessionLengthBlock: '## Длинные сессии\n\nflagged content', overrideUsageBlock: '## Использование override-фраз\n\nflagged content', }); expect(md).toContain('## Длинные сессии'); expect(md).toContain('## Использование override-фраз'); }); }); // ── Phase 3 deferred #3: 4 new helper blocks ───────────────────────────────── const PRICING_TEST = { sonnet46: { input_per_mtok: 3.0, output_per_mtok: 15.0 }, opus47: { input_per_mtok: 15.0, output_per_mtok: 75.0 }, }; const makeEp = (overrides = {}) => ({ schema_version: 2, task_cost: { classifier_input_tokens: 0, classifier_output_tokens: 0, self_assessment_input_tokens: 0, self_assessment_output_tokens: 0, reviewer_subagent_usd: null, reviewer_input_tokens: 0, reviewer_output_tokens: 0, reviewer_direct_fallback_usd: null, }, review: { reviewed_at: null, reviewer: null, reviewer_error: false }, ...overrides, }); describe('computeCostBlock', () => { it('sums token costs and formats USD for 3 episodes', () => { const eps = [ makeEp({ task_cost: { classifier_input_tokens: 1000, classifier_output_tokens: 200, self_assessment_input_tokens: 500, self_assessment_output_tokens: 100, reviewer_subagent_usd: null, reviewer_input_tokens: 0, reviewer_output_tokens: 0, reviewer_direct_fallback_usd: null } }), makeEp({ task_cost: { classifier_input_tokens: 2000, classifier_output_tokens: 400, self_assessment_input_tokens: 1000, self_assessment_output_tokens: 200, reviewer_subagent_usd: 0.01, reviewer_input_tokens: 500, reviewer_output_tokens: 100, reviewer_direct_fallback_usd: null } }), makeEp({ task_cost: { classifier_input_tokens: 500, classifier_output_tokens: 100, self_assessment_input_tokens: 250, self_assessment_output_tokens: 50, reviewer_subagent_usd: null, reviewer_input_tokens: 0, reviewer_output_tokens: 0, reviewer_direct_fallback_usd: 0.005 } }), ]; const block = computeCostBlock(eps, PRICING_TEST); expect(block).toMatch(/## Стоимость месяца/); expect(block).toMatch(/Classifier/); expect(block).toMatch(/Self-assessment/); expect(block).toMatch(/Reviewer/); expect(block).toMatch(/\$\d+\.\d{2}/); }); it('returns a block with zeros when episodes array is empty', () => { const block = computeCostBlock([], PRICING_TEST); expect(block).toMatch(/## Стоимость месяца/); expect(block).toMatch(/\$0\.00/); }); }); describe('computeAnomalyBlock', () => { it('returns "Аномалий нет." when all outputs are within threshold', () => { const eps = [ makeEp({ task_cost: { ...makeEp().task_cost, classifier_output_tokens: 100 } }), makeEp({ task_cost: { ...makeEp().task_cost, classifier_output_tokens: 120 } }), makeEp({ task_cost: { ...makeEp().task_cost, classifier_output_tokens: 110 } }), ]; const block = computeAnomalyBlock(eps); expect(block).toMatch(/## Аномалии классификатора/); expect(block).toMatch(/Аномалий нет\./); }); it('lists the outlier episode when one exceeds threshold', () => { const eps = [ makeEp({ task_cost: { ...makeEp().task_cost, classifier_output_tokens: 100 } }), makeEp({ task_cost: { ...makeEp().task_cost, classifier_output_tokens: 100 } }), makeEp({ task_cost: { ...makeEp().task_cost, classifier_output_tokens: 100 } }), makeEp({ task_cost: { ...makeEp().task_cost, classifier_output_tokens: 50000 } }), ]; const block = computeAnomalyBlock(eps); expect(block).toMatch(/## Аномалии классификатора/); expect(block).toMatch(/50000/); }); }); describe('computeSelfRetrospectBlock', () => { it('returns block with days-ago and no warning when under threshold', () => { const fakeFs = { existsSync: () => true, readFileSync: () => JSON.stringify({ last_run_at: new Date(Date.now() - 2 * 86400000).toISOString(), episodes_since_last: 3, threshold: 10 }), }; const block = computeSelfRetrospectBlock('fake/path.json', fakeFs); expect(block).toMatch(/## Авто-ретроспектива/); expect(block).toMatch(/2 day\(s\) ago/); expect(block).not.toMatch(/⚠️/); }); it('adds warning when episodes_since_last >= threshold', () => { const fakeFs = { existsSync: () => true, readFileSync: () => JSON.stringify({ last_run_at: new Date(Date.now() - 5 * 86400000).toISOString(), episodes_since_last: 15, threshold: 10 }), }; const block = computeSelfRetrospectBlock('fake/path.json', fakeFs); expect(block).toMatch(/⚠️/); expect(block).toMatch(/15/); }); it('returns "never" when counter file is missing', () => { const fakeFs = { existsSync: () => false }; const block = computeSelfRetrospectBlock('fake/path.json', fakeFs); expect(block).toMatch(/## Авто-ретроспектива/); expect(block).toMatch(/never/); }); }); describe('computeReviewerBlock', () => { it('shows subagent and fallback counts with percentages', () => { const eps = [ makeEp({ review: { reviewed_at: '2026-05-01T00:00:00Z', reviewer: 'subagent-opus-4-7', reviewer_error: false } }), makeEp({ review: { reviewed_at: '2026-05-02T00:00:00Z', reviewer: 'subagent-opus-4-7', reviewer_error: false } }), makeEp({ review: { reviewed_at: '2026-05-03T00:00:00Z', reviewer: 'direct-opus-fallback', reviewer_error: false } }), makeEp({ review: { reviewed_at: null, reviewer: null, reviewer_error: false } }), ]; const block = computeReviewerBlock(eps); expect(block).toMatch(/## Reviewer: субагент vs fallback/); expect(block).toMatch(/subagent-opus-4-7/); expect(block).toMatch(/direct-opus-fallback/); expect(block).toMatch(/\d+%/); }); it('shows fallback message when no episodes were reviewed', () => { const eps = [ makeEp(), makeEp(), ]; const block = computeReviewerBlock(eps); expect(block).toMatch(/## Reviewer: субагент vs fallback/); expect(block).toMatch(/0 эпизодов проверено/); }); }); // brain-retro #7 C4 (2026-05-27): surface reviewer findings (wrong_skill, // wrong_chain_order, alternative suggestions) directly in STATUS.md so the // owner doesn't have to run /brain-retro to see them. describe('computeReviewerFindingsBlock (C4 — brain-retro #7)', () => { it('counts error_root_cause and alternative_better across reviewed episodes', () => { const eps = [ makeEp({ review: { reviewed_at: '2026-05-26T17:00:00Z', error_root_cause: 'wrong_skill', alternative_better: '#3', node_quality: 'wrong_node' } }), makeEp({ review: { reviewed_at: '2026-05-26T17:10:00Z', error_root_cause: 'wrong_skill', alternative_better: '#3', node_quality: 'disputable' } }), makeEp({ review: { reviewed_at: '2026-05-26T17:20:00Z', error_root_cause: 'wrong_chain_order', alternative_better: '#8', node_quality: 'disputable' } }), makeEp({ review: { reviewed_at: '2026-05-26T17:30:00Z', error_root_cause: 'external_failure', alternative_better: null, node_quality: 'correct' } }), makeEp({ review: { reviewed_at: '2026-05-26T17:40:00Z', error_root_cause: 'n/a', alternative_better: null, node_quality: 'correct' } }), makeEp({ review: { reviewed_at: null } }), // not reviewed — excluded ]; const block = computeReviewerFindingsBlock(eps); expect(block).toMatch(/Reviewer findings/); expect(block).toMatch(/wrong_skill.*2/); expect(block).toMatch(/wrong_chain_order.*1/); expect(block).toMatch(/external_failure.*1/); // Top alternatives: #3 (x2), #8 (x1) expect(block).toMatch(/#3.*2/); expect(block).toMatch(/#8.*1/); }); it('emits "никаких находок" when 0 reviewed episodes with actionable findings', () => { const eps = [makeEp(), makeEp()]; const block = computeReviewerFindingsBlock(eps); expect(block).toMatch(/Reviewer findings/); expect(block).toMatch(/нет проверенных/i); }); // batch-reviewer (tools/brain-retro-batch-reviewer.mjs) writes // `outcome_reviewed_source: "direct_api_batch"` at episode level and the // review object lacks `reviewed_at`. Detect via either signal. it('detects batch-reviewed episodes via outcome_reviewed_source (no reviewed_at)', () => { const eps = [ { ...makeEp(), outcome_reviewed_source: 'direct_api_batch', review: { error_root_cause: 'wrong_skill', alternative_better: '#3', node_quality: 'wrong_node' } }, { ...makeEp(), outcome_reviewed_source: 'direct_api_batch', review: { error_root_cause: 'n/a', alternative_better: null, node_quality: 'correct' } }, ]; const block = computeReviewerFindingsBlock(eps); expect(block).toMatch(/Reviewer findings/); expect(block).toMatch(/wrong_skill.*1/); }); it('emits "всё чисто" when reviewed but no wrong_skill / wrong_chain_order', () => { const eps = [ makeEp({ review: { reviewed_at: '2026-05-26T17:00:00Z', error_root_cause: 'n/a', alternative_better: null, node_quality: 'correct' } }), makeEp({ review: { reviewed_at: '2026-05-26T17:10:00Z', error_root_cause: 'n/a', alternative_better: null, node_quality: 'correct' } }), ]; const block = computeReviewerFindingsBlock(eps); expect(block).toMatch(/Reviewer findings/); expect(block).toMatch(/всё чисто|без проблем/i); }); }); // brain-retro #7 C4 follow-up: wiring extra blocks risked introducing // markdownlint MD012 (multiple consecutive blank lines) by chaining // `\n${block}\n` with blocks whose templates end with `\n`. Guard. describe('renderStatus — no double-blank-lines (MD012 lint guard)', () => { it('does not emit "\\n\\n\\n" anywhere when all blocks chained', () => { const inputs = { now: '2026-05-27T04:00:00Z', c1: { status: 'ok', detail: 'OK' }, c2: { status: 'ok', detail: 'OK' }, c3: { status: 'ok', detail: 'OK' }, c5: { status: 'ok', detail: 'OK' }, c6: { status: 'ok', detail: 'OK' }, observer: { episodeCount: 1, observerErrors: 0, piiMatches: 0 }, missed: { totalMissed: 0, byNode: {}, byClassification: {} }, reviewerBlock: '## Reviewer: субагент vs fallback\n\n0 эпизодов проверено.\n', reviewerFindingsBlock: '## Reviewer findings\n\n(нет данных)\n', overrideUsageBlock: '## Использование override-фраз\n\n| ph | n |\n|---|---|\n| x | 0 |\n', selfRetrospectBlock: '## Авто-ретроспектива\n\nLast: never\n', costBlock: '## Cost\n\n$0\n', }; const md = renderStatus(inputs); expect(md).not.toMatch(/\n\n\n/); }); }); describe('renderStatus — 4 new optional blocks integration', () => { const minInputs = { now: '2026-05-25T10:00:00Z', c1: { status: 'ok', detail: 'OK' }, c2: { status: 'ok', detail: 'OK' }, c3: { status: 'ok', detail: 'OK' }, c5: { status: 'ok', detail: 'OK' }, c6: { status: 'ok', detail: 'OK' }, observer: { episodeCount: 5, observerErrors: 0, piiMatches: 0, v1Episodes: 0 }, missed: { totalMissed: 0, byNode: {}, byClassification: {} }, }; it('renders all 4 blocks when provided as strings', () => { const md = renderStatus({ ...minInputs, costBlock: '## Стоимость месяца\ncost content', anomalyBlock: '## Аномалии классификатора\nanomaly content', selfRetrospectBlock: '## Авто-ретроспектива\nretro content', reviewerBlock: '## Reviewer: субагент vs fallback\nreviewer content', }); expect(md).toContain('## Стоимость месяца'); expect(md).toContain('## Аномалии классификатора'); expect(md).toContain('## Авто-ретроспектива'); expect(md).toContain('## Reviewer: субагент vs fallback'); expect(md).toContain('## Алерт-индикаторы'); }); it('omits all 4 blocks when absent (backward compat)', () => { const md = renderStatus(minInputs); expect(md).not.toContain('## Стоимость месяца'); expect(md).not.toContain('## Аномалии классификатора'); expect(md).not.toContain('## Авто-ретроспектива'); expect(md).not.toContain('## Reviewer: субагент vs fallback'); }); }); // ----------------------------------------------------------------------------- // computeSessionLengthBlock — brain-retro #5 candidate B (2026-05-26) // Long sessions correlate with discipline drift; surface a warning when any // session today (UTC) has ≥50 turns. // ----------------------------------------------------------------------------- describe('computeSessionLengthBlock', () => { const day = '2026-05-26'; const ep = (turn, opts = {}) => ({ task_id: opts.id ?? 'sess-1', timestamps: { started_at: `${opts.day ?? day}T01:00:0${turn % 10}Z`, ended_at: `${opts.day ?? day}T01:00:0${turn % 10}Z` }, environment: { session_turn: turn }, path_type: opts.regulated ? 'regulated' : 'improvised', }); it('returns "no data" placeholder when episodes empty', () => { expect(computeSessionLengthBlock([])).toContain('(нет данных)'); }); it('returns OK (✅) when no session reaches threshold', () => { const out = computeSessionLengthBlock([ep(1), ep(2), ep(10)], { now: `${day}T05:00:00Z` }); expect(out).toContain('✅'); expect(out).toContain('Ни одной сессии'); }); it('flags a session that crossed threshold', () => { const eps = Array.from({ length: 55 }, (_, i) => ep(i + 1)); const out = computeSessionLengthBlock(eps, { now: `${day}T05:00:00Z` }); expect(out).toContain('⚠️'); expect(out).toContain('`sess-1'); expect(out).toContain('55'); // max turn }); it('respects custom threshold', () => { const eps = Array.from({ length: 15 }, (_, i) => ep(i + 1)); const flagged = computeSessionLengthBlock(eps, { now: `${day}T05:00:00Z`, threshold: 10 }); const notFlagged = computeSessionLengthBlock(eps, { now: `${day}T05:00:00Z`, threshold: 20 }); expect(flagged).toContain('⚠️'); expect(notFlagged).toContain('✅'); }); it('ignores episodes from other UTC days', () => { const eps = Array.from({ length: 55 }, (_, i) => ep(i + 1, { day: '2026-05-25' })); const out = computeSessionLengthBlock(eps, { now: `${day}T05:00:00Z` }); expect(out).toContain('✅'); // yesterday's session not counted }); it('computes regulated % per long session', () => { const eps = Array.from({ length: 50 }, (_, i) => ep(i + 1, { regulated: i < 10 })); const out = computeSessionLengthBlock(eps, { now: `${day}T05:00:00Z`, threshold: 40 }); expect(out).toContain('⚠️'); expect(out).toContain('20%'); // 10 regulated out of 50 = 20% }); it('handles missing session_turn / task_id gracefully', () => { const eps = [ { task_id: 'x', timestamps: { started_at: `${day}T01:00:00Z` } }, // no session_turn { timestamps: { started_at: `${day}T01:00:00Z` }, environment: { session_turn: 60 } }, // no task_id ep(70, { id: 'real' }), ]; const out = computeSessionLengthBlock(eps, { now: `${day}T05:00:00Z` }); expect(out).toContain('⚠️'); expect(out).toContain('`real'); expect(out).toContain('70'); }); }); import { computeGuardBoardBlock } from './status-md-generator.mjs'; describe('computeGuardBoardBlock (М7 Фаза 6 §7 — доска «кто на посту»)', () => { it('все стражи зарегистрированы → ПОСТ ПОЛНЫЙ + режим судьи', () => { const md = computeGuardBoardBlock({ manifest: { registered: ['enforce-floor.mjs', 'enforce-judge-gate.mjs'], missing: [], ok: true }, judgeMode: 'inert', }); expect(md).toMatch(/ПОСТ ПОЛНЫЙ/); expect(md).toMatch(/inert/); expect(md).toMatch(/enforce-judge-gate\.mjs/); }); it('страж не зарегистрирован → ⚠️ ПОСТ ПУСТОЙ + перечень missing', () => { const md = computeGuardBoardBlock({ manifest: { registered: ['enforce-floor.mjs'], missing: ['enforce-judge-gate.mjs', 'enforce-snapshot.mjs'], ok: false }, judgeMode: 'inert', }); expect(md).toMatch(/ПОСТ ПУСТОЙ/); expect(md).toMatch(/enforce-judge-gate\.mjs/); expect(md).toMatch(/enforce-snapshot\.mjs/); }); it('недавние escape/блоки отражены счётчиками', () => { const md = computeGuardBoardBlock({ manifest: { registered: [], missing: [], ok: true }, judgeMode: 'live-block', recentEscapes: [{}, {}], recentBlocks: [{}], }); expect(md).toMatch(/escape владельца: 2/); expect(md).toMatch(/блоки: 1/); expect(md).toMatch(/live-block/); }); it('тотален на пустом входе (read-only, не бросает)', () => { expect(() => computeGuardBoardBlock({})).not.toThrow(); }); it('рендерит деталь-таблицу при непустых событиях с экранированием', () => { const md = computeGuardBoardBlock({ manifest: { registered: [], missing: [], ok: true }, judgeMode: 'inert', recentEscapes: [{ ts: '2026-06-08T10:00Z', action: 'push', reason: 'a | b\nc' }], recentBlocks: [], }); expect(md).toMatch(/Недавние escape владельца \(детали\)/); expect(md).toContain('a \\| b c'); }); it('пустые массивы → нет деталь-таблицы (byte-identical-путь)', () => { const md = computeGuardBoardBlock({ manifest: { registered: [], missing: [], ok: true }, judgeMode: 'inert', recentEscapes: [], recentBlocks: [], }); expect(md).not.toMatch(/\(детали\)/); }); }); import { readFileSync as readFileSync7 } from 'node:fs'; import { fileURLToPath as ffu7 } from 'node:url'; import { dirname as dn7, join as jn7 } from 'node:path'; describe('доска судьи (М7 Фаза 7): judgeMode ← реальный judgeGateMode(), не хардкод', () => { const src = readFileSync7(jn7(dn7(ffu7(import.meta.url)), 'status-md-generator.mjs'), 'utf8'); it('CLI импортирует judgeGateMode', () => { expect(src.includes("import { judgeGateMode }")).toBe(true); }); it('CLI передаёт judgeMode: judgeGateMode() (реальный режим), не хардкод inert', () => { expect(src.includes('judgeMode: judgeGateMode()')).toBe(true); expect(src.includes("judgeMode: 'inert'")).toBe(false); }); }); // ── Блок B Класс 1 (R-09): очередь обучения роутера ── import { computeLearningQueueBlock } from './status-md-generator.mjs'; describe('computeLearningQueueBlock (R-09 очередь обучения)', () => { it('пустая очередь → «очередь пуста»', () => { const md = computeLearningQueueBlock({ queue: [] }); expect(md).toContain('## Очередь обучения роутера'); expect(md).toContain('Очередь пуста'); }); it('нет аргумента (undefined) → не падает, очередь пуста', () => { const md = computeLearningQueueBlock({}); expect(md).toContain('Очередь пуста'); }); it('есть pending → показывает «ждут одобрения: N» и список', () => { const queue = [ { id: 'c1', kind: 'example', summary: 'кейс A', why_proposed: 'p', status: 'pending' }, { id: 'c2', kind: 'example', summary: 'кейс B', why_proposed: 'p', status: 'approved' }, ]; const md = computeLearningQueueBlock({ queue }); expect(md).toContain('ждут одобрения: 1'); expect(md).toContain('c1'); expect(md).not.toContain('кейс B'); }); it('экранирует pipe в summary (anti-injection)', () => { const queue = [{ id: 'x', kind: 'k', summary: 'a | b', why_proposed: '', status: 'pending' }]; const md = computeLearningQueueBlock({ queue }); expect(md).toContain('a \\| b'); }); }); describe('renderStatus — learningQueueBlock (R-09)', () => { const base = { now: '2026-06-09T10:00:00Z', c1: { status: 'ok', detail: 'OK' }, c2: { status: 'ok', detail: 'OK' }, c3: { status: 'ok', detail: 'OK' }, c5: { status: 'ok', detail: 'OK' }, observer: { episodeCount: 0, observerErrors: 0, piiMatches: 0 }, }; it('вставляет блок строкой при наличии', () => { const md = renderStatus({ ...base, learningQueueBlock: '## Очередь обучения роутера\n\nОчередь пуста.' }); expect(md).toContain('## Очередь обучения роутера'); }); it('опускает при отсутствии (backward compat)', () => { const md = renderStatus(base); expect(md).not.toContain('## Очередь обучения роутера'); }); }); // ── Блок B Класс 2 (R-24): покрытие дверей ── import { computeDoorCoverageBlock } from './status-md-generator.mjs'; describe('computeDoorCoverageBlock (R-24 покрытие дверей)', () => { it('matcher "*" → все двери покрыты (✅)', () => { const settings = { hooks: { PreToolUse: [{ matcher: '*', hooks: [{ command: 'node tools/enforce-supreme-gate.mjs' }] }] } }; const md = computeDoorCoverageBlock({ settings }); expect(md).toContain('## Покрытие дверей'); expect(md).toContain('✅'); expect(md).toContain('двери покрыты'); }); it('хук не зарегистрирован → ⚠️ забытые двери (все мутирующие)', () => { const md = computeDoorCoverageBlock({ settings: {} }); expect(md).toContain('забытые двери'); expect(md).toContain('Write'); expect(md).toContain('Bash'); }); it('частичный matcher → флагует непокрытые', () => { const settings = { hooks: { PreToolUse: [{ matcher: 'Edit|Write', hooks: [{ command: 'node tools/enforce-supreme-gate.mjs' }] }] } }; const md = computeDoorCoverageBlock({ settings }); expect(md).toContain('забытые двери'); expect(md).toContain('Bash'); }); it('не падает при пустом settings undefined', () => { const md = computeDoorCoverageBlock({}); expect(md).toContain('## Покрытие дверей'); }); }); describe('renderStatus — doorCoverageBlock (R-24)', () => { const base2 = { now: '2026-06-09T10:00:00Z', c1: { status: 'ok', detail: 'OK' }, c2: { status: 'ok', detail: 'OK' }, c3: { status: 'ok', detail: 'OK' }, c5: { status: 'ok', detail: 'OK' }, observer: { episodeCount: 0, observerErrors: 0, piiMatches: 0 }, }; it('вставляет блок при наличии', () => { const md = renderStatus({ ...base2, doorCoverageBlock: '## Покрытие дверей\n\nвсе двери покрыты.' }); expect(md).toContain('## Покрытие дверей'); }); it('опускает при отсутствии', () => { expect(renderStatus(base2)).not.toContain('## Покрытие дверей'); }); }); // ── Блок B Класс 3 (R-30): целостность журналов через verifyChain live ── import { computeJournalIntegrityBlock } from './status-md-generator.mjs'; describe('computeJournalIntegrityBlock (R-30 целостность журналов)', () => { it('ключ не provisioned → проверка недоступна (не «битые»)', () => { const md = computeJournalIntegrityBlock({ keyAvailable: false }); expect(md).toContain('## Целостность журналов действий'); expect(md).toContain('Ключ подписанта не provisioned'); expect(md).not.toContain('🔴'); }); it('журналов нет → так и пишем', () => { const md = computeJournalIntegrityBlock({ keyAvailable: true, results: [] }); expect(md).toContain('Журналов сессий не найдено'); }); it('все цепи целы → ✅', () => { const md = computeJournalIntegrityBlock({ keyAvailable: true, results: [ { sessionId: 'a', ok: true, brokenAt: null }, { sessionId: 'b', ok: true, brokenAt: null }, ] }); expect(md).toContain('✅'); expect(md).toContain('2 сессий'); }); it('битая цепь → 🔴 с seq', () => { const md = computeJournalIntegrityBlock({ keyAvailable: true, results: [ { sessionId: 'a', ok: true, brokenAt: null }, { sessionId: 'bad', ok: false, brokenAt: 7 }, ] }); expect(md).toContain('🔴'); expect(md).toContain('bad'); expect(md).toContain('7'); }); }); describe('renderStatus — journalIntegrityBlock (R-30)', () => { const base3 = { now: '2026-06-09T10:00:00Z', c1: { status: 'ok', detail: 'OK' }, c2: { status: 'ok', detail: 'OK' }, c3: { status: 'ok', detail: 'OK' }, c5: { status: 'ok', detail: 'OK' }, observer: { episodeCount: 0, observerErrors: 0, piiMatches: 0 }, }; it('вставляет при наличии', () => { const md = renderStatus({ ...base3, journalIntegrityBlock: '## Целостность журналов действий\n\n✅ ок.' }); expect(md).toContain('## Целостность журналов действий'); }); it('опускает при отсутствии', () => { expect(renderStatus(base3)).not.toContain('## Целостность журналов действий'); }); });