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); }); });