ab8abe2c87
- stampProvenance ведёт История-таймлайн (in/out) и многоходовый провенанс при смене зачёркивания строки - splitRawIntoTurns/prepareTurnFiles: нарезка raw на <дело>/ходы/turn-N.log; Шаги ссылаются на файл хода - buildStepsFromRaw + обработчик off: Шаг на КАЖДЫЙ ход (без пропусков выкл-ходов) - neutralizeMarkers в buildRawRecord: защита от самозагрязнения лога копиями маркеров - полная форма протокола (9 категорий) + дело создание-секретаря приведено к виду; набор секретаря 56/56 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
103 lines
6.2 KiB
JavaScript
103 lines
6.2 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { buildRawRecord, buildStepLine, splitRawIntoTurns, turnFileName, prepareTurnFiles, buildStepsFromRaw } from './secretary-layer1.mjs';
|
|
|
|
describe('обезвреживание маркеров на записи (от самозагрязнения лога)', () => {
|
|
it('маркеры внутри текста реплик/действий не дают лишних структурных совпадений', () => {
|
|
const rec = buildRawRecord({
|
|
turn: 7, time: 't', session: 's',
|
|
user: 'смотри: === ХОД turn=1 · x · session=y ===\nтело\n=== КОНЕЦ ХОДА ===',
|
|
assistant: 'ок',
|
|
actions: [{ tool: 'Edit', input: 'new_string:"=== КОНЕЦ ХОДА ==="', result: '' }],
|
|
});
|
|
expect((rec.match(/=== ХОД turn=/g) || []).length).toBe(1); // только реальный заголовок
|
|
expect((rec.match(/=== КОНЕЦ ХОДА ===/g) || []).length).toBe(1); // только реальный конец
|
|
});
|
|
});
|
|
|
|
describe('buildStepsFromRaw — Шаг на КАЖДЫЙ ход (пересборка на остановке)', () => {
|
|
const raw = [
|
|
'=== ХОД turn=1 · t · session=s ===', '[ЮЗЕР]', 'привет', '[АССИСТЕНТ]', 'ответ раз два три', '[ДЕЙСТВИЕ] Read in=x', '[ВЫДАЧА] Read', '', '=== КОНЕЦ ХОДА ===', '',
|
|
'=== ХОД turn=2 · t · session=s ===', '[ЮЗЕР]', 'второй вопрос достаточно длинный', '[АССИСТЕНТ]', 'второй ответ', '=== КОНЕЦ ХОДА ===', '',
|
|
].join('\n');
|
|
it('по шагу на каждый ход, с сессией и инструментами', () => {
|
|
const steps = buildStepsFromRaw(raw, 's');
|
|
expect(steps.map((s) => s.turn)).toEqual([1, 2]);
|
|
expect(steps[0].session).toBe('s');
|
|
expect(steps[0].text).toContain('Ход 1 — я: привет');
|
|
expect(steps[0].text).toContain('делал: Read');
|
|
expect(steps[1].text).toContain('Ход 2 — я: второй вопрос');
|
|
});
|
|
});
|
|
|
|
describe('нарезка сырья на отдельные файлы ходов (при остановке секретаря)', () => {
|
|
const raw = [
|
|
'=== ХОД turn=1 · t · session=s ===', '[ЮЗЕР]', 'аа', '[АССИСТЕНТ]', 'бб', '=== КОНЕЦ ХОДА ===', '',
|
|
'=== ХОД turn=2 · t · session=s ===', '[ЮЗЕР]', 'вв', '[АССИСТЕНТ]', 'гг', '=== КОНЕЦ ХОДА ===', '',
|
|
].join('\n');
|
|
it('splitRawIntoTurns даёт по блоку на каждый ход', () => {
|
|
const parts = splitRawIntoTurns(raw);
|
|
expect(parts.map((p) => p.turn)).toEqual([1, 2]);
|
|
expect(parts[0].block).toContain('turn=1');
|
|
expect(parts[0].block.trim().endsWith('=== КОНЕЦ ХОДА ===')).toBe(true);
|
|
expect(parts[1].block).toContain('вв');
|
|
});
|
|
it('turnFileName — короткое имя файла хода', () => {
|
|
expect(turnFileName(3)).toBe('turn-3.log');
|
|
});
|
|
it('prepareTurnFiles: по файлу на ход + ссылка ходы/turn-N.log в каждый шаг', () => {
|
|
const proto = { steps: [{ turn: 1, session: 's', text: 'Ход 1' }, { turn: 2, session: 's', text: 'Ход 2' }] };
|
|
const { files, steps } = prepareTurnFiles(raw, proto);
|
|
expect(files.map((f) => f.name)).toEqual(['turn-1.log', 'turn-2.log']);
|
|
expect(files[0].content).toContain('turn=1');
|
|
expect(steps[0].file).toBe('ходы/turn-1.log');
|
|
expect(steps[1].file).toBe('ходы/turn-2.log');
|
|
});
|
|
it('prepareTurnFiles не трогает шаг, для которого нет блока в сырье', () => {
|
|
const proto = { steps: [{ turn: 9, session: 's', text: 'Ход 9' }] };
|
|
const { steps } = prepareTurnFiles(raw, proto);
|
|
expect(steps[0].file).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('buildRawRecord', () => {
|
|
it('содержит заголовок с turn, реплики и действие', () => {
|
|
const rec = buildRawRecord({
|
|
turn: 7, time: '2026-06-22T10:00:00Z', session: 'abc',
|
|
user: 'привет', assistant: 'ответ',
|
|
actions: [{ tool: 'Read', input: '{"f":"x"}', result: 'текст' }],
|
|
});
|
|
expect(rec).toContain('turn=7');
|
|
expect(rec).toContain('[ЮЗЕР]');
|
|
expect(rec).toContain('[ДЕЙСТВИЕ] Read');
|
|
expect(rec.trim().endsWith('=== КОНЕЦ ХОДА ===')).toBe(true);
|
|
});
|
|
it('без действий — блок без [ДЕЙСТВИЕ]', () => {
|
|
const rec = buildRawRecord({ turn: 1, time: 't', session: 's', user: 'u', assistant: 'a' });
|
|
expect(rec).not.toContain('[ДЕЙСТВИЕ]');
|
|
});
|
|
});
|
|
|
|
describe('buildStepLine', () => {
|
|
it('формат «Ход N — я: … · ты: … · делал: <инструменты>», без служебных строк', () => {
|
|
const s = buildStepLine({ turn: 5, user: 'сделай флажок.', assistant: 'экономия: 100%\nГотово.', actions: ['Edit', 'PowerShell', 'Edit'] });
|
|
expect(s).toContain('Ход 5 — я: сделай флажок.');
|
|
expect(s).toContain('· ты: Готово.');
|
|
expect(s).toContain('· делал: Edit, PowerShell');
|
|
expect(s).not.toContain('экономия');
|
|
});
|
|
it('пустой вопрос → (без вопроса); без действий → —', () => {
|
|
const s = buildStepLine({ turn: 2, user: '', assistant: 'a.' });
|
|
expect(s).toContain('я: (без вопроса)');
|
|
expect(s).toContain('делал: —');
|
|
});
|
|
it('убирает ведущую нумерацию (не «я: 1.») и берёт содержательную фразу', () => {
|
|
const s = buildStepLine({ turn: 3, user: '1. содержание никчёмное, нужно о чём и где', assistant: 'Понял.' });
|
|
expect(s).toContain('я: содержание никчёмное');
|
|
expect(s).not.toContain('я: 1.');
|
|
});
|
|
it('служебный ход — метка вместо шума', () => {
|
|
expect(buildStepLine({ turn: 1, user: 'Stop hook feedback: coverage missing', assistant: '' })).toContain('я: (гейт проверки)');
|
|
expect(buildStepLine({ turn: 2, user: 'Base directory for this skill: C:\\x\\skills\\writing-plans\\SKILL.md', assistant: 'x.' })).toContain('я: (навык: writing-plans)');
|
|
});
|
|
});
|