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>
105 lines
5.9 KiB
JavaScript
105 lines
5.9 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
||
import { parseLastExchange } from './secretary-transcript.mjs';
|
||
|
||
describe('parseLastExchange', () => {
|
||
it('тащит последний user + assistant + действия', () => {
|
||
const t = [
|
||
JSON.stringify({ message: { role: 'user', content: 'старое' } }),
|
||
JSON.stringify({ message: { role: 'user', content: 'привет' } }),
|
||
JSON.stringify({ message: { role: 'assistant', content: [
|
||
{ type: 'text', text: 'ответ' },
|
||
{ type: 'tool_use', name: 'Read', input: { f: 'x' } },
|
||
] } }),
|
||
].join('\n');
|
||
const ex = parseLastExchange(t);
|
||
expect(ex.user).toBe('привет');
|
||
expect(ex.assistant).toBe('ответ');
|
||
expect(ex.actions).toEqual([{ tool: 'Read', input: '{"f":"x"}' }]);
|
||
});
|
||
it('строковый content тоже понимает; битые строки пропускает', () => {
|
||
const t = ['не-json', JSON.stringify({ message: { role: 'user', content: 'у' } }),
|
||
JSON.stringify({ message: { role: 'assistant', content: 'а' } })].join('\n');
|
||
const ex = parseLastExchange(t);
|
||
expect(ex.user).toBe('у');
|
||
expect(ex.assistant).toBe('а');
|
||
expect(ex.actions).toEqual([]);
|
||
});
|
||
it('пропускает tool_result (role:user) — берёт настоящий промпт + все действия', () => {
|
||
const t = [
|
||
JSON.stringify({ message: { role: 'user', content: 'настоящий вопрос' } }),
|
||
JSON.stringify({ message: { role: 'assistant', content: [
|
||
{ type: 'text', text: 'думаю' }, { type: 'tool_use', name: 'Read', input: { f: 'a' } }] } }),
|
||
JSON.stringify({ message: { role: 'user', content: [{ type: 'tool_result', content: 'результат' }] } }),
|
||
JSON.stringify({ message: { role: 'assistant', content: [{ type: 'text', text: 'готово' }] } }),
|
||
].join('\n');
|
||
const ex = parseLastExchange(t);
|
||
expect(ex.user).toBe('настоящий вопрос');
|
||
expect(ex.assistant).toContain('думаю');
|
||
expect(ex.assistant).toContain('готово');
|
||
expect(ex.actions).toEqual([{ tool: 'Read', input: '{"f":"a"}' }]);
|
||
});
|
||
});
|
||
|
||
describe('parseLastExchange — захват выдачи инструмента (tool_result по tool_use_id)', () => {
|
||
it('привязывает результат к действию по совпадающему id', () => {
|
||
const t = [
|
||
JSON.stringify({ message: { role: 'user', content: 'вопрос' } }),
|
||
JSON.stringify({ message: { role: 'assistant', content: [
|
||
{ type: 'tool_use', id: 'tu_1', name: 'Read', input: { f: 'a' } }] } }),
|
||
JSON.stringify({ message: { role: 'user', content: [
|
||
{ type: 'tool_result', tool_use_id: 'tu_1', content: 'СОДЕРЖИМОЕ ФАЙЛА' }] } }),
|
||
].join('\n');
|
||
const ex = parseLastExchange(t);
|
||
expect(ex.actions).toEqual([{ tool: 'Read', input: '{"f":"a"}', result: 'СОДЕРЖИМОЕ ФАЙЛА' }]);
|
||
});
|
||
it('результат из массива text-блоков склеивается', () => {
|
||
const t = [
|
||
JSON.stringify({ message: { role: 'user', content: 'в' } }),
|
||
JSON.stringify({ message: { role: 'assistant', content: [
|
||
{ type: 'tool_use', id: 'tu_9', name: 'Bash', input: {} }] } }),
|
||
JSON.stringify({ message: { role: 'user', content: [
|
||
{ type: 'tool_result', tool_use_id: 'tu_9', content: [{ type: 'text', text: 'строка вывода' }] }] } }),
|
||
].join('\n');
|
||
const ex = parseLastExchange(t);
|
||
expect(ex.actions[0].result).toBe('строка вывода');
|
||
});
|
||
it('длинный результат НЕ обрезается (полная картина для секретаря)', () => {
|
||
const big = 'x'.repeat(5000);
|
||
const t = [
|
||
JSON.stringify({ message: { role: 'user', content: 'в' } }),
|
||
JSON.stringify({ message: { role: 'assistant', content: [
|
||
{ type: 'tool_use', id: 'tu_2', name: 'Read', input: {} }] } }),
|
||
JSON.stringify({ message: { role: 'user', content: [
|
||
{ type: 'tool_result', tool_use_id: 'tu_2', content: big }] } }),
|
||
].join('\n');
|
||
const ex = parseLastExchange(t);
|
||
expect(ex.actions[0].result).toBe(big); // целиком
|
||
expect(ex.actions[0].result.endsWith('…')).toBe(false);
|
||
});
|
||
it('помечает userIsMeta для служебного сообщения (isMeta:true на записи)', () => {
|
||
const t = [
|
||
JSON.stringify({ message: { role: 'user', content: 'настоящий' } }),
|
||
JSON.stringify({ message: { role: 'assistant', content: [{ type: 'text', text: 'ответ' }] } }),
|
||
JSON.stringify({ isMeta: true, message: { role: 'user', content: 'Stop hook feedback: x' } }),
|
||
JSON.stringify({ message: { role: 'assistant', content: [{ type: 'text', text: 'продолжение' }] } }),
|
||
].join('\n');
|
||
const ex = parseLastExchange(t);
|
||
expect(ex.user).toBe('Stop hook feedback: x'); // выбор сообщения прежний (последнее текстовое)
|
||
expect(ex.userIsMeta).toBe(true); // но помечено как служебное
|
||
});
|
||
it('реальный промпт — userIsMeta false', () => {
|
||
const t = [JSON.stringify({ message: { role: 'user', content: 'привет' } }),
|
||
JSON.stringify({ message: { role: 'assistant', content: 'ок' } })].join('\n');
|
||
expect(parseLastExchange(t).userIsMeta).toBe(false);
|
||
});
|
||
it('без совпадающего id результат не привязывается — старая форма {tool,input} цела', () => {
|
||
const t = [
|
||
JSON.stringify({ message: { role: 'user', content: 'в' } }),
|
||
JSON.stringify({ message: { role: 'assistant', content: [
|
||
{ type: 'tool_use', id: 'tu_3', name: 'Read', input: { f: 'z' } }] } }),
|
||
].join('\n');
|
||
const ex = parseLastExchange(t);
|
||
expect(ex.actions).toEqual([{ tool: 'Read', input: '{"f":"z"}' }]);
|
||
});
|
||
});
|