Files
portal/tools/skill-contract-registry.test.mjs
T

70 lines
4.4 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 { 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([]);
});
});