import { describe, it, expect } from 'vitest'; import { signLoopMarker, verifyLoopMarker, computeFingerprint, decideStopTeeth, resolveOwnerArbitration, buildGate3ProductFromMarker, } from './enforce-gate3-loop.mjs'; const KEY = 'test-key-123'; describe('signLoopMarker/verifyLoopMarker', () => { it('roundtrip', () => { const m = signLoopMarker({ taskId: 't1', planId: 'p1', artifactId: 'a1', steps: [], at: 1 }, KEY); expect(typeof m.sig).toBe('string'); expect(verifyLoopMarker(m, KEY)).toBe(true); }); it('подмена поля ломает подпись', () => { const m = signLoopMarker({ taskId: 't1', planId: 'p1', artifactId: 'a1', steps: [], at: 1 }, KEY); expect(verifyLoopMarker({ ...m, planId: 'p2' }, KEY)).toBe(false); }); it('нет sig → false', () => { expect(verifyLoopMarker({ taskId: 't1' }, KEY)).toBe(false); }); }); describe('computeFingerprint', () => { it('детерминирован, не зависит от порядка greens', () => { expect(computeFingerprint({ planId: 'p', greenIds: ['c2', 'c1'], negotiationText: 'x' })) .toBe(computeFingerprint({ planId: 'p', greenIds: ['c1', 'c2'], negotiationText: 'x' })); }); it('меняется на новый green', () => { expect(computeFingerprint({ planId: 'p', greenIds: ['c1'], negotiationText: 'x' })) .not.toBe(computeFingerprint({ planId: 'p', greenIds: ['c1', 'c2'], negotiationText: 'x' })); }); it('меняется на новый довод', () => { expect(computeFingerprint({ planId: 'p', greenIds: ['c1'], negotiationText: 'x' })) .not.toBe(computeFingerprint({ planId: 'p', greenIds: ['c1'], negotiationText: 'y' })); }); }); describe('decideStopTeeth', () => { const go = { wired: true, decision: 'GO' }; const nogo = { wired: true, decision: 'NO-GO' }; const degraded = { wired: false, decision: 'GO' }; it('GO → allow + clear', () => { const r = decideStopTeeth({ verdict: go, noGoCount: 0, ownerArbitration: null }); expect(r.block).toBe(false); expect(r.clear).toBe(true); }); it('accept → allow + clear', () => { const r = decideStopTeeth({ verdict: nogo, noGoCount: 0, ownerArbitration: 'accept' }); expect(r.block).toBe(false); expect(r.clear).toBe(true); }); it('continue → allow, держим', () => { const r = decideStopTeeth({ verdict: nogo, noGoCount: 0, ownerArbitration: 'continue' }); expect(r.block).toBe(false); expect(r.clear).toBe(false); }); it('NO-GO круг<3 → block negotiate', () => { const r = decideStopTeeth({ verdict: nogo, noGoCount: 1, ownerArbitration: null }); expect(r.block).toBe(true); expect(r.state).toBe('negotiate'); }); it('NO-GO круг>=3 → block arbitrate+card', () => { const r = decideStopTeeth({ verdict: nogo, noGoCount: 3, ownerArbitration: null }); expect(r.block).toBe(true); expect(r.state).toBe('arbitrate'); expect(r.card).toBe(true); }); it('degraded → block, НЕ closed/arbitrate', () => { const r = decideStopTeeth({ verdict: degraded, noGoCount: 5, ownerArbitration: null }); expect(r.block).toBe(true); expect(r.clear).toBe(false); expect(r.state).toBe('negotiate'); }); }); describe('resolveOwnerArbitration', () => { const fp = 'abc'; const g = (action, ts = 1000) => ({ action, ts }); it('accept грант → accept', () => { expect(resolveOwnerArbitration({ fingerprint: fp, grants: [g(`gate3-arb:accept:${fp}`)], consumed: [], now: 1000 })).toBe('accept'); }); it('continue грант → continue', () => { expect(resolveOwnerArbitration({ fingerprint: fp, grants: [g(`gate3-arb:continue:${fp}`)], consumed: [], now: 1000 })).toBe('continue'); }); it('нет гранта → null', () => { expect(resolveOwnerArbitration({ fingerprint: fp, grants: [], consumed: [], now: 1000 })).toBe(null); }); it('грант на другой отпечаток → null (анти-реплей)', () => { expect(resolveOwnerArbitration({ fingerprint: fp, grants: [g('gate3-arb:accept:OTHER')], consumed: [], now: 1000 })).toBe(null); }); }); describe('buildGate3ProductFromMarker', () => { it('цель из секций + шаги + greens', () => { const marker = { steps: [{ op: 'Write', object: 'a.mjs', criterion_id: 'c1' }], planId: 'p' }; const frozenArtifact = { sections: { s1: 'построить X', s2: 'критерий Y' } }; const out = buildGate3ProductFromMarker({ marker, frozenArtifact, greens: [{ criterion_id: 'c1', green: true }] }); expect(out.goal).toContain('построить X'); expect(out.product).toContain('a.mjs'); expect(out.product).toContain('green'); }); }); import { gate3SurfaceRecord } from './enforce-gate3-loop.mjs'; describe('gate3SurfaceRecord (видимость gate3)', () => { it('GO verdict → stage judge:gate3, status GO', () => { expect(gate3SurfaceRecord({ verdict: { wired: true, decision: 'GO' }, hash: 'h' })) .toEqual({ stage: 'judge:gate3', hash: 'h', status: 'GO', reason: '' }); }); it('NO-GO → status NO-GO, reason дословно', () => { const r = gate3SurfaceRecord({ verdict: { wired: true, decision: 'NO-GO', reason: 'не достигнуто' }, hash: 'h' }); expect(r.status).toBe('NO-GO'); expect(r.reason).toBe('не достигнуто'); }); it('degraded (wired:false) → degraded', () => { expect(gate3SurfaceRecord({ verdict: { wired: false }, hash: 'h' }).status).toBe('degraded'); }); it('нет verdict → skip, hash null', () => { expect(gate3SurfaceRecord({}).status).toBe('skip'); expect(gate3SurfaceRecord({}).hash).toBe(null); }); }); import { gate3CardSurfaceRecord } from './enforce-gate3-loop.mjs'; describe('gate3CardSurfaceRecord (видимость судьи карточки, стадия judge:gate3card)', () => { it('GO verdict → stage judge:gate3card, status GO', () => { expect(gate3CardSurfaceRecord({ verdict: { wired: true, decision: 'GO' }, hash: 'h' })) .toEqual({ stage: 'judge:gate3card', hash: 'h', status: 'GO', reason: '' }); }); it('NO-GO → status NO-GO, reason дословно', () => { const r = gate3CardSurfaceRecord({ verdict: { wired: true, decision: 'NO-GO', reason: 'приукрашивание' }, hash: 'h' }); expect(r.status).toBe('NO-GO'); expect(r.reason).toBe('приукрашивание'); }); it('degraded (wired:false) → degraded', () => { expect(gate3CardSurfaceRecord({ verdict: { wired: false }, hash: 'h' }).status).toBe('degraded'); }); it('нет verdict → skip, hash null', () => { expect(gate3CardSurfaceRecord({}).status).toBe('skip'); expect(gate3CardSurfaceRecord({}).hash).toBe(null); }); }); describe('loop marker delivery', () => { const KEY = 'k-loop-deliv'; it('delivery в подписанной метке верифицируется и ломается при подмене', () => { const m = signLoopMarker({ taskId: 't', planId: 'p', artifactId: 'a', steps: [], delivery: 'user-result', at: 1 }, KEY); expect(m.delivery).toBe('user-result'); expect(verifyLoopMarker(m, KEY)).toBe(true); expect(verifyLoopMarker({ ...m, delivery: 'internal' }, KEY)).toBe(false); }); }); import { produceGate3Verdict } from './enforce-gate3-loop.mjs'; describe('produceGate3Verdict (видимость срыва gate3 — фикс silent-swallow)', () => { it('нет ключа судьи → degraded (wired:false, unavailable), без cause', async () => { const r = await produceGate3Verdict({ judgeKey: null, callJudge: async () => ({ wired: true, decision: 'GO' }), buildProduct: () => ({}) }); expect(r.wired).toBe(false); expect(r.unavailable).toBe(true); }); it('callJudge бросил → ВИДИМЫЙ degraded с непустым cause (не молчит)', async () => { const r = await produceGate3Verdict({ judgeKey: 'K', callJudge: async () => { throw new Error('boom'); }, buildProduct: () => ({}) }); expect(r.wired).toBe(false); expect(r.unavailable).toBe(true); expect(typeof r.cause).toBe('string'); expect(r.cause.length).toBeGreaterThan(0); }); it('buildProduct бросил → degraded (срыв построения продукта тоже виден)', async () => { const r = await produceGate3Verdict({ judgeKey: 'K', callJudge: async () => ({ wired: true, decision: 'GO' }), buildProduct: () => { throw new Error('bad marker'); } }); expect(r.wired).toBe(false); expect(r.unavailable).toBe(true); }); it('заход вернул вердикт → проброс без искажения', async () => { const v = { wired: true, decision: 'NO-GO', reason: 'не достигнуто' }; const r = await produceGate3Verdict({ judgeKey: 'K', callJudge: async () => v, buildProduct: () => ({ goal: 'g' }) }); expect(r).toEqual(v); }); }); import { buildOwnerCardFromMarker, produceCardVerdict, buildCardJudgeArgs, renderOwnerCardMessage } from './enforce-gate3-loop.mjs'; describe('decideStopTeeth — delivery + cardVerdict (Фаза 2d приёмка)', () => { const go = { wired: true, decision: 'GO' }; const cardGo = { wired: true, decision: 'GO' }; const cardNoGo = { wired: true, decision: 'NO-GO', reason: 'приукрашено' }; const cardDegraded = { wired: false, decision: 'GO' }; it('internal + код-GO → allow + clear (closed)', () => { const r = decideStopTeeth({ verdict: go, delivery: 'internal' }); expect(r.block).toBe(false); expect(r.clear).toBe(true); }); it('user-result + код-GO + карточка NO-GO → block await-card (владельца не зовём)', () => { const r = decideStopTeeth({ verdict: go, delivery: 'user-result', cardVerdict: cardNoGo }); expect(r.block).toBe(true); expect(r.state).toBe('await-card'); }); it('user-result + код-GO + карточка GO → block await-owner, unverified false', () => { const r = decideStopTeeth({ verdict: go, delivery: 'user-result', cardVerdict: cardGo }); expect(r.block).toBe(true); expect(r.state).toBe('await-owner'); expect(r.unverified).toBe(false); }); it('user-result + код-GO + карточка degraded → block await-owner unverified:true', () => { const r = decideStopTeeth({ verdict: go, delivery: 'user-result', cardVerdict: cardDegraded }); expect(r.block).toBe(true); expect(r.state).toBe('await-owner'); expect(r.unverified).toBe(true); }); it('user-result + код-GO + карточка ещё не сверена → await-card', () => { const r = decideStopTeeth({ verdict: go, delivery: 'user-result', cardVerdict: null }); expect(r.block).toBe(true); expect(r.state).toBe('await-card'); }); it('user-result + подписанный accept → allow + clear (closed)', () => { const r = decideStopTeeth({ verdict: go, delivery: 'user-result', cardVerdict: cardGo, ownerArbitration: 'accept' }); expect(r.block).toBe(false); expect(r.clear).toBe(true); }); }); describe('buildOwnerCardFromMarker (Фаза 2d сборка карточки)', () => { it('цель из секций + verifySteps из GREEN + честные заглушки + machinery + warning', () => { const marker = { steps: [], planId: 'p', delivery: 'user-result' }; const frozenArtifact = { sections: { s1: 'дать владельцу приёмку' } }; const card = buildOwnerCardFromMarker({ marker, frozenArtifact, greens: [{ criterion_id: 'c1', green: true }, { criterion_id: 'c2', green: false }] }); expect(card.goal).toContain('дать владельцу приёмку'); expect(card.verifySteps.join(' ')).toContain('c1'); expect(card.verifySteps.join(' ')).not.toContain('c2'); expect(card.kind).toBe('machinery'); expect(card.honestyChecked).toBe(false); expect(card.warning).toBeTruthy(); }); it('пустые входы → честные заглушки, не выдумки', () => { const card = buildOwnerCardFromMarker({ marker: { steps: [] }, frozenArtifact: null, greens: [] }); expect(card.goal).toBe('(цель не указана)'); expect(card.change).toEqual(['(что изменилось — не указано)']); }); }); describe('produceCardVerdict (видимость срыва судьи карточки)', () => { it('нет ключа → degraded', async () => { const r = await produceCardVerdict({ judgeKey: null, callCardJudge: async () => ({ wired: true, decision: 'GO' }), buildArgs: () => ({}) }); expect(r.wired).toBe(false); expect(r.unavailable).toBe(true); }); it('заход бросил → видимый degraded с cause', async () => { const r = await produceCardVerdict({ judgeKey: 'K', callCardJudge: async () => { throw new Error('boom'); }, buildArgs: () => ({}) }); expect(r.wired).toBe(false); expect(typeof r.cause).toBe('string'); expect(r.cause.length).toBeGreaterThan(0); }); it('вердикт → проброс', async () => { const v = { wired: true, decision: 'NO-GO', reason: 'overstatement' }; const r = await produceCardVerdict({ judgeKey: 'K', callCardJudge: async () => v, buildArgs: () => ({}) }); expect(r).toEqual(v); }); }); describe('buildCardJudgeArgs', () => { it('факты продукта + сериализованная карточка + цель', () => { const args = buildCardJudgeArgs({ card: { goal: 'G', change: ['c'], verifySteps: ['v'], boundary: 'b' }, gate3Product: { product: 'ФАКТЫ', goal: 'G' } }); expect(args.product).toContain('ФАКТЫ'); expect(args.product).toContain('как проверить'); expect(args.product).toContain('v'); expect(args.goal).toBe('G'); }); }); describe('renderOwnerCardMessage', () => { it('содержит accept/continue по отпечатку + поля карточки', () => { const msg = renderOwnerCardMessage({ card: { goal: 'G', change: ['ch'], verifySteps: ['st'], boundary: 'bd', honestyChecked: true }, fingerprint: 'FP' }); expect(msg).toContain('gate3-arb:accept:FP'); expect(msg).toContain('gate3-arb:continue:FP'); expect(msg).toContain('st'); }); it('unverified → видимое предупреждение', () => { const msg = renderOwnerCardMessage({ card: { goal: 'G', warning: 'не проверено' }, fingerprint: 'FP', unverified: true }); expect(msg).toContain('⚠'); }); });