69e20099db
Второй аудит машин 1-4 другим объективом (sharp-edges: устойчивость к неправильному применению / мягкие умолчания / совпадение по пустоте-подстроке). Криптоядра здоровы (подтверждено). 8 реальных дыр закрыты по TDD: M3: - coverage-machine F-1: покрытие считалось по двусторонней ПОДСТРОКЕ — produces "a" покрывал запрос "audit-rls-policy" (ложное «всё покрыто»). Новый tokensCover: точное равенство ИЛИ подмножество слов по границам. coveringSkill + coverageRegistry. - router-engine F-8: confidence не проверялся на диапазон — 5/Infinity проходили как «уверен» (обход воздержания 5.2), -3 как принуд. abstain. validateTrace: [0,1] finite. - round-control C: пустой roundKey="" активировал managed-режим (!= null) → все сессии делили один счётчик-бакет. Теперь managed требует непустую строку. - router-learning-queue G: повторное approve уже-решённого id повторно клало запись в фонд (дубль). applyApprovalBatch: переводит только status==='pending'. M2: - plan-lock F5: шаг с пустым object был джокером (object:'' матчил действие, чей путь не извлёкся → object''). actionMatchesStep: пустой object шага не матчит ничего. M4 (инертна; чистые fail-closed правки кода, корректны и при включении): - judge-slop-counter H: битый/null вердикт в списке ронял счёт (v.missing на null). Теперь не крашит, считается халтурой (безопасная сторона). - judge-engine J: consensusDecision на пустом/битом списке дрейфовал к GO. Теперь GO только если есть голоса И каждый чистый GO; иначе NO-GO (fail-closed для hard-risk). - judge-orchestrator K: finalGate снимал вето пола на любой falsy floorBlocked (undefined от упавшей проверки = fail-open). Теперь снять может только явный false. Регрессия tools-only 2555 passed + 2 skip (+15 TDD-тестов, 0 регрессий). Осознанно НЕ менялось (без призраков): - M1 receipt-sign domain default '' / разделитель пробел — backward-compat контракт (тест 18-19), инъективен на enum-доменах без пробелов. - M1 action-journal атомарность записи головы + битая .jsonl строка — fail-closed (битьё → verifyChain ok:false → стена блокирует); чистого behavioral-теста нет. - M3 round-control requiredSkills=[] — контракт вызывающего (пустой = не требуется). Co-Authored-By: Claude Opus 4.8 (1M context) <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);
|
||
});
|
||
});
|