import { describe, it, expect } from 'vitest'; import { buildRegistry, loadRegistry, dispatchContract } from './skill-contract-registry.mjs'; const own = (skill) => ({ skill, kind: 'own', needs: [], produces: [], constraints: [], 'preview-form': 'none', defaults: [], 'key-decisions': [], 'acceptance-criteria': [] }); describe('dispatchContract (G3 — детерминированная диспетчеризация: точно|мягко, молча не доверять)', () => { it('есть+свежий контракт → mode exact + сам контракт', () => { const reg = buildRegistry([{ contract: own('a') }]); const d = dispatchContract(reg, 'a'); expect(d.mode).toBe('exact'); expect(d.contract.skill).toBe('a'); }); it('нет адаптера для скила → mode soft-reasoning (откат на мягкое рассуждение)', () => { const reg = buildRegistry([{ contract: own('a') }]); const d = dispatchContract(reg, 'нет-такого'); expect(d.mode).toBe('soft-reasoning'); expect(typeof d.reason).toBe('string'); }); it('дрейф external → mode soft-reasoning + причина (G4-флаг подхвачен)', () => { const ext = { skill: 'e', kind: 'external', needs: [], produces: [], constraints: [], 'preview-form': 'none', defaults: [], 'key-decisions': [], 'acceptance-criteria': [], source: { version: '1', hash: 'f'.repeat(64) } }; const reg = buildRegistry([{ contract: ext, currentContent: 'changed body' }]); const d = dispatchContract(reg, 'e'); expect(d.mode).toBe('soft-reasoning'); expect(d.reason).toMatch(/дрейф/); }); }); describe('buildRegistry (чистый)', () => { it('валидные контракты собираются', () => { const r = buildRegistry([{ contract: own('a') }, { contract: own('b') }]); expect(r.contracts).toHaveLength(2); expect(r.errors).toEqual([]); expect(r.driftFlags).toEqual([]); }); it('невалидный контракт → в errors, не в contracts', () => { const r = buildRegistry([{ contract: { skill: '', kind: 'own' } }]); expect(r.contracts).toHaveLength(0); expect(r.errors).toHaveLength(1); }); it('дубль skill → ошибка, первый остаётся', () => { const r = buildRegistry([{ contract: own('a') }, { contract: own('a') }]); expect(r.contracts).toHaveLength(1); expect(r.errors[0].errors[0]).toMatch(/duplicate/); }); it('external дрейф → в driftFlags, контракт всё равно загружен (с пометкой)', () => { const ext = { skill: 'e', kind: 'external', needs: [], produces: [], constraints: [], 'preview-form': 'none', defaults: [], 'key-decisions': [], 'acceptance-criteria': [], source: { version: '1', hash: 'f'.repeat(64) } }; const r = buildRegistry([{ contract: ext, currentContent: 'changed body' }]); expect(r.contracts).toHaveLength(1); expect(r.driftFlags).toHaveLength(1); expect(r.driftFlags[0].skill).toBe('e'); }); }); function memFs(map) { return { readdirSync: () => Object.keys(map).map((p) => p.split('/').pop()), readFileSync: (p) => { const k = String(p); if (!(k in map)) { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; } return map[k]; }, }; } describe('loadRegistry (диск, fs инъектируется)', () => { it('читает *.contract.json, валидирует, external тянет source.path для дрейфа', () => { const ownC = JSON.stringify({ skill: 'wp', kind: 'own', needs: [], produces: [], constraints: [], 'preview-form': 'none', defaults: [], 'key-decisions': [], 'acceptance-criteria': [] }); const extC = JSON.stringify({ skill: 'pd', kind: 'external', needs: [], produces: [], constraints: [], 'preview-form': 'none', defaults: [], 'key-decisions': [], 'acceptance-criteria': [], source: { version: '1', hash: '0'.repeat(64), path: '/skills/pd/SKILL.md' } }); const map = { '/c/wp.contract.json': ownC, '/c/pd.contract.json': extC, '/skills/pd/SKILL.md': 'current body' }; const r = loadRegistry({ dir: '/c', fsImpl: memFs(map) }); expect(r.contracts).toHaveLength(2); expect(r.driftFlags).toHaveLength(1); // hash 0..0 ≠ хеш 'current body' }); it('игнорирует не-.contract.json', () => { const map = { '/c/readme.md': 'x' }; const r = loadRegistry({ dir: '/c', fsImpl: memFs(map) }); expect(r.contracts).toEqual([]); expect(r.errors).toEqual([]); }); });