Files
brain/tools/enforce-gate3-loop.test.mjs
2026-06-18 05:28:49 +03:00

264 lines
14 KiB
JavaScript
Raw Permalink 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 {
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('⚠');
});
});