ceda265a5d
Баг: границы спанов метились предсказанным номером хода (turnCount+1 в prompt-hook), который уезжает под гейт-петлёй (coverage-хук вставляет служебные ходы, Claude Code очередит промпт). Итог — служебный ход принимался за реальную просьбу (фантомный «Ход 5» в тетради + ложные скрытые вопросы про coverage). Корень: терялся структурный ярлычок isMeta (служебное vs владелец), который уже есть в транскрипте. Теперь: - parseLastExchange читает entry.isMeta -> userIsMeta; - buildRawRecord пишет метку meta=1 в заголовок служебного хода; - realBoundariesFromRaw определяет границы СТРУКТУРНО (meta=1; фолбэк по тексту) — это ОСНОВНОЙ источник; ненадёжный realPromptTurns/prompt-hook-механизм убран; - разбор одного спана вынесен в общий distillSpan (stop-хук и пересборка из сырья). Свод секретаря зелёный (143 теста). Живая пересборка дела на реальной модели дала чистую тетрадь: Шаги по реальным промптам, гейт-шум не плодит скрытые вопросы. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
3.5 KiB
JavaScript
80 lines
3.5 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
||
import { computeSpans, spansToDistill, parseTurnBlock, assembleSpan } from './secretary-span.mjs';
|
||
import { buildRawRecord } from './secretary-layer1.mjs';
|
||
|
||
describe('computeSpans', () => {
|
||
it('границы → отрезки; последний открыт', () => {
|
||
expect(computeSpans([3, 12, 15], 17)).toEqual([
|
||
{ start: 3, end: 11, open: false },
|
||
{ start: 12, end: 14, open: false },
|
||
{ start: 15, end: 17, open: true },
|
||
]);
|
||
});
|
||
it('одна граница → один открытый спан', () => {
|
||
expect(computeSpans([3], 5)).toEqual([{ start: 3, end: 5, open: true }]);
|
||
});
|
||
it('пустой список → нет спанов', () => {
|
||
expect(computeSpans([], 5)).toEqual([]);
|
||
});
|
||
it('неотсортированные/дубли нормализуются', () => {
|
||
expect(computeSpans([12, 3, 3], 13)).toEqual([
|
||
{ start: 3, end: 11, open: false },
|
||
{ start: 12, end: 13, open: true },
|
||
]);
|
||
});
|
||
});
|
||
|
||
describe('spansToDistill', () => {
|
||
it('закрытые спаны с индексом > курсора', () => {
|
||
expect(spansToDistill([3, 12, 15], 17, -1)).toEqual([
|
||
{ start: 3, end: 11, index: 0 },
|
||
{ start: 12, end: 14, index: 1 },
|
||
]);
|
||
});
|
||
it('курсор уже прошёл первый закрытый — отдаём только второй', () => {
|
||
expect(spansToDistill([3, 12, 15], 17, 0)).toEqual([{ start: 12, end: 14, index: 1 }]);
|
||
expect(spansToDistill([3, 12, 15], 17, 1)).toEqual([]);
|
||
});
|
||
it('открытый спан не отдаётся', () => {
|
||
expect(spansToDistill([3, 12], 14, -1)).toEqual([{ start: 3, end: 11, index: 0 }]);
|
||
});
|
||
});
|
||
|
||
describe('parseTurnBlock', () => {
|
||
it('тащит turn, user, assistant, действия с input/result', () => {
|
||
const block = buildRawRecord({
|
||
turn: 4, time: 't', session: 's', user: 'вопрос', assistant: 'ответ',
|
||
actions: [{ tool: 'Read', input: '{"f":"a"}', result: 'СОДЕРЖИМОЕ\nдве строки' }],
|
||
});
|
||
const pt = parseTurnBlock(block);
|
||
expect(pt.turn).toBe(4);
|
||
expect(pt.user).toBe('вопрос');
|
||
expect(pt.assistant).toBe('ответ');
|
||
expect(pt.actions).toEqual([{ tool: 'Read', input: '{"f":"a"}', result: 'СОДЕРЖИМОЕ\nдве строки' }]);
|
||
});
|
||
});
|
||
|
||
describe('assembleSpan', () => {
|
||
const raw = [
|
||
buildRawRecord({ turn: 3, time: 't', session: 's', user: 'настоящий промпт', assistant: 'первый ответ',
|
||
actions: [{ tool: 'Read', input: 'a', result: 'r1' }] }),
|
||
buildRawRecord({ turn: 4, time: 't', session: 's', user: 'Stop hook feedback: x', assistant: 'второй ответ',
|
||
actions: [{ tool: 'Grep', input: 'b', result: 'r2' }] }),
|
||
].join('');
|
||
it('склеивает обмен спана: user из start, assistant и actions со всех ходов', () => {
|
||
const ex = assembleSpan(raw, { start: 3, end: 4 });
|
||
expect(ex.user).toBe('настоящий промпт');
|
||
expect(ex.assistant).toContain('первый ответ');
|
||
expect(ex.assistant).toContain('второй ответ');
|
||
expect(ex.actions).toEqual([
|
||
{ tool: 'Read', input: 'a', result: 'r1' },
|
||
{ tool: 'Grep', input: 'b', result: 'r2' },
|
||
]);
|
||
});
|
||
it('спан из одного хода', () => {
|
||
const ex = assembleSpan(raw, { start: 3, end: 3 });
|
||
expect(ex.user).toBe('настоящий промпт');
|
||
expect(ex.actions).toHaveLength(1);
|
||
});
|
||
});
|