15e217fcb4
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
163 lines
8.7 KiB
JavaScript
163 lines
8.7 KiB
JavaScript
// Тесты sub-plan E Task 5 — терминация внешней петли (§4 SE-R7-6 спеки R6.3).
|
|
import { describe, it, expect } from 'vitest';
|
|
import { loopTerminationDecision } from './loop-termination.mjs';
|
|
|
|
describe('loopTerminationDecision (SE-R7-6)', () => {
|
|
it('владелец объявил цель достигнутой → terminate', () => {
|
|
expect(loopTerminationDecision({ ownerDeclaredDone: true }).terminate).toBe(true);
|
|
});
|
|
it('судья gate-3 GO → terminate', () => {
|
|
expect(loopTerminationDecision({ judgeGate3Go: true }).terminate).toBe(true);
|
|
});
|
|
it('ни того ни другого → продолжать (контроллер сам не закрывает)', () => {
|
|
const r = loopTerminationDecision({});
|
|
expect(r.terminate).toBe(false);
|
|
expect(r.reason).toMatch(/контроллер сам не закрывает/);
|
|
});
|
|
it('оба → terminate', () => {
|
|
expect(loopTerminationDecision({ ownerDeclaredDone: true, judgeGate3Go: true }).terminate).toBe(true);
|
|
});
|
|
it('truthy-не-true НЕ закрывает (строгий ===true, fail-safe)', () => {
|
|
expect(loopTerminationDecision({ ownerDeclaredDone: 'yes' }).terminate).toBe(false);
|
|
expect(loopTerminationDecision({ judgeGate3Go: 1 }).terminate).toBe(false);
|
|
});
|
|
});
|
|
|
|
import { buildGate3Product, decideGate3Closure } from './loop-termination.mjs';
|
|
|
|
describe('gate-3 buildGate3Product (E-S1 §g2)', () => {
|
|
it('цель+шаги+greens → продукт со сводкой исполнения и green-метками', () => {
|
|
const r = buildGate3Product({
|
|
goal: 'добавить хелпер',
|
|
planSteps: [{ id: 's1', op: 'Write', object: 'tools/x.mjs' }],
|
|
greenRuns: [{ stepId: 's1', criterion: 'x-test' }],
|
|
});
|
|
expect(r.goal).toBe('добавить хелпер');
|
|
expect(r.product).toMatch(/Write tools\/x\.mjs/);
|
|
expect(r.product).toMatch(/green: x-test/);
|
|
expect(r.cards).toEqual([]);
|
|
});
|
|
it('нет greens → пометка «нет доказательств исполнения»', () => {
|
|
const r = buildGate3Product({ goal: 'g', planSteps: [{ op: 'Bash', object: 'echo a' }], greenRuns: [] });
|
|
expect(r.product).toMatch(/нет доказательств исполнения/);
|
|
});
|
|
it('пустой ввод → не бросает, форма {product,goal,cards}', () => {
|
|
const r = buildGate3Product({});
|
|
expect(r).toEqual({ product: expect.stringMatching(/нет доказательств/), goal: '', cards: [] });
|
|
});
|
|
});
|
|
|
|
describe('gate-3 decideGate3Closure (E-S1 §g1/§g4/§t1)', () => {
|
|
it('degraded судья (wired:false) без accept → negotiate, НЕ closed', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: false, decision: 'GO' } });
|
|
expect(r.state).toBe('negotiate');
|
|
expect(r.terminate).toBe(false);
|
|
});
|
|
it('подписанный владелец accept → closed+terminate (приоритет над судьёй)', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: false }, ownerArbitration: 'accept' });
|
|
expect(r.state).toBe('closed');
|
|
expect(r.terminate).toBe(true);
|
|
});
|
|
it('реальный GO движка → closed+terminate', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'GO' } });
|
|
expect(r.state).toBe('closed');
|
|
expect(r.terminate).toBe(true);
|
|
});
|
|
it('владелец continue → open', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'NO-GO' }, ownerArbitration: 'continue' });
|
|
expect(r.state).toBe('open');
|
|
expect(r.terminate).toBe(false);
|
|
});
|
|
it('NO-GO & круг<3 → negotiate', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'NO-GO' }, noGoCount: 1 });
|
|
expect(r.state).toBe('negotiate');
|
|
});
|
|
it('NO-GO & круг>=3 → arbitrate + карточка', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'NO-GO' }, noGoCount: 3 });
|
|
expect(r.state).toBe('arbitrate');
|
|
expect(r.card).toBe(true);
|
|
});
|
|
it('контроллер без подписи (нет accept, NO-GO) закрыть не может', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'NO-GO' }, noGoCount: 0 });
|
|
expect(r.terminate).toBe(false);
|
|
});
|
|
});
|
|
|
|
import { buildOwnerCard } from './loop-termination.mjs';
|
|
|
|
describe('buildOwnerCard (E-S1 Фаза 2 §u2)', () => {
|
|
it('собирает 4 части простым языком (goal/change/verifySteps/boundary)', () => {
|
|
const c = buildOwnerCard({
|
|
goal: 'закрыть петлю по приёмке',
|
|
change: ['владелец видит карточку результата'],
|
|
verifySteps: ['запусти X → увидишь Y'],
|
|
boundary: 'screen-путь не задействован',
|
|
kind: 'machinery',
|
|
honestyChecked: true,
|
|
});
|
|
expect(c.goal).toBe('закрыть петлю по приёмке');
|
|
expect(c.change).toEqual(['владелец видит карточку результата']);
|
|
expect(c.verifySteps).toEqual(['запусти X → увидишь Y']);
|
|
expect(c.boundary).toBe('screen-путь не задействован');
|
|
expect(c.kind).toBe('machinery');
|
|
expect(c.honestyChecked).toBe(true);
|
|
expect(c.warning).toBeUndefined();
|
|
});
|
|
it('kind screen ветвит заглушку шагов проверки', () => {
|
|
const c = buildOwnerCard({ goal: 'g', kind: 'screen', honestyChecked: true });
|
|
expect(c.kind).toBe('screen');
|
|
expect(c.verifySteps[0]).toMatch(/экран/);
|
|
});
|
|
it('пустые входы → честные заглушки, не выдумки', () => {
|
|
const c = buildOwnerCard({});
|
|
expect(c.goal).toMatch(/не указана/);
|
|
expect(c.change[0]).toMatch(/не указано/);
|
|
expect(c.boundary).toMatch(/не указана/);
|
|
expect(c.kind).toBe('machinery');
|
|
});
|
|
it('honestyChecked:false → карточка несёт видимое предупреждение', () => {
|
|
const c = buildOwnerCard({ goal: 'g', honestyChecked: false });
|
|
expect(c.honestyChecked).toBe(false);
|
|
expect(c.warning).toMatch(/автоматическая сверка честности недоступна/);
|
|
});
|
|
});
|
|
|
|
describe('decideGate3Closure Фаза 2 (delivery+cardVerdict §u5)', () => {
|
|
it('internal + код-GO → closed (обратная совместимость)', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'GO' }, delivery: 'internal' });
|
|
expect(r.state).toBe('closed');
|
|
expect(r.terminate).toBe(true);
|
|
});
|
|
it('user-result + код-GO + карточка wired-NO-GO → await-card (владельца не зовём)', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'GO' }, delivery: 'user-result', cardVerdict: { wired: true, decision: 'NO-GO' } });
|
|
expect(r.state).toBe('await-card');
|
|
expect(r.terminate).toBe(false);
|
|
});
|
|
it('user-result + код-GO + карточка GO + нет подписи → await-owner', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'GO' }, delivery: 'user-result', cardVerdict: { wired: true, decision: 'GO' } });
|
|
expect(r.state).toBe('await-owner');
|
|
expect(r.terminate).toBe(false);
|
|
expect(r.unverified).toBeUndefined();
|
|
});
|
|
it('user-result + код-GO + карточка GO + подписанный accept → closed', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'GO' }, delivery: 'user-result', cardVerdict: { wired: true, decision: 'GO' }, ownerArbitration: 'accept' });
|
|
expect(r.state).toBe('closed');
|
|
expect(r.terminate).toBe(true);
|
|
});
|
|
it('user-result + код-GO + карточка degraded → await-owner unverified:true (НЕ висит)', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'GO' }, delivery: 'user-result', cardVerdict: { wired: false } });
|
|
expect(r.state).toBe('await-owner');
|
|
expect(r.unverified).toBe(true);
|
|
expect(r.terminate).toBe(false);
|
|
});
|
|
it('user-result + код-GO + карточки ещё нет → await-card', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'GO' }, delivery: 'user-result', cardVerdict: null });
|
|
expect(r.state).toBe('await-card');
|
|
expect(r.terminate).toBe(false);
|
|
});
|
|
it('user-result контроллер без подписи карточку-GO сам не закрывает', () => {
|
|
const r = decideGate3Closure({ gate3Verdict: { wired: true, decision: 'GO' }, delivery: 'user-result', cardVerdict: { wired: true, decision: 'GO' } });
|
|
expect(r.terminate).toBe(false);
|
|
});
|
|
});
|