feat(observer): differentiate error events by tool + summary
Closes brain-retro 2026-05-20 #7 — each tool_result.is_error now emits { kind:'error', tool:<name>, summary:<first 80 chars> }. Allows aggregation by tool (Bash/Edit/Read) + cause prefix (ENOENT/timeout/ 'String to replace not found'). Required updating existing 'emits error events for tool_result with is_error' test assertion (old shape had bare 'message' field). 4 new vitest tests + 1 existing relaxed, 286/286 GREEN. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -172,7 +172,10 @@ describe('parseTranscript', () => {
|
||||
assistantTurn([{ type: 'tool_use', id: 't1', name: 'Bash', input: {} }], '2026-05-19T10:01:00Z'),
|
||||
toolResult('t1', true, '2026-05-19T10:02:00Z'),
|
||||
]);
|
||||
expect(parseTranscript(t).events.filter((e) => e.kind === 'error')).toHaveLength(1);
|
||||
const errs = parseTranscript(t).events.filter((e) => e.kind === 'error');
|
||||
expect(errs).toHaveLength(1);
|
||||
expect(errs[0].tool).toBe('Bash');
|
||||
expect(errs[0].summary).toBe('r');
|
||||
});
|
||||
|
||||
it('skips broken JSONL lines without throwing', () => {
|
||||
@@ -1180,3 +1183,68 @@ describe('parseTranscript — heuristic primary_rationale (Task 6)', () => {
|
||||
expect(ep.primary_rationale.boundaries_applied).toContain('ADR-011');
|
||||
});
|
||||
});
|
||||
|
||||
describe('error event differentiation (Task 7)', () => {
|
||||
it('attributes error to the failing tool and captures summary', () => {
|
||||
const transcript = [
|
||||
JSON.stringify({ sessionId: 's' }),
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: 'делай' }, uuid: 'u1', timestamp: '2026-05-20T00:00:00Z' }),
|
||||
JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: [
|
||||
{ type: 'tool_use', id: 'tu1', name: 'Bash', input: {} }
|
||||
] }, uuid: 'u2', timestamp: '2026-05-20T00:00:01Z' }),
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: [
|
||||
{ type: 'tool_result', tool_use_id: 'tu1', is_error: true, content: 'ENOENT: no such file or directory' }
|
||||
] }, uuid: 'u3', timestamp: '2026-05-20T00:00:02Z' }),
|
||||
].join('\n');
|
||||
const ep = parseTranscript(transcript);
|
||||
const errorEvents = ep.events.filter((e) => e.kind === 'error');
|
||||
expect(errorEvents).toHaveLength(1);
|
||||
expect(errorEvents[0].tool).toBe('Bash');
|
||||
expect(errorEvents[0].summary).toContain('ENOENT');
|
||||
});
|
||||
it('attributes error to "unknown" when tool_use_id not seen', () => {
|
||||
const transcript = [
|
||||
JSON.stringify({ sessionId: 's' }),
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: 'делай' }, uuid: 'u1', timestamp: '2026-05-20T00:00:00Z' }),
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: [
|
||||
{ type: 'tool_result', tool_use_id: 'orphan', is_error: true, content: 'mystery error' }
|
||||
] }, uuid: 'u2', timestamp: '2026-05-20T00:00:02Z' }),
|
||||
].join('\n');
|
||||
const ep = parseTranscript(transcript);
|
||||
const errs = ep.events.filter((e) => e.kind === 'error');
|
||||
expect(errs[0].tool).toBe('unknown');
|
||||
expect(errs[0].summary).toBe('mystery error');
|
||||
});
|
||||
it('truncates summary at 80 chars', () => {
|
||||
const longText = 'a'.repeat(200);
|
||||
const transcript = [
|
||||
JSON.stringify({ sessionId: 's' }),
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: 'делай' }, uuid: 'u1', timestamp: '2026-05-20T00:00:00Z' }),
|
||||
JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: [
|
||||
{ type: 'tool_use', id: 'tu1', name: 'Read', input: {} }
|
||||
] }, uuid: 'u2', timestamp: '2026-05-20T00:00:01Z' }),
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: [
|
||||
{ type: 'tool_result', tool_use_id: 'tu1', is_error: true, content: longText }
|
||||
] }, uuid: 'u3', timestamp: '2026-05-20T00:00:02Z' }),
|
||||
].join('\n');
|
||||
const ep = parseTranscript(transcript);
|
||||
const e = ep.events.find((x) => x.kind === 'error');
|
||||
expect(e.summary.length).toBe(80);
|
||||
});
|
||||
it('handles structured content array (text blocks)', () => {
|
||||
const transcript = [
|
||||
JSON.stringify({ sessionId: 's' }),
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: 'делай' }, uuid: 'u1', timestamp: '2026-05-20T00:00:00Z' }),
|
||||
JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: [
|
||||
{ type: 'tool_use', id: 'tu1', name: 'Edit', input: {} }
|
||||
] }, uuid: 'u2', timestamp: '2026-05-20T00:00:01Z' }),
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: [
|
||||
{ type: 'tool_result', tool_use_id: 'tu1', is_error: true, content: [{ type: 'text', text: 'String to replace not found' }] }
|
||||
] }, uuid: 'u3', timestamp: '2026-05-20T00:00:02Z' }),
|
||||
].join('\n');
|
||||
const ep = parseTranscript(transcript);
|
||||
const e = ep.events.find((x) => x.kind === 'error');
|
||||
expect(e.tool).toBe('Edit');
|
||||
expect(e.summary).toContain('String to replace');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user