397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
7.8 KiB
JavaScript
148 lines
7.8 KiB
JavaScript
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');
|
||
});
|
||
});
|