// tools/skeleton-radar.test.mjs import { describe, it, expect } from 'vitest'; import { buildSkeletonRadar, REPORT_STATUSES } from './skeleton-radar.mjs'; describe('buildSkeletonRadar (§5.7 скелет-радар)', () => { const headers = ['## Auth', '## Billing', '## Webhooks']; it('каждый заголовок → пункт; непрорепорченный → not-reported', () => { const r = buildSkeletonRadar({ headers, reports: { '## Auth': 'touched' } }); expect(r.items).toHaveLength(3); expect(r.items.find((i) => i.header === '## Auth').status).toBe('touched'); expect(r.items.find((i) => i.header === '## Billing').status).toBe('not-reported'); expect(r.unreported.map((i) => i.header).sort()).toEqual(['## Billing', '## Webhooks']); }); it('REPORT_STATUSES заморожен и содержит 4 статуса', () => { expect(Object.isFrozen(REPORT_STATUSES)).toBe(true); expect(REPORT_STATUSES).toEqual(['touched', 'not-relevant', 'not-read', 'not-reported']); }); it('невалидный статус в reports → not-reported (fail-safe)', () => { const r = buildSkeletonRadar({ headers: ['## A'], reports: { '## A': 'выдумка' } }); expect(r.items[0].status).toBe('not-reported'); }); it('битые входы → пусто, не крашит', () => { expect(buildSkeletonRadar({ headers: null }).items).toEqual([]); expect(buildSkeletonRadar({}).unreported).toEqual([]); }); // F-F3: пустой скелет виден — summary.headers=0 (зеркало F-F1 graph-radar) it('F-F3: summary {headers, unreported}; пустой скелет = headers:0', () => { const r = buildSkeletonRadar({ headers, reports: { '## Auth': 'touched' } }); expect(r.summary).toEqual({ headers: 3, unreported: 2 }); const empty = buildSkeletonRadar({ headers: [] }); expect(empty.summary).toEqual({ headers: 0, unreported: 0 }); }); // F-F4: items заморожены — машинная мутация статуса бросает TypeError (L2-пол) it('F-F4: элементы items заморожены, мутация status бросает TypeError', () => { const r = buildSkeletonRadar({ headers: ['## A'], reports: {} }); expect(r.items.every((i) => Object.isFrozen(i))).toBe(true); expect(() => { r.items[0].status = 'touched'; }).toThrow(TypeError); }); // F-F5 (характеризация, без RED — SE9): дубликаты заголовков сохраняются оба, // делят один ключ отчёта (направление fail-safe — пункты не теряются) it('F-F5: дубликаты заголовков → оба пункта, один статус на ключ', () => { const r = buildSkeletonRadar({ headers: ['## A', '## A'], reports: { '## A': 'touched' } }); expect(r.items).toHaveLength(2); expect(r.items.every((i) => i.status === 'touched')).toBe(true); }); });