Files
brain/tools/secretary-prompt-hook.test.mjs
T
Дмитрий 90f1360065 feat(secretary): устойчивость к обрывам — источник=транскрипт, склейка продолжений, догон сессий
Секретарь перестал терять промпт владельца при обрыве (сбой API / ручной стоп /
жёсткий крах). Источник правды — транскрипт на диске: сырьё (Слой 1) пересобирается
из всего транскрипта на каждом завершении, а не дописывается по последнему обмену.

- classifyEntry/assembleExchanges: распознавание машинных меток (isApiErrorMessage,
  [Request interrupted by user] обе формы, isCompactSummary, isMeta) — метка не считается
  настоящим промптом; промпт после обрыва помечается продолжением (cont=1), хвост — tail=1.
- realBoundariesFromRaw: продолжение не открывает новый спан (одна работа не дробится).
- честные пометки спана: «(связь прерывалась — продолжено)» / «(прервана, не завершена)».
- stop-хук: пересборка сырья из транскрипта + догон недоразобранного хвоста прошлых
  (умерших) сессий дела при «включи секретаря <дело>» (_sessions.json, secretary-sessions).
- parseLastExchange → тонкая обёртка над assembleExchanges (без дубля логики).

Свод секретаря зелёный: 172 теста / 12 файлов.
Спека: docs/superpowers/specs/2026-06-23-secretary-interruption-resilience-spec.md
План: docs/superpowers/plans/2026-06-23-secretary-interruption-resilience.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 04:19:16 +03:00

47 lines
2.7 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { planActivation, planPromptTurn } from './secretary-prompt-hook.mjs';
describe('planPromptTurn — вкл/выкл (границы спанов определяет stop-хук из сырья)', () => {
it('cmd=off → флажок mode:closing с сохранением полей', () => {
const r = planPromptTurn({ cmd: 'off', flag: { mode: 'on', work: 'дело', spanCursor: 0, session: 's' } });
expect(r.flag.mode).toBe('closing');
expect(r.flag.work).toBe('дело');
expect(r.flag.spanCursor).toBe(0);
});
it('обычный промпт (cmd=null) → ничего не пишем', () => {
expect(planPromptTurn({ cmd: null, flag: { mode: 'on' } }).flag).toBeNull();
});
});
describe('planActivation — решение хука: активировать или переспросить', () => {
it('новое имя (нет похожих) — флажок on с work', () => {
const r = planActivation({ requested: 'биллинг', existing: ['general'], startedAtTurn: 3, session: 's1' });
expect(r.confirm).toBe(false);
expect(r.flag).toEqual({ mode: 'on', startedAtTurn: 3, work: 'биллинг', session: 's1', catchUp: [] });
});
it('точное совпадение — флажок on с существующим именем', () => {
const r = planActivation({ requested: 'general', existing: ['general'], startedAtTurn: 0, session: 's2' });
expect(r.confirm).toBe(false);
expect(r.flag.work).toBe('general');
});
it('похожее имя — переспросить: флажок не ставить, дать кандидатов и подсказку', () => {
const r = planActivation({ requested: 'создание', existing: ['создание-секретаря'], startedAtTurn: 1, session: 's3' });
expect(r.confirm).toBe(true);
expect(r.flag).toBeUndefined();
expect(r.candidates).toContain('создание-секретаря');
expect(r.context).toContain('создание-секретаря');
});
it('проставляет catchUp из прошлых сессий дела', () => {
const plan = planActivation({
requested: 'наставник', existing: ['наставник'], startedAtTurn: 0, session: 's2',
sessionsOfCase: [{ session: 's1', cursor: 2 }, { session: 's2', cursor: 0 }],
});
expect(plan.confirm).toBe(false);
expect(plan.flag.catchUp).toEqual([{ session: 's1', cursor: 2 }]);
});
it('без прошлых сессий — catchUp пустой', () => {
const plan = planActivation({ requested: 'новое', existing: [], session: 's1', sessionsOfCase: [] });
expect(plan.flag.catchUp).toEqual([]);
});
});