81da2e2c45
Новый observer-verdicts: читает персистентный verdict-snapshot-<sid>.json и сводит к четырём звеньям (последний вердикт по ts на звено). Эпизод наблюдателя получил поле verdicts из снимка текущей сессии → по логам восстановимо, на каком звене план отскочил. Раньше в эпизоде был только сигнал роутера. Граница не тронута (observer-stop-hook, recommended_chain, цепочки). Хвост спеки роутера §7 (логирование решающих), эпик роутер-реестр этап 3, item 3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
58 lines
2.7 KiB
JavaScript
58 lines
2.7 KiB
JavaScript
// tools/observer-verdicts.test.mjs
|
|
import { describe, it, expect } from 'vitest';
|
|
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { readVerdictSnapshot, extractFourVerdicts } from './observer-verdicts.mjs';
|
|
|
|
describe('extractFourVerdicts (D1)', () => {
|
|
it('снимок с четырьмя звеньями → все четыре с правильным status', () => {
|
|
const snap = {
|
|
router: { status: 'recommend', reason: 'r', ts: 1 },
|
|
'mentor:plan': { status: 'GO', reason: 'm', ts: 2 },
|
|
'judge:plan': { status: 'GO', reason: 'j', ts: 3 },
|
|
'judge:gate3': { status: 'NO-GO', reason: 'g', ts: 4 },
|
|
};
|
|
const v = extractFourVerdicts(snap);
|
|
expect(v.router.status).toBe('recommend');
|
|
expect(v.mentor.status).toBe('GO');
|
|
expect(v.judge.status).toBe('GO');
|
|
expect(v.gate3.status).toBe('NO-GO');
|
|
});
|
|
it('несколько записей звена → берётся последняя по ts', () => {
|
|
const snap = {
|
|
'mentor:plan': { status: 'NO-GO', reason: 'old', ts: 1 },
|
|
'mentor:spec': { status: 'GO', reason: 'new', ts: 5 },
|
|
};
|
|
expect(extractFourVerdicts(snap).mentor).toEqual({ status: 'GO', reason: 'new' });
|
|
});
|
|
it('отсутствующее звено → null', () => {
|
|
expect(extractFourVerdicts({ router: { status: 'recommend', ts: 1 } }).mentor).toBeNull();
|
|
});
|
|
it('пустой снимок → все четыре null', () => {
|
|
const v = extractFourVerdicts({});
|
|
expect(v).toEqual({ router: null, mentor: null, judge: null, gate3: null });
|
|
});
|
|
});
|
|
|
|
describe('readVerdictSnapshot (D1)', () => {
|
|
it('читает файл снимка и агрегирует последнюю запись по стадии', () => {
|
|
const baseDir = mkdtempSync(join(tmpdir(), 'verdict-snap-'));
|
|
const obj = {
|
|
h1: { router: { status: 'recommend', reason: 'r', ts: 1 }, 'mentor:plan': { status: 'NO-GO', reason: 'a', ts: 2 } },
|
|
h2: { 'mentor:plan': { status: 'GO', reason: 'b', ts: 9 }, 'judge:plan': { status: 'GO', reason: 'j', ts: 10 } },
|
|
};
|
|
writeFileSync(join(baseDir, 'verdict-snapshot-sX.json'), JSON.stringify(obj));
|
|
const snap = readVerdictSnapshot('sX', baseDir);
|
|
expect(snap['router'].status).toBe('recommend');
|
|
expect(snap['mentor:plan']).toMatchObject({ status: 'GO', ts: 9 }); // последняя по ts
|
|
expect(snap['judge:plan'].status).toBe('GO');
|
|
rmSync(baseDir, { recursive: true, force: true });
|
|
});
|
|
it('нет файла → {} (fail-safe)', () => {
|
|
const baseDir = mkdtempSync(join(tmpdir(), 'verdict-snap-'));
|
|
expect(readVerdictSnapshot('nope', baseDir)).toEqual({});
|
|
rmSync(baseDir, { recursive: true, force: true });
|
|
});
|
|
});
|