Files
brain/tools/secretary-protocol.test.mjs
T
Дмитрий 2772b197b3 feat(secretary): аудитор скрытых вопросов (9 линз) — второй проход + изоляция реестра
secretary-audit.mjs (новый): 9 линз, buildAuditPrompt -> {system,user}, parseAuditResponse,
applyAudit (новые СВ с номером от хука, мутация+родословная, close/тихо/partial, горящие
блоки Л8/Л9), preserveRegistry (реестр СВ изолирован от reconcile).
protocol: поля hidden/acceptance/tails/nextSvId + рендер горящих блоков и раздела
«Скрытые вопросы (фон)». stop-hook: второй проход после reconcile + снимок реестра ДО
reconcile (reconcile не владеет СВ). + дизайн-спека и план.

97 юнит-тестов зелёные. Живьём подтверждены: наполнение, мутация под тем же номером,
routing Л9 в горящий блок. Известно: старый реестр в деле «линза» уже искажён до фикса.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:42:04 +03:00

126 lines
7.4 KiB
JavaScript
Raw 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 { renderProtocol, EMPTY_PROTOCOL } from './secretary-protocol.mjs';
describe('EMPTY_PROTOCOL', () => {
it('пустой протокол со всеми 9 разделами', () => {
expect(EMPTY_PROTOCOL()).toEqual({
subject: '', status: 'открыто',
decisions: [], alternatives: [], consequences: [],
will: [], open: [], doneNext: [], history: [], steps: [],
hidden: [], acceptance: [], tails: [], nextSvId: 1,
});
});
it('EMPTY_PROTOCOL содержит поля аудитора скрытых вопросов', () => {
const p = EMPTY_PROTOCOL();
expect(p.hidden).toEqual([]);
expect(p.acceptance).toEqual([]);
expect(p.tails).toEqual([]);
expect(p.nextSvId).toBe(1);
});
});
describe('renderProtocol — 9 категорий + шаги', () => {
const proto = {
subject: 'фоновый секретарь', status: 'открыто',
decisions: [{ text: 'D', why: 'w', turns: [7], session: '69992620-x' }],
alternatives: [{ text: 'ALT', turns: [8], session: '69992620-x' }],
consequences: [{ text: 'CONS', turns: [9], session: '69992620-x' }],
will: [{ text: 'W', turns: [10], session: '69992620-x' }],
open: [{ text: 'Q', turns: [11], session: '69992620-x' }],
doneNext: [{ text: 'N', done: false, turns: [12], session: '69992620-x' }],
history: [],
};
it('шапка «Дело» со статусом/хозяином/целью (по opts)', () => {
const md = renderProtocol(proto, { work: 'создание-секретаря', date: '2026-06-22 11:00' });
expect(md).toContain('**Дело:** создание-секретаря');
expect(md).toContain('**Статус:** открыто');
expect(md).toContain('**Хозяин:** владелец');
expect(md).toContain('**Цель:** фоновый секретарь');
});
it('разделы Альтернативы и Последствия / цена', () => {
const md = renderProtocol(proto);
expect(md).toContain('## Альтернативы');
expect(md).toContain('- ALT');
expect(md).toContain('## Последствия / цена');
expect(md).toContain('- CONS');
});
it('провенанс [→N] без метки файла @ (имя файла — только в Шагах)', () => {
const md = renderProtocol(proto);
expect(md).toContain('- D — w [→7]');
expect(md).not.toContain('@69992620');
expect(md).toContain('## Твоя воля / запреты');
expect(md).toContain('## Открытые вопросы');
});
it('история: тайм-линия toggle (внёс →, вынес ←, вернул →, снова вынес ←)', () => {
const md = renderProtocol({
subject: '', status: 'открыто', steps: [],
decisions: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [],
history: [{ text: 'пункт X', events: [{ turn: 41, dir: 'in' }, { turn: 43, dir: 'out' }, { turn: 55, dir: 'in' }, { turn: 70, dir: 'out' }] }],
});
expect(md).toContain('~~пункт X~~ [→41] [←43] [→55] [←70]');
});
it('провенанс с несколькими ходами: [→33], [50]', () => {
const md = renderProtocol({
subject: '', status: 'открыто', steps: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [], history: [],
decisions: [{ text: 'Y', why: null, turns: [33, 50] }],
});
expect(md).toContain('- Y [→33], [50]');
});
it('зачёркивание во всех корзинах', () => {
const md = renderProtocol({
subject: '', status: 'открыто', history: [],
decisions: [{ text: 'D', struck: true }], alternatives: [{ text: 'A', struck: true }],
consequences: [{ text: 'C', struck: true }], will: [{ text: 'W', struck: true }],
open: [{ text: 'Q', struck: true }], doneNext: [{ text: 'N', struck: true, done: false }],
});
for (const t of ['~~D~~', '~~A~~', '~~C~~', '~~W~~', '~~Q~~', '~~N~~']) expect(md).toContain(t);
});
it('раздел Шаги: ссылка на отдельный файл хода (s.file) вместо общего лога', () => {
const md = renderProtocol({
subject: '', status: 'открыто', history: [],
decisions: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [],
steps: [{ turn: 1, session: 'sess', file: 'ходы/turn-1.log', text: 'Ход 1 — я: x · ты: y · делал: —' }],
});
expect(md).toContain('Ход 1 — я: x · ты: y · делал: — · ходы/turn-1.log');
expect(md).not.toContain('· sess.log');
});
it('раздел Шаги (Слой 1): строка на ход + название файла полного хода в конце строки', () => {
const md = renderProtocol({
subject: '', status: 'открыто', history: [],
decisions: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [],
steps: [{ turn: 1, session: '69992620-x', text: 'Ход 1 — я: про оглавление · ты: тема+время · делал: читал хук' }],
});
expect(md).toContain('## Шаги (Слой 1)');
expect(md).toContain('Ход 1 — я: про оглавление · ты: тема+время · делал: читал хук · 69992620-x.log');
});
it('рендерит горящие блоки и скрытые вопросы с мутацией', () => {
const p = { ...EMPTY_PROTOCOL(),
acceptance: [{ text: 'заявлено работает', born: 14, lastTouch: 14, done: false },
{ text: 'старое', born: 1, lastTouch: 1, done: true }],
tails: [{ text: 'не запушено', born: 23, lastTouch: 23, done: false }],
hidden: [{ id: 'СВ-2', lens: 'Л1', status: 'мутировал', text: 'новая', born: 1, lastTouch: 11,
lineage: [{ turn: 1, text: 'старая' }] }] };
const md = renderProtocol(p, { work: 'x', date: '2026-06-22 14:00' });
expect(md).toContain('⚠️ ЗАЯВЛЕНО ГОТОВО');
expect(md).toContain('заявлено работает');
expect(md).not.toContain('старое'); // done — не показываем
expect(md).toContain('🧹 ХВОСТЫ');
expect(md).toContain('Скрытые вопросы');
expect(md).toContain('~~старая~~ → новая'); // мутация зачёркиванием
});
it('Шаги: разделитель «—— сессия X ——» при смене сессии (не перед первой)', () => {
const md = renderProtocol({
subject: '', status: 'открыто', history: [],
decisions: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [],
steps: [
{ turn: 1, session: 'sА', text: 'Ход 1 — я: a · ты: b · делал: —' },
{ turn: 2, session: 'sА', text: 'Ход 2 — я: c · ты: d · делал: —' },
{ turn: 3, session: 'sБ', text: 'Ход 3 — я: e · ты: f · делал: —' },
],
});
expect(md).toContain('—— сессия sБ ——');
expect(md).not.toContain('—— сессия sА ——'); // перед первой сессией разделителя нет
expect(md.indexOf('—— сессия sБ ——')).toBeLessThan(md.indexOf('Ход 3'));
});
});