@shipfox/api-logs 10.0.0 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +23 -0
- package/dist/core/append-logs.d.ts.map +1 -1
- package/dist/core/append-logs.js +13 -5
- package/dist/core/append-logs.js.map +1 -1
- package/dist/core/close-stream.d.ts.map +1 -1
- package/dist/core/close-stream.js +16 -1
- package/dist/core/close-stream.js.map +1 -1
- package/dist/core/entities/attempt-stream.d.ts +2 -1
- package/dist/core/entities/attempt-stream.d.ts.map +1 -1
- package/dist/core/entities/attempt-stream.js.map +1 -1
- package/dist/core/session/claude/rows.d.ts +15 -3
- package/dist/core/session/claude/rows.d.ts.map +1 -1
- package/dist/core/session/claude/rows.js +221 -15
- package/dist/core/session/claude/rows.js.map +1 -1
- package/dist/core/session/claude-parser.d.ts +4 -2
- package/dist/core/session/claude-parser.d.ts.map +1 -1
- package/dist/core/session/claude-parser.js +56 -10
- package/dist/core/session/claude-parser.js.map +1 -1
- package/dist/db/db.d.ts +113 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/schema/attempt-streams.d.ts +113 -0
- package/dist/db/schema/attempt-streams.d.ts.map +1 -1
- package/dist/db/schema/attempt-streams.js +2 -0
- package/dist/db/schema/attempt-streams.js.map +1 -1
- package/dist/db/streams.d.ts +3 -1
- package/dist/db/streams.d.ts.map +1 -1
- package/dist/db/streams.js +6 -1
- package/dist/db/streams.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0002_wise_dorian_gray.sql +1 -0
- package/drizzle/meta/0002_snapshot.json +561 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +4 -4
- package/src/core/append-logs.test.ts +57 -0
- package/src/core/append-logs.ts +15 -3
- package/src/core/close-stream.ts +17 -1
- package/src/core/entities/attempt-stream.ts +2 -1
- package/src/core/session/claude/rows.ts +295 -13
- package/src/core/session/claude-parser.test.ts +620 -11
- package/src/core/session/claude-parser.ts +59 -10
- package/src/db/schema/attempt-streams.ts +3 -1
- package/src/db/streams.ts +10 -2
- package/test/fixtures/claude-session/agent-step.jsonl +12 -0
- package/test/fixtures/claude-session/sdk-system-subtypes.json +32 -0
- package/test/fixtures/claude-session.ts +45 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,12 +1,279 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
readClaudeSessionFixture,
|
|
3
|
+
readClaudeSystemSubtypeFixture,
|
|
4
|
+
readInstalledClaudeSdkVersion,
|
|
5
|
+
readInstalledClaudeSystemSubtypes,
|
|
6
|
+
} from '#test/fixtures/claude-session.js';
|
|
7
|
+
import {
|
|
8
|
+
flushPendingToolRows,
|
|
9
|
+
PURE_PROGRESS_CLAUDE_SYSTEM_SUBTYPES,
|
|
10
|
+
SYSTEM_EVENT_MAPPINGS,
|
|
11
|
+
} from './claude/rows.js';
|
|
2
12
|
import {createClaudeParseContext, parseClaudeSessionRecord} from './claude-parser.js';
|
|
3
13
|
|
|
4
14
|
const record = (data: unknown, ts = 1) => ({
|
|
5
15
|
ts,
|
|
6
16
|
data: typeof data === 'string' ? data : JSON.stringify(data),
|
|
7
17
|
});
|
|
18
|
+
const meta = (label: string, value: string, inline?: boolean) =>
|
|
19
|
+
inline == null ? {label, value} : {label, value, inline};
|
|
20
|
+
|
|
21
|
+
const namedSystemEvents = [
|
|
22
|
+
{
|
|
23
|
+
subtype: 'permission_denied',
|
|
24
|
+
data: {tool_name: 'mcp__shipfox_outputs__set_output', decision_reason: 'Permission mode'},
|
|
25
|
+
expected: {
|
|
26
|
+
label: 'Permission denied',
|
|
27
|
+
detail: 'mcp__shipfox_outputs__set_output',
|
|
28
|
+
meta: [meta('reason', 'Permission mode')],
|
|
29
|
+
tone: 'error',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
subtype: 'api_retry',
|
|
34
|
+
data: {attempt: 2, max_retries: 3, error_status: 429, retry_delay_ms: 1_500},
|
|
35
|
+
expected: {
|
|
36
|
+
label: 'API retry',
|
|
37
|
+
detail: 'attempt 2/3',
|
|
38
|
+
meta: [meta('status', '429'), meta('retry delay', '1,500 ms')],
|
|
39
|
+
tone: 'warning',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
subtype: 'informational',
|
|
44
|
+
data: {level: 'warning', content: 'The hook blocked continuation.'},
|
|
45
|
+
expected: {
|
|
46
|
+
label: 'Warning',
|
|
47
|
+
detail: 'The hook blocked continuation.',
|
|
48
|
+
meta: [],
|
|
49
|
+
tone: 'warning',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
subtype: 'compact_boundary',
|
|
54
|
+
data: {
|
|
55
|
+
compact_metadata: {trigger: 'auto', pre_tokens: 12_000, post_tokens: 4_000, duration_ms: 250},
|
|
56
|
+
},
|
|
57
|
+
expected: {
|
|
58
|
+
label: 'Context compacted',
|
|
59
|
+
detail: null,
|
|
60
|
+
meta: [
|
|
61
|
+
meta('trigger', 'auto'),
|
|
62
|
+
meta('tokens before', '12K tokens'),
|
|
63
|
+
meta('tokens after', '4,000 tokens'),
|
|
64
|
+
meta('duration', '250 ms'),
|
|
65
|
+
],
|
|
66
|
+
tone: 'default',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
subtype: 'hook_response',
|
|
71
|
+
data: {hook_name: 'PreToolUse', outcome: 'success', output: 'Hook completed', exit_code: 0},
|
|
72
|
+
expected: {
|
|
73
|
+
label: 'Hook completed',
|
|
74
|
+
detail: 'Hook completed',
|
|
75
|
+
meta: [meta('hook', 'PreToolUse'), meta('exit code', '0')],
|
|
76
|
+
tone: 'default',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
subtype: 'hook_response',
|
|
81
|
+
data: {hook_name: 'PreToolUse', outcome: 'error', stderr: 'command not found', exit_code: 127},
|
|
82
|
+
expected: {
|
|
83
|
+
label: 'Hook failed',
|
|
84
|
+
detail: 'command not found',
|
|
85
|
+
meta: [meta('hook', 'PreToolUse'), meta('exit code', '127')],
|
|
86
|
+
tone: 'error',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
subtype: 'hook_response',
|
|
91
|
+
data: {hook_name: 'PostToolUse', outcome: 'cancelled'},
|
|
92
|
+
expected: {
|
|
93
|
+
label: 'Hook cancelled',
|
|
94
|
+
detail: null,
|
|
95
|
+
meta: [meta('hook', 'PostToolUse')],
|
|
96
|
+
tone: 'warning',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
subtype: 'model_refusal_fallback',
|
|
101
|
+
data: {},
|
|
102
|
+
expected: {
|
|
103
|
+
label: 'Model refused, fell back',
|
|
104
|
+
detail: null,
|
|
105
|
+
meta: [],
|
|
106
|
+
tone: 'warning',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
subtype: 'model_refusal_no_fallback',
|
|
111
|
+
data: {},
|
|
112
|
+
expected: {
|
|
113
|
+
label: 'Model refused',
|
|
114
|
+
detail: null,
|
|
115
|
+
meta: [],
|
|
116
|
+
tone: 'error',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
subtype: 'mirror_error',
|
|
121
|
+
data: {error: 'Session store unavailable'},
|
|
122
|
+
expected: {
|
|
123
|
+
label: 'Session mirror failed',
|
|
124
|
+
detail: 'Session store unavailable',
|
|
125
|
+
meta: [],
|
|
126
|
+
tone: 'error',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
subtype: 'worker_shutting_down',
|
|
131
|
+
data: {},
|
|
132
|
+
expected: {
|
|
133
|
+
label: 'Worker shutting down',
|
|
134
|
+
detail: null,
|
|
135
|
+
meta: [],
|
|
136
|
+
tone: 'warning',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
subtype: 'elicitation_complete',
|
|
141
|
+
data: {},
|
|
142
|
+
expected: {
|
|
143
|
+
label: 'Elicitation complete',
|
|
144
|
+
detail: null,
|
|
145
|
+
meta: [],
|
|
146
|
+
tone: 'default',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
subtype: 'notification',
|
|
151
|
+
data: {},
|
|
152
|
+
expected: {
|
|
153
|
+
label: 'Notification',
|
|
154
|
+
detail: null,
|
|
155
|
+
meta: [],
|
|
156
|
+
tone: 'default',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
subtype: 'task_started',
|
|
161
|
+
data: {},
|
|
162
|
+
expected: {
|
|
163
|
+
label: 'Task started',
|
|
164
|
+
detail: null,
|
|
165
|
+
meta: [],
|
|
166
|
+
tone: 'default',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
subtype: 'task_updated',
|
|
171
|
+
data: {},
|
|
172
|
+
expected: {
|
|
173
|
+
label: 'Task updated',
|
|
174
|
+
detail: null,
|
|
175
|
+
meta: [],
|
|
176
|
+
tone: 'default',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
subtype: 'task_notification',
|
|
181
|
+
data: {},
|
|
182
|
+
expected: {
|
|
183
|
+
label: 'Task notification',
|
|
184
|
+
detail: null,
|
|
185
|
+
meta: [],
|
|
186
|
+
tone: 'default',
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
] as const;
|
|
8
190
|
|
|
9
191
|
describe('parseClaudeSessionRecord', () => {
|
|
192
|
+
it('covers every system subtype in the installed Claude SDK union', () => {
|
|
193
|
+
const fixture = readClaudeSystemSubtypeFixture();
|
|
194
|
+
const installedVersion = readInstalledClaudeSdkVersion();
|
|
195
|
+
const installedSubtypes = readInstalledClaudeSystemSubtypes();
|
|
196
|
+
const parserSubtypes = [
|
|
197
|
+
'init',
|
|
198
|
+
...PURE_PROGRESS_CLAUDE_SYSTEM_SUBTYPES,
|
|
199
|
+
...Object.keys(SYSTEM_EVENT_MAPPINGS),
|
|
200
|
+
].sort();
|
|
201
|
+
|
|
202
|
+
expect(
|
|
203
|
+
fixture.version,
|
|
204
|
+
`The checked-in SDK subtype fixture targets ${fixture.version}, but the installed SDK is ${installedVersion}`,
|
|
205
|
+
).toBe(installedVersion);
|
|
206
|
+
expect(
|
|
207
|
+
fixture.subtypes,
|
|
208
|
+
`The checked-in SDK subtype fixture is stale; update it for: ${installedSubtypes
|
|
209
|
+
.filter((subtype) => !fixture.subtypes.includes(subtype))
|
|
210
|
+
.join(', ')}`,
|
|
211
|
+
).toEqual(installedSubtypes);
|
|
212
|
+
expect(
|
|
213
|
+
parserSubtypes,
|
|
214
|
+
`The parser does not classify these installed SDK system subtypes: ${installedSubtypes
|
|
215
|
+
.filter((subtype) => !parserSubtypes.includes(subtype))
|
|
216
|
+
.join(', ')}`,
|
|
217
|
+
).toEqual(fixture.subtypes);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('parses a captured Claude agent step with tools, signal events, and a re-prompt', () => {
|
|
221
|
+
const context = createClaudeParseContext();
|
|
222
|
+
const records = readClaudeSessionFixture('agent-step.jsonl');
|
|
223
|
+
let sawResult = false;
|
|
224
|
+
const rows = records.flatMap((sessionRecord) => {
|
|
225
|
+
const message = JSON.parse(sessionRecord.data) as {type?: string};
|
|
226
|
+
const isFinalResult = message.type !== 'result' || sawResult;
|
|
227
|
+
if (message.type === 'result') sawResult = true;
|
|
228
|
+
return parseClaudeSessionRecord(sessionRecord, context, isFinalResult);
|
|
229
|
+
});
|
|
230
|
+
const allRows = [...rows, ...flushPendingToolRows(context)];
|
|
231
|
+
const lifecycleRows = allRows.filter((row) => row.kind === 'lifecycle');
|
|
232
|
+
const toolCall = allRows.find((row) => row.kind === 'tool-call');
|
|
233
|
+
const toolResult = allRows.find((row) => row.kind === 'tool-result');
|
|
234
|
+
|
|
235
|
+
expect(lifecycleRows).toEqual(
|
|
236
|
+
expect.arrayContaining([
|
|
237
|
+
expect.objectContaining({label: 'Session started'}),
|
|
238
|
+
expect.objectContaining({
|
|
239
|
+
label: 'Permission denied',
|
|
240
|
+
detail: 'mcp__shipfox_outputs__set_output',
|
|
241
|
+
tone: 'error',
|
|
242
|
+
}),
|
|
243
|
+
expect.objectContaining({label: 'API retry', detail: 'attempt 2/3', tone: 'warning'}),
|
|
244
|
+
expect.objectContaining({label: 'Hook completed', detail: 'Hook completed'}),
|
|
245
|
+
expect.objectContaining({label: 'Context compacted'}),
|
|
246
|
+
expect.objectContaining({label: 'Turn 1 completed'}),
|
|
247
|
+
expect.objectContaining({label: 'Turn 2 started'}),
|
|
248
|
+
expect.objectContaining({label: 'Session completed'}),
|
|
249
|
+
]),
|
|
250
|
+
);
|
|
251
|
+
expect(allRows).toEqual(
|
|
252
|
+
expect.arrayContaining([
|
|
253
|
+
expect.objectContaining({
|
|
254
|
+
kind: 'message',
|
|
255
|
+
role: 'platform',
|
|
256
|
+
text: expect.stringContaining('required workflow outputs'),
|
|
257
|
+
}),
|
|
258
|
+
]),
|
|
259
|
+
);
|
|
260
|
+
expect(toolCall).toEqual(
|
|
261
|
+
expect.objectContaining({kind: 'tool-call', id: 'toolu-1', name: 'Read'}),
|
|
262
|
+
);
|
|
263
|
+
expect(toolResult).toEqual(
|
|
264
|
+
expect.objectContaining({
|
|
265
|
+
kind: 'tool-result',
|
|
266
|
+
toolCallId: 'toolu-1',
|
|
267
|
+
toolName: expect.any(String),
|
|
268
|
+
output: expect.stringContaining('# Shipfox'),
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
if (toolCall?.kind !== 'tool-call' || toolResult?.kind !== 'tool-result') {
|
|
272
|
+
throw new Error('The captured tool call and result must both be parsed');
|
|
273
|
+
}
|
|
274
|
+
expect(toolResult.toolCallId).toBe(toolCall.id);
|
|
275
|
+
});
|
|
276
|
+
|
|
10
277
|
it('returns a raw row for malformed JSON', () => {
|
|
11
278
|
const rows = parseClaudeSessionRecord(record('{not json'));
|
|
12
279
|
|
|
@@ -44,8 +311,9 @@ describe('parseClaudeSessionRecord', () => {
|
|
|
44
311
|
kind: 'lifecycle',
|
|
45
312
|
timestamp: 1,
|
|
46
313
|
label: 'Session started',
|
|
47
|
-
detail:
|
|
314
|
+
detail: null,
|
|
48
315
|
meta: [
|
|
316
|
+
{label: 'session', value: 'session-1'},
|
|
49
317
|
{label: 'cwd', value: '/workspace', inline: false},
|
|
50
318
|
{label: 'model', value: 'claude-opus-4-8'},
|
|
51
319
|
],
|
|
@@ -63,6 +331,183 @@ describe('parseClaudeSessionRecord', () => {
|
|
|
63
331
|
expect(rows).toEqual([]);
|
|
64
332
|
});
|
|
65
333
|
|
|
334
|
+
it.each([
|
|
335
|
+
['tool_progress', {type: 'tool_progress'}],
|
|
336
|
+
['prompt_suggestion', {type: 'prompt_suggestion'}],
|
|
337
|
+
[
|
|
338
|
+
'tool_use_summary',
|
|
339
|
+
{
|
|
340
|
+
type: 'tool_use_summary',
|
|
341
|
+
tool_use_id: 'tool-1',
|
|
342
|
+
summary: 'The tool completed successfully.',
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
] as const)('does not store companion message type %s as a row', (_type, message) => {
|
|
346
|
+
const rows = parseClaudeSessionRecord(record(message));
|
|
347
|
+
|
|
348
|
+
expect(rows).toEqual([]);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('folds a tool-use summary into the last matching tool-call row', () => {
|
|
352
|
+
const context = createClaudeParseContext();
|
|
353
|
+
expect(
|
|
354
|
+
parseClaudeSessionRecord(
|
|
355
|
+
record({
|
|
356
|
+
type: 'assistant',
|
|
357
|
+
message: {
|
|
358
|
+
role: 'assistant',
|
|
359
|
+
content: [
|
|
360
|
+
{type: 'tool_use', id: 'tool-1', name: 'Read', input: {file_path: 'src/a.ts'}},
|
|
361
|
+
{type: 'tool_use', id: 'tool-2', name: 'Read', input: {file_path: 'src/b.ts'}},
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
}),
|
|
365
|
+
context,
|
|
366
|
+
),
|
|
367
|
+
).toEqual([]);
|
|
368
|
+
|
|
369
|
+
expect(
|
|
370
|
+
parseClaudeSessionRecord(
|
|
371
|
+
record({
|
|
372
|
+
type: 'tool_use_summary',
|
|
373
|
+
summary: 'Read both source files.',
|
|
374
|
+
preceding_tool_use_ids: ['tool-1', 'tool-2'],
|
|
375
|
+
}),
|
|
376
|
+
context,
|
|
377
|
+
),
|
|
378
|
+
).toEqual([
|
|
379
|
+
{
|
|
380
|
+
kind: 'tool-call',
|
|
381
|
+
timestamp: 1,
|
|
382
|
+
id: 'tool-1',
|
|
383
|
+
name: 'Read',
|
|
384
|
+
input: '{\n "file_path": "src/a.ts"\n}',
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
kind: 'tool-call',
|
|
388
|
+
timestamp: 1,
|
|
389
|
+
id: 'tool-2',
|
|
390
|
+
name: 'Read',
|
|
391
|
+
input: '{\n "file_path": "src/b.ts"\n}',
|
|
392
|
+
summary: 'Read both source files.',
|
|
393
|
+
},
|
|
394
|
+
]);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('supports the legacy single tool-use id summary shape', () => {
|
|
398
|
+
const context = createClaudeParseContext();
|
|
399
|
+
expect(
|
|
400
|
+
parseClaudeSessionRecord(
|
|
401
|
+
record({
|
|
402
|
+
type: 'assistant',
|
|
403
|
+
message: {
|
|
404
|
+
role: 'assistant',
|
|
405
|
+
content: [
|
|
406
|
+
{type: 'tool_use', id: 'tool-1', name: 'Read', input: {file_path: 'src/a.ts'}},
|
|
407
|
+
],
|
|
408
|
+
},
|
|
409
|
+
}),
|
|
410
|
+
context,
|
|
411
|
+
),
|
|
412
|
+
).toEqual([]);
|
|
413
|
+
|
|
414
|
+
expect(
|
|
415
|
+
parseClaudeSessionRecord(
|
|
416
|
+
record({
|
|
417
|
+
type: 'tool_use_summary',
|
|
418
|
+
tool_use_id: 'tool-1',
|
|
419
|
+
summary: 'Read the source file.',
|
|
420
|
+
}),
|
|
421
|
+
context,
|
|
422
|
+
),
|
|
423
|
+
).toEqual([expect.objectContaining({summary: 'Read the source file.'})]);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it.each([
|
|
427
|
+
[
|
|
428
|
+
'signed in',
|
|
429
|
+
{type: 'auth_status', isAuthenticating: false, output: ['Signed in']},
|
|
430
|
+
{label: 'Authentication complete', detail: 'Signed in', tone: 'default'},
|
|
431
|
+
],
|
|
432
|
+
[
|
|
433
|
+
'authenticating',
|
|
434
|
+
{type: 'auth_status', isAuthenticating: true, output: []},
|
|
435
|
+
{label: 'Authenticating', detail: null, tone: 'default'},
|
|
436
|
+
],
|
|
437
|
+
[
|
|
438
|
+
'failed',
|
|
439
|
+
{type: 'auth_status', isAuthenticating: false, output: [], error: 'Invalid credentials'},
|
|
440
|
+
{label: 'Authentication failed', detail: 'Invalid credentials', tone: 'error'},
|
|
441
|
+
],
|
|
442
|
+
] as const)('maps auth status messages to lifecycle rows (%s)', (_name, message, expected) => {
|
|
443
|
+
const rows = parseClaudeSessionRecord(record(message));
|
|
444
|
+
|
|
445
|
+
expect(rows).toEqual([
|
|
446
|
+
{kind: 'lifecycle', timestamp: 1, meta: [], terminalFailure: false, ...expected},
|
|
447
|
+
]);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it.each([
|
|
451
|
+
[
|
|
452
|
+
'warning',
|
|
453
|
+
{status: 'allowed_warning', rateLimitType: 'five_hour', utilization: 0.42},
|
|
454
|
+
{
|
|
455
|
+
label: 'Rate limit warning',
|
|
456
|
+
detail: 'Five hour',
|
|
457
|
+
meta: [{label: 'utilization', value: '42%'}],
|
|
458
|
+
tone: 'warning',
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
[
|
|
462
|
+
'rejected',
|
|
463
|
+
{status: 'rejected', rateLimitType: 'five_hour', utilization: 1},
|
|
464
|
+
{
|
|
465
|
+
label: 'Rate limit exceeded',
|
|
466
|
+
detail: 'Five hour',
|
|
467
|
+
meta: [{label: 'utilization', value: '100%'}],
|
|
468
|
+
tone: 'error',
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
[
|
|
472
|
+
'allowed',
|
|
473
|
+
{status: 'allowed'},
|
|
474
|
+
{label: 'Rate limit available', detail: null, meta: [], tone: 'default'},
|
|
475
|
+
],
|
|
476
|
+
[
|
|
477
|
+
'unrecognized',
|
|
478
|
+
{status: 'some_future_status'},
|
|
479
|
+
{
|
|
480
|
+
label: 'Rate limit updated',
|
|
481
|
+
detail: null,
|
|
482
|
+
meta: [{label: 'status', value: 'some_future_status'}],
|
|
483
|
+
tone: 'warning',
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
] as const)('maps rate-limit events to lifecycle rows (%s)', (_name, rateLimitInfo, expected) => {
|
|
487
|
+
const rows = parseClaudeSessionRecord(
|
|
488
|
+
record({type: 'rate_limit_event', rate_limit_info: rateLimitInfo}),
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
expect(rows).toEqual([{kind: 'lifecycle', timestamp: 1, terminalFailure: false, ...expected}]);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it.each(namedSystemEvents)('names and tones $subtype system events', ({
|
|
495
|
+
subtype,
|
|
496
|
+
data,
|
|
497
|
+
expected,
|
|
498
|
+
}) => {
|
|
499
|
+
const rows = parseClaudeSessionRecord(record({type: 'system', subtype, ...data}));
|
|
500
|
+
|
|
501
|
+
expect(rows).toEqual([
|
|
502
|
+
{
|
|
503
|
+
kind: 'lifecycle',
|
|
504
|
+
timestamp: 1,
|
|
505
|
+
...expected,
|
|
506
|
+
terminalFailure: false,
|
|
507
|
+
},
|
|
508
|
+
]);
|
|
509
|
+
});
|
|
510
|
+
|
|
66
511
|
it('reports repeated init and result records as turns within one session', () => {
|
|
67
512
|
const context = createClaudeParseContext();
|
|
68
513
|
const rows = [
|
|
@@ -90,8 +535,8 @@ describe('parseClaudeSessionRecord', () => {
|
|
|
90
535
|
kind: 'lifecycle',
|
|
91
536
|
timestamp: 1,
|
|
92
537
|
label: 'Session started',
|
|
93
|
-
detail:
|
|
94
|
-
meta: [],
|
|
538
|
+
detail: null,
|
|
539
|
+
meta: [meta('session', 'session-1')],
|
|
95
540
|
tone: 'default',
|
|
96
541
|
terminalFailure: false,
|
|
97
542
|
},
|
|
@@ -108,7 +553,7 @@ describe('parseClaudeSessionRecord', () => {
|
|
|
108
553
|
kind: 'lifecycle',
|
|
109
554
|
timestamp: 1,
|
|
110
555
|
label: 'Turn 2 started',
|
|
111
|
-
detail:
|
|
556
|
+
detail: null,
|
|
112
557
|
meta: [{label: 'turn', value: '2'}],
|
|
113
558
|
tone: 'default',
|
|
114
559
|
terminalFailure: false,
|
|
@@ -125,7 +570,7 @@ describe('parseClaudeSessionRecord', () => {
|
|
|
125
570
|
]);
|
|
126
571
|
});
|
|
127
572
|
|
|
128
|
-
it('
|
|
573
|
+
it('qualifies unmapped system events instead of using a bare generic label', () => {
|
|
129
574
|
const rows = parseClaudeSessionRecord(
|
|
130
575
|
record({type: 'system', subtype: 'custom_event', session_id: 'session-1'}),
|
|
131
576
|
);
|
|
@@ -134,8 +579,8 @@ describe('parseClaudeSessionRecord', () => {
|
|
|
134
579
|
{
|
|
135
580
|
kind: 'lifecycle',
|
|
136
581
|
timestamp: 1,
|
|
137
|
-
label: 'Session event',
|
|
138
|
-
detail:
|
|
582
|
+
label: 'Session event (custom_event)',
|
|
583
|
+
detail: null,
|
|
139
584
|
meta: [],
|
|
140
585
|
tone: 'default',
|
|
141
586
|
terminalFailure: false,
|
|
@@ -172,6 +617,7 @@ describe('parseClaudeSessionRecord', () => {
|
|
|
172
617
|
});
|
|
173
618
|
|
|
174
619
|
it('expands assistant text, thinking, and tool-use blocks in order', () => {
|
|
620
|
+
const context = createClaudeParseContext();
|
|
175
621
|
const rows = parseClaudeSessionRecord(
|
|
176
622
|
record({
|
|
177
623
|
type: 'assistant',
|
|
@@ -184,9 +630,10 @@ describe('parseClaudeSessionRecord', () => {
|
|
|
184
630
|
],
|
|
185
631
|
},
|
|
186
632
|
}),
|
|
633
|
+
context,
|
|
187
634
|
);
|
|
188
635
|
|
|
189
|
-
expect(rows).toEqual([
|
|
636
|
+
expect([...rows, ...flushPendingToolRows(context)]).toEqual([
|
|
190
637
|
{
|
|
191
638
|
kind: 'message',
|
|
192
639
|
timestamp: 1,
|
|
@@ -207,6 +654,168 @@ describe('parseClaudeSessionRecord', () => {
|
|
|
207
654
|
]);
|
|
208
655
|
});
|
|
209
656
|
|
|
657
|
+
it('keeps assistant content after an identified tool call in order', () => {
|
|
658
|
+
const context = createClaudeParseContext();
|
|
659
|
+
const rows = parseClaudeSessionRecord(
|
|
660
|
+
record({
|
|
661
|
+
type: 'assistant',
|
|
662
|
+
message: {
|
|
663
|
+
role: 'assistant',
|
|
664
|
+
content: [
|
|
665
|
+
{type: 'text', text: 'Before the tool.'},
|
|
666
|
+
{type: 'tool_use', id: 'tool-1', name: 'Read', input: {file_path: 'src/a.ts'}},
|
|
667
|
+
{type: 'text', text: 'After the tool.'},
|
|
668
|
+
],
|
|
669
|
+
},
|
|
670
|
+
}),
|
|
671
|
+
context,
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
expect(rows).toEqual([
|
|
675
|
+
{
|
|
676
|
+
kind: 'message',
|
|
677
|
+
timestamp: 1,
|
|
678
|
+
role: 'assistant',
|
|
679
|
+
label: 'assistant',
|
|
680
|
+
meta: [],
|
|
681
|
+
text: 'Before the tool.',
|
|
682
|
+
terminalFailure: false,
|
|
683
|
+
},
|
|
684
|
+
]);
|
|
685
|
+
expect(flushPendingToolRows(context)).toEqual([
|
|
686
|
+
{
|
|
687
|
+
kind: 'tool-call',
|
|
688
|
+
timestamp: 1,
|
|
689
|
+
id: 'tool-1',
|
|
690
|
+
name: 'Read',
|
|
691
|
+
input: '{\n "file_path": "src/a.ts"\n}',
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
kind: 'message',
|
|
695
|
+
timestamp: 1,
|
|
696
|
+
role: 'assistant',
|
|
697
|
+
label: 'assistant',
|
|
698
|
+
meta: [],
|
|
699
|
+
text: 'After the tool.',
|
|
700
|
+
terminalFailure: false,
|
|
701
|
+
},
|
|
702
|
+
]);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('keeps mixed user tool-result and text rows in order', () => {
|
|
706
|
+
const context = createClaudeParseContext();
|
|
707
|
+
expect(
|
|
708
|
+
parseClaudeSessionRecord(
|
|
709
|
+
record({
|
|
710
|
+
type: 'assistant',
|
|
711
|
+
message: {
|
|
712
|
+
role: 'assistant',
|
|
713
|
+
content: [
|
|
714
|
+
{type: 'tool_use', id: 'tool-1', name: 'Read', input: {file_path: 'src/a.ts'}},
|
|
715
|
+
],
|
|
716
|
+
},
|
|
717
|
+
}),
|
|
718
|
+
context,
|
|
719
|
+
),
|
|
720
|
+
).toEqual([]);
|
|
721
|
+
|
|
722
|
+
expect(
|
|
723
|
+
parseClaudeSessionRecord(
|
|
724
|
+
record({
|
|
725
|
+
type: 'user',
|
|
726
|
+
message: {
|
|
727
|
+
role: 'user',
|
|
728
|
+
content: [
|
|
729
|
+
{type: 'tool_result', tool_use_id: 'tool-1', content: 'file contents'},
|
|
730
|
+
{type: 'text', text: 'The file was read.'},
|
|
731
|
+
],
|
|
732
|
+
},
|
|
733
|
+
}),
|
|
734
|
+
context,
|
|
735
|
+
),
|
|
736
|
+
).toEqual([]);
|
|
737
|
+
expect(flushPendingToolRows(context)).toEqual([
|
|
738
|
+
{
|
|
739
|
+
kind: 'tool-call',
|
|
740
|
+
timestamp: 1,
|
|
741
|
+
id: 'tool-1',
|
|
742
|
+
name: 'Read',
|
|
743
|
+
input: '{\n "file_path": "src/a.ts"\n}',
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
kind: 'tool-result',
|
|
747
|
+
timestamp: 1,
|
|
748
|
+
toolCallId: 'tool-1',
|
|
749
|
+
toolName: expect.any(String),
|
|
750
|
+
output: 'file contents',
|
|
751
|
+
isError: false,
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
kind: 'message',
|
|
755
|
+
timestamp: 1,
|
|
756
|
+
role: 'user',
|
|
757
|
+
label: 'user',
|
|
758
|
+
meta: [],
|
|
759
|
+
text: 'The file was read.',
|
|
760
|
+
terminalFailure: false,
|
|
761
|
+
},
|
|
762
|
+
]);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('keeps lifecycle rows behind a pending tool call until its summary arrives', () => {
|
|
766
|
+
const context = createClaudeParseContext();
|
|
767
|
+
expect(
|
|
768
|
+
parseClaudeSessionRecord(
|
|
769
|
+
record({
|
|
770
|
+
type: 'assistant',
|
|
771
|
+
message: {
|
|
772
|
+
role: 'assistant',
|
|
773
|
+
content: [
|
|
774
|
+
{type: 'tool_use', id: 'tool-1', name: 'Read', input: {file_path: 'src/a.ts'}},
|
|
775
|
+
],
|
|
776
|
+
},
|
|
777
|
+
}),
|
|
778
|
+
context,
|
|
779
|
+
),
|
|
780
|
+
).toEqual([]);
|
|
781
|
+
|
|
782
|
+
expect(
|
|
783
|
+
parseClaudeSessionRecord(
|
|
784
|
+
record({type: 'auth_status', isAuthenticating: true, output: []}),
|
|
785
|
+
context,
|
|
786
|
+
),
|
|
787
|
+
).toEqual([]);
|
|
788
|
+
|
|
789
|
+
expect(
|
|
790
|
+
parseClaudeSessionRecord(
|
|
791
|
+
record({
|
|
792
|
+
type: 'tool_use_summary',
|
|
793
|
+
preceding_tool_use_ids: ['tool-1'],
|
|
794
|
+
summary: 'Read the source file.',
|
|
795
|
+
}),
|
|
796
|
+
context,
|
|
797
|
+
),
|
|
798
|
+
).toEqual([
|
|
799
|
+
{
|
|
800
|
+
kind: 'tool-call',
|
|
801
|
+
timestamp: 1,
|
|
802
|
+
id: 'tool-1',
|
|
803
|
+
name: 'Read',
|
|
804
|
+
input: '{\n "file_path": "src/a.ts"\n}',
|
|
805
|
+
summary: 'Read the source file.',
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
kind: 'lifecycle',
|
|
809
|
+
timestamp: 1,
|
|
810
|
+
label: 'Authenticating',
|
|
811
|
+
detail: null,
|
|
812
|
+
meta: [],
|
|
813
|
+
tone: 'default',
|
|
814
|
+
terminalFailure: false,
|
|
815
|
+
},
|
|
816
|
+
]);
|
|
817
|
+
});
|
|
818
|
+
|
|
210
819
|
it('maps user tool results to tool-result rows', () => {
|
|
211
820
|
const rows = parseClaudeSessionRecord(
|
|
212
821
|
record({
|
|
@@ -230,7 +839,7 @@ describe('parseClaudeSessionRecord', () => {
|
|
|
230
839
|
kind: 'tool-result',
|
|
231
840
|
timestamp: 1,
|
|
232
841
|
toolCallId: 'tool-1',
|
|
233
|
-
toolName:
|
|
842
|
+
toolName: expect.any(String),
|
|
234
843
|
output: 'file contents',
|
|
235
844
|
isError: true,
|
|
236
845
|
},
|
|
@@ -270,7 +879,7 @@ describe('parseClaudeSessionRecord', () => {
|
|
|
270
879
|
kind: 'tool-result',
|
|
271
880
|
timestamp: 1,
|
|
272
881
|
toolCallId: 'tool-1',
|
|
273
|
-
toolName:
|
|
882
|
+
toolName: expect.any(String),
|
|
274
883
|
output: 'tool output',
|
|
275
884
|
isError: false,
|
|
276
885
|
},
|