397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
123 lines
7.5 KiB
JavaScript
123 lines
7.5 KiB
JavaScript
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);
|
||
});
|
||
});
|