import { describe, it, expect } from 'vitest'; import { codeRoundCheck, ROUND_OUTCOMES, decideRoundOutcome, runRoundControl } from './round-control.mjs'; import { addQuestion, answerQuestion } from './question-slots.mjs'; describe('codeRoundCheck ($0 — скил вызван по журналу + нет повисших вопросов)', () => { it('всё закрыто → ok', () => { let s = addQuestion([], { id: 'q1', text: 't' }); s = answerQuestion(s, 'q1', 'a'); const r = codeRoundCheck({ slots: s, invokedSkills: ['brainstorming'], requiredSkills: ['brainstorming'] }); expect(r.ok).toBe(true); expect(r.reasons).toEqual([]); }); it('повисший вопрос → not ok + причина', () => { const s = addQuestion([], { id: 'q1', text: 't' }); const r = codeRoundCheck({ slots: s, invokedSkills: [], requiredSkills: [] }); expect(r.ok).toBe(false); expect(r.reasons.some((x) => /повис/i.test(x))).toBe(true); }); it('требуемый скил не вызван (журнал) → not ok + причина', () => { const r = codeRoundCheck({ slots: [], invokedSkills: [], requiredSkills: ['writing-plans'] }); expect(r.ok).toBe(false); expect(r.reasons.some((x) => /не вызван/i.test(x))).toBe(true); }); }); describe('decideRoundOutcome (§3А — 3 прохода → владелец; двойной терминатор)', () => { it('код+смысл сошлись → proceed', () => { expect(decideRoundOutcome({ codeOk: true, routerFaithful: true }).outcome).toBe('proceed'); }); it('пробел и проходы не исчерпаны → soft-return', () => { expect(decideRoundOutcome({ codeOk: false, routerFaithful: true, roundCount: 1, maxRounds: 3 }).outcome).toBe('soft-return'); }); it('пробел и проходы исчерпаны → escalate-owner', () => { expect(decideRoundOutcome({ codeOk: true, routerFaithful: false, roundCount: 3, maxRounds: 3 }).outcome).toBe('escalate-owner'); }); it('ROUND_OUTCOMES — закрытый список', () => { expect(ROUND_OUTCOMES).toEqual(['proceed', 'soft-return', 'escalate-owner']); }); }); describe('decideRoundOutcome — консервативность при битом счётчике (аудит F4)', () => { it('невалидный roundCount (отриц) при пробеле → escalate-owner (не вечный loop)', () => { expect(decideRoundOutcome({ codeOk: false, routerFaithful: true, roundCount: -1 }).outcome).toBe('escalate-owner'); }); it('невалидный roundCount (NaN) при пробеле → escalate-owner', () => { expect(decideRoundOutcome({ codeOk: false, routerFaithful: true, roundCount: NaN }).outcome).toBe('escalate-owner'); }); it('битый roundCount, но круг сошёлся → всё равно proceed', () => { expect(decideRoundOutcome({ codeOk: true, routerFaithful: true, roundCount: NaN }).outcome).toBe('proceed'); }); }); describe('runRoundControl (оркестратор: код + роутер-верность смысла + решение)', () => { const okSlots = () => { let s = addQuestion([], { id: 'q1', text: 't' }); return answerQuestion(s, 'q1', 'a'); }; it('код ok + смысл верен → proceed', async () => { const r = await runRoundControl({ slots: okSlots(), invokedSkills: ['b'], requiredSkills: ['b'], verityCall: async () => ({ ok: true, faithful: true }) }); expect(r.outcome).toBe('proceed'); expect(r.routerFaithful).toBe(true); }); it('код ok, смысл искажён (faithful=false) → soft-return (есть проходы)', async () => { const r = await runRoundControl({ slots: okSlots(), invokedSkills: ['b'], requiredSkills: ['b'], roundCount: 0, verityCall: async () => ({ ok: true, faithful: false }) }); expect(r.outcome).toBe('soft-return'); }); it('повисший вопрос (код not ok) → soft-return даже при верном смысле', async () => { const s = addQuestion([], { id: 'q1', text: 't' }); const r = await runRoundControl({ slots: s, verityCall: async () => ({ ok: true, faithful: true }) }); expect(r.outcome).toBe('soft-return'); expect(r.codeCheck.ok).toBe(false); }); it('сбой verityCall → routerFaithful=false (безопасно, пробел)', async () => { const r = await runRoundControl({ slots: okSlots(), invokedSkills: ['b'], requiredSkills: ['b'], verityCall: async () => { throw new Error('net'); } }); expect(r.routerFaithful).toBe(false); expect(r.outcome).not.toBe('proceed'); }); it('пробел и проходы исчерпаны → escalate-owner', async () => { const r = await runRoundControl({ slots: okSlots(), invokedSkills: ['b'], requiredSkills: ['b'], roundCount: 3, maxRounds: 3, verityCall: async () => ({ ok: true, faithful: false }) }); expect(r.outcome).toBe('escalate-owner'); }); }); describe('runRoundControl — авто-инкремент по roundKey (F2: терминатор не зависит от внешнего счётчика)', () => { const okSlots = () => { let s = addQuestion([], { id: 'q1', text: 't' }); return answerQuestion(s, 'q1', 'a'); }; const store0 = () => { const store = {}; return { store, readRounds: (k) => store[k] ?? 0, writeRounds: (k, n) => { store[k] = n; } }; }; it('повторные вызовы с тем же roundKey при стойком пробеле САМИ доходят до escalate (счётчик НЕ передаём)', async () => { const { readRounds, writeRounds } = store0(); const call = () => runRoundControl({ slots: okSlots(), invokedSkills: ['b'], requiredSkills: ['b'], roundKey: 'plan-1', readRounds, writeRounds, maxRounds: 3, verityCall: async () => ({ ok: true, faithful: false }), }); expect((await call()).outcome).toBe('soft-return'); expect((await call()).outcome).toBe('soft-return'); expect((await call()).outcome).toBe('soft-return'); expect((await call()).outcome).toBe('escalate-owner'); }); it('круг сошёлся → счётчик roundKey сбрасывается (следующий разговор с нуля)', async () => { const store = { 'plan-2': 2 }; const r = await runRoundControl({ slots: okSlots(), invokedSkills: ['b'], requiredSkills: ['b'], roundKey: 'plan-2', readRounds: (k) => store[k] ?? 0, writeRounds: (k, n) => { store[k] = n; }, verityCall: async () => ({ ok: true, faithful: true }), }); expect(r.outcome).toBe('proceed'); expect(store['plan-2']).toBe(0); }); it('без roundKey — поведение прежнее (обратная совместимость: явный roundCount)', async () => { const r = await runRoundControl({ slots: okSlots(), invokedSkills: ['b'], requiredSkills: ['b'], roundCount: 0, verityCall: async () => ({ ok: true, faithful: false }), }); expect(r.outcome).toBe('soft-return'); }); // fix: tools/round-control.mjs (C, аудит M1-M4) — пустой roundKey не должен слипать сессии в один бакет it('пустой roundKey НЕ активирует managed-режим (общий бакет закрыт)', async () => { let wrote = false; const r = await runRoundControl({ slots: okSlots(), invokedSkills: ['b'], requiredSkills: ['b'], roundKey: '', readRounds: () => 5, writeRounds: () => { wrote = true; }, verityCall: async () => ({ ok: true, faithful: false }), maxRounds: 3, }); expect(wrote).toBe(false); expect(r.roundCount).toBe(0); }); });