Files
brain/tools/secretary-protocol.test.mjs
T

148 lines
8.8 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,
candidates: [], nextKdId: 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);
});
it('содержит грядку кандидатов (candidates[] + nextKdId)', () => {
const p = EMPTY_PROTOCOL();
expect(p.candidates).toEqual([]);
expect(p.nextKdId).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('кап родословной: ~~первая~~ → текущая (середина скрыта, данные в JSON целы)', () => {
const p = { ...EMPTY_PROTOCOL(),
hidden: [{ id: 'СВ-1', lens: 'Л3', status: 'мутировал', text: 'нынешняя', born: 3, lastTouch: 15,
lineage: [{ turn: 3, text: 'первая' }, { turn: 9, text: 'средняя-1' }, { turn: 12, text: 'средняя-2' }] }] };
const md = renderProtocol(p, { work: 'x', date: 'd' });
expect(md).toContain('~~первая~~ → нынешняя');
expect(md).not.toContain('средняя-1');
expect(md).not.toContain('средняя-2');
});
it('«висит N промптов» считает спаны, прошедшие с born (не сырые ходы)', () => {
const p = { ...EMPTY_PROTOCOL(),
acceptance: [{ text: 'заявлено готово', born: 3, lastTouch: 3, done: false }] };
// реальные промпты на ходах 3,12,15,22; текущий ход 31 → с born=3 прошло 3 промпта (12,15,22)
const md = renderProtocol(p, { work: 'x', date: 'd', turn: 31, realPromptTurns: [3, 12, 15, 22] });
expect(md).toContain('висит 3 промптов');
});
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'));
});
});