397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
4.4 KiB
JavaScript
70 lines
4.4 KiB
JavaScript
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([]);
|
|
});
|
|
});
|