Files
brain/tools/status-md-generator.test.mjs
T

715 lines
34 KiB
JavaScript
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, 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('## Целостность журналов действий');
});
});