Files
brain/tools/round-control.test.mjs

123 lines
7.5 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 { 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);
});
});