2b5e265c3e
# Conflicts: # tools/enforce-gate3-loop.mjs
173 lines
8.7 KiB
JavaScript
173 lines
8.7 KiB
JavaScript
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);
|
|
});
|
|
});
|