Files
brain/tools/coverage-machine.test.mjs
T

148 lines
7.8 KiB
JavaScript
Raw Normal View History

import { describe, it, expect } from 'vitest';
import {
normToken, buildDependencyGraph, topoOrder, findHoles, decompositionGroups,
coverageRegistry, requestsChecklist, readinessChecklist, effectiveNeeds,
} from './coverage-machine.mjs';
const c = (skill, needs, produces, constraints = []) => ({ skill, needs, produces, constraints });
const CHAIN = [c('X', ['spec'], ['code']), c('Y', [], ['spec'])];
describe('normToken', () => {
it('lower + trim', () => { expect(normToken(' Spec ')).toBe('spec'); });
});
// fix: tools/coverage-machine.mjs (F-1, аудит M1-M4) — покрытие по словам, не по подстроке
describe('F-1: покрытие по границам слов, не по подстроке', () => {
it('одиночная буква в produces не покрывает многословную просьбу', () => {
const cl = requestsChecklist(['audit-rls-policy'], [{ skill: 'x', needs: [], produces: ['a'] }]);
expect(cl[0].ok).toBe(false);
});
it('coverageRegistry не гасит сироту по подстрочному совпадению', () => {
const { orphans } = coverageRegistry([{ skill: 's', needs: [], produces: ['a'] }], { requests: ['audit-deals'] });
expect(orphans.length).toBe(1);
});
it('точное слово и подмножество слов по-прежнему покрывают (coverage-machine.mjs)', () => {
expect(requestsChecklist(['csv'], [{ skill: 'e', needs: [], produces: ['csv-export-deals'] }])[0].ok).toBe(true);
expect(requestsChecklist(['csv-export'], [{ skill: 'e', needs: [], produces: ['csv-export'] }])[0].ok).toBe(true);
});
});
describe('buildDependencyGraph (A: needs↔produces)', () => {
it('ребро producer→consumer via need', () => {
const g = buildDependencyGraph(CHAIN);
expect(g.edges).toContainEqual({ from: 'Y', to: 'X', via: 'spec' });
});
});
describe('topoOrder (A: порядок = топосортировка, цикл = флаг)', () => {
it('порядок: Y перед X', () => {
const r = topoOrder(CHAIN);
expect(r.cycle).toBe(null);
expect(r.order.indexOf('Y')).toBeLessThan(r.order.indexOf('X'));
});
it('цикл помечается', () => {
const cyc = [c('A', ['b'], ['a']), c('B', ['a'], ['b'])];
const r = topoOrder(cyc);
expect(r.order).toBe(null);
expect(r.cycle.sort()).toEqual(['A', 'B']);
});
});
describe('findHoles (A: нужда без producer; D: ограничения тоже)', () => {
it('нужда, которую никто не производит → дыра', () => {
const h = findHoles([c('X', ['spec'], ['code'])]);
expect(h).toContainEqual({ need: 'spec', neededBy: 'X', kind: 'need' });
});
it('initialInputs закрывают нужду (не дыра)', () => {
expect(findHoles([c('X', ['spec'], ['code'])], { initialInputs: ['spec'] })).toEqual([]);
});
it('ограничение без покрытия → дыра kind=constraint (D)', () => {
const h = findHoles([c('X', [], ['code'], ['must be RLS-safe'])]);
expect(h).toContainEqual({ need: 'must be RLS-safe', neededBy: 'X', kind: 'constraint' });
});
it('produced нужда не дыра', () => {
expect(findHoles(CHAIN)).toEqual([]);
});
});
describe('decompositionGroups (A: связные группы)', () => {
it('связанные скилы — одна группа, несвязанный — отдельная', () => {
const groups = decompositionGroups([...CHAIN, c('Z', [], ['unrelated'])]);
const sizes = groups.map((g) => g.length).sort();
expect(sizes).toEqual([1, 2]);
});
});
describe('coverageRegistry (B: нужды↔решения, дыры + сироты)', () => {
it('дыра попадает в holes', () => {
const r = coverageRegistry([c('X', ['spec'], ['code'])]);
expect(r.holes.map((h) => h.need)).toContain('spec');
});
it('сирота-скоупкрип: produces никому не нужен и не покрывает просьбу', () => {
const r = coverageRegistry([c('X', ['spec'], ['code']), c('Y', [], ['spec']), c('Z', [], ['extra'])], { requests: ['code'] });
expect(r.orphans.map((o) => o.skill)).toContain('Z');
expect(r.orphans.map((o) => o.skill)).not.toContain('Y');
});
});
describe('requestsChecklist (C: просьбы цели → план)', () => {
it('просьба, которую кто-то производит → ok', () => {
const list = requestsChecklist(['code'], [c('X', [], ['code'])]);
expect(list).toEqual([{ request: 'code', coveredBy: 'X', ok: true }]);
});
it('непокрытая просьба → ok=false, coveredBy=null', () => {
const list = requestsChecklist(['report'], [c('X', [], ['code'])]);
expect(list[0]).toMatchObject({ request: 'report', coveredBy: null, ok: false });
});
it('покрытие по подстроке (мягкий край)', () => {
const list = requestsChecklist(['csv'], [c('X', [], ['export to csv file'])]);
expect(list[0].ok).toBe(true);
});
});
describe('readinessChecklist (хребет — галочки + указатели §)', () => {
it('полный план → ready=true, все галочки', () => {
const r = readinessChecklist({ contracts: [c('Y', [], ['spec']), c('X', ['spec'], ['code'])], requests: ['code'] });
expect(r.ready).toBe(true);
expect(r.items.every((i) => i.ok)).toBe(true);
expect(r.items.every((i) => typeof i.pointer === 'string')).toBe(true);
});
it('дыра → ready=false + пункт про дыры провален', () => {
const r = readinessChecklist({ contracts: [c('X', ['spec'], ['code'])], requests: ['code'] });
expect(r.ready).toBe(false);
expect(r.items.find((i) => /дыр|hole/i.test(i.label)).ok).toBe(false);
});
it('цикл → пункт про циклы провален', () => {
const r = readinessChecklist({ contracts: [c('A', ['b'], ['a']), c('B', ['a'], ['b'])], requests: [] });
expect(r.items.find((i) => /цикл|cycle/i.test(i.label)).ok).toBe(false);
});
});
describe('effectiveNeeds + findHoles c inherent (#1 — присущее по природе = названная нужда)', () => {
const withInh = { skill: 'fd', needs: ['design'], produces: [], constraints: ['a11y'], inherent: [{ need: 'мобильный', rationale: 'адаптив по природе' }] };
it('effectiveNeeds включает inherent (kind=inherent) наравне с needs/constraints', () => {
const en = effectiveNeeds(withInh);
expect(en).toContainEqual({ token: 'design', kind: 'need' });
expect(en).toContainEqual({ token: 'a11y', kind: 'constraint' });
expect(en).toContainEqual({ token: 'мобильный', kind: 'inherent' });
});
it('inherent-нужда без producer → дыра kind=inherent', () => {
const h = findHoles([withInh]);
expect(h).toContainEqual({ need: 'мобильный', neededBy: 'fd', kind: 'inherent' });
});
it('контракт без inherent → поведение не изменилось', () => {
expect(effectiveNeeds({ skill: 'x', needs: ['n'], constraints: [] })).toEqual([{ token: 'n', kind: 'need' }]);
});
});
describe('пустой/пробельный запрос НЕ покрывается (F3 — empty-token footgun)', () => {
it('пробельная просьба → ok=false, coveredBy=null (а не ложное «покрыто»)', () => {
const list = requestsChecklist([' '], [c('a', [], ['x'])]);
expect(list[0]).toMatchObject({ coveredBy: null, ok: false });
});
it('пустая просьба не делает любой навык покрывающим → сирота считается', () => {
const r = coverageRegistry([c('Z', [], ['extra'])], { requests: [' '] });
expect(r.orphans.map((o) => o.skill)).toContain('Z');
});
});