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

148 lines
7.8 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 {
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');
});
});