@muyichengshayu/promptx 0.2.7 → 0.2.8

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/apps/runner/src/engines/claudeCodeRunner.js +69 -1
  3. package/apps/runner/src/engines/claudeCodeRunner.test.js +279 -0
  4. package/apps/runner/src/engines/openCodeRunner.test.js +73 -0
  5. package/apps/runner/src/engines/shellRunner.test.js +46 -0
  6. package/apps/runner/src/runManager.js +110 -11
  7. package/apps/runner/src/runManager.test.js +913 -0
  8. package/apps/runner/src/serverClient.test.js +93 -0
  9. package/apps/server/src/agentSessionDiscovery.test.js +127 -0
  10. package/apps/server/src/agents/claudeCodeRunner.test.js +433 -0
  11. package/apps/server/src/agents/openCodeRunner.test.js +236 -0
  12. package/apps/server/src/agents/runnerContract.test.js +382 -0
  13. package/apps/server/src/appPaths.test.js +52 -0
  14. package/apps/server/src/assetRoutes.test.js +168 -0
  15. package/apps/server/src/codex.test.js +518 -0
  16. package/apps/server/src/codexRoutes.test.js +376 -0
  17. package/apps/server/src/codexRuns.test.js +160 -0
  18. package/apps/server/src/codexSessions.test.js +369 -0
  19. package/apps/server/src/db.test.js +182 -0
  20. package/apps/server/src/gitDiff.test.js +542 -0
  21. package/apps/server/src/gitDiffClient.test.js +140 -0
  22. package/apps/server/src/internalRoutes.test.js +134 -0
  23. package/apps/server/src/maintenance.test.js +154 -0
  24. package/apps/server/src/processControl.test.js +147 -0
  25. package/apps/server/src/relayClient.test.js +478 -0
  26. package/apps/server/src/relayConfig.test.js +73 -0
  27. package/apps/server/src/relayProtocol.test.js +49 -0
  28. package/apps/server/src/relayServer.test.js +798 -0
  29. package/apps/server/src/relayTenants.test.js +137 -0
  30. package/apps/server/src/relayUsageStore.test.js +65 -0
  31. package/apps/server/src/repository.test.js +150 -0
  32. package/apps/server/src/runDispatchService.test.js +563 -0
  33. package/apps/server/src/runEventIngest.test.js +225 -0
  34. package/apps/server/src/runRecovery.test.js +73 -0
  35. package/apps/server/src/runnerClient.test.js +80 -0
  36. package/apps/server/src/runnerDispatch.test.js +136 -0
  37. package/apps/server/src/systemConfig.test.js +112 -0
  38. package/apps/server/src/systemRoutes.test.js +319 -0
  39. package/apps/server/src/taskRoutes.test.js +726 -0
  40. package/apps/server/src/upload.test.js +30 -0
  41. package/apps/server/src/webAppRoutes.test.js +67 -0
  42. package/apps/server/src/workspaceFiles.test.js +262 -0
  43. package/package.json +14 -21
@@ -0,0 +1,93 @@
1
+ import assert from 'node:assert/strict'
2
+ import http from 'node:http'
3
+ import test from 'node:test'
4
+
5
+ import { getInternalAuthHeaderName, getInternalAuthToken } from './internalAuth.js'
6
+ import { createServerClient } from './serverClient.js'
7
+
8
+ function startJsonServer(handler) {
9
+ return new Promise((resolve) => {
10
+ const server = http.createServer(handler)
11
+ server.listen(0, '127.0.0.1', () => {
12
+ const address = server.address()
13
+ resolve({
14
+ server,
15
+ baseUrl: `http://127.0.0.1:${address.port}`,
16
+ })
17
+ })
18
+ })
19
+ }
20
+
21
+ test('serverClient attaches internal auth header and posts JSON payload', async () => {
22
+ const { server, baseUrl } = await startJsonServer(async (request, response) => {
23
+ assert.equal(request.headers[getInternalAuthHeaderName()], getInternalAuthToken())
24
+
25
+ const chunks = []
26
+ for await (const chunk of request) {
27
+ chunks.push(chunk)
28
+ }
29
+ const body = JSON.parse(Buffer.concat(chunks).toString('utf8'))
30
+ assert.equal(body.items.length, 1)
31
+
32
+ response.writeHead(200, { 'Content-Type': 'application/json' })
33
+ response.end(JSON.stringify({ ok: true }))
34
+ })
35
+
36
+ try {
37
+ const client = createServerClient({ baseUrl, timeoutMs: 1000 })
38
+ const payload = await client.postEvents([{ id: 1 }], { runnerId: 'runner-1' })
39
+ assert.deepEqual(payload, { ok: true })
40
+ } finally {
41
+ await new Promise((resolve) => server.close(resolve))
42
+ }
43
+ })
44
+
45
+ test('serverClient fails fast on timeout', async () => {
46
+ const { server, baseUrl } = await startJsonServer((_request, response) => {
47
+ setTimeout(() => {
48
+ response.writeHead(200, { 'Content-Type': 'application/json' })
49
+ response.end(JSON.stringify({ ok: true }))
50
+ }, 700)
51
+ })
52
+
53
+ try {
54
+ const client = createServerClient({ baseUrl, timeoutMs: 500 })
55
+ await assert.rejects(
56
+ () => client.postStatus({ ok: true }),
57
+ (error) => error?.statusCode === 504 && /超时/.test(String(error?.message || ''))
58
+ )
59
+ } finally {
60
+ await new Promise((resolve) => server.close(resolve))
61
+ }
62
+ })
63
+
64
+ test('serverClient reads system config with internal auth header', async () => {
65
+ const { server, baseUrl } = await startJsonServer((request, response) => {
66
+ assert.equal(request.method, 'GET')
67
+ assert.equal(request.url, '/internal/system-config')
68
+ assert.equal(request.headers[getInternalAuthHeaderName()], getInternalAuthToken())
69
+
70
+ response.writeHead(200, { 'Content-Type': 'application/json' })
71
+ response.end(JSON.stringify({
72
+ config: {
73
+ runner: {
74
+ maxConcurrentRuns: 4,
75
+ },
76
+ },
77
+ }))
78
+ })
79
+
80
+ try {
81
+ const client = createServerClient({ baseUrl, timeoutMs: 1000 })
82
+ const payload = await client.getSystemConfig()
83
+ assert.deepEqual(payload, {
84
+ config: {
85
+ runner: {
86
+ maxConcurrentRuns: 4,
87
+ },
88
+ },
89
+ })
90
+ } finally {
91
+ await new Promise((resolve) => server.close(resolve))
92
+ }
93
+ })
@@ -0,0 +1,127 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import test from 'node:test'
6
+
7
+ import {
8
+ decodeClaudeProjectPath,
9
+ listKnownClaudeCodeSessions,
10
+ listKnownOpenCodeSessions,
11
+ } from './agentSessionDiscovery.js'
12
+
13
+ test('decodeClaudeProjectPath decodes unix-style project keys', () => {
14
+ assert.equal(
15
+ decodeClaudeProjectPath('-Users-bravf-code-promptx'),
16
+ '/Users/bravf/code/promptx'
17
+ )
18
+ })
19
+
20
+ test('listKnownClaudeCodeSessions merges transcripts and project files', () => {
21
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-discovery-'))
22
+ const claudeHome = path.join(tempRoot, '.claude')
23
+ const transcriptDir = path.join(claudeHome, 'transcripts')
24
+ const projectsDir = path.join(claudeHome, 'projects', '-Users-bravf-code-promptx')
25
+
26
+ fs.mkdirSync(transcriptDir, { recursive: true })
27
+ fs.mkdirSync(projectsDir, { recursive: true })
28
+
29
+ const transcriptPath = path.join(transcriptDir, 'ses_transcript_only.jsonl')
30
+ const projectPath = path.join(projectsDir, 'ses_project.jsonl')
31
+
32
+ fs.writeFileSync(transcriptPath, `${JSON.stringify({ type: 'user', message: { text: '请继续修复 PromptX' } })}\n`)
33
+ fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: '帮我看下项目源码' } })}\n`)
34
+
35
+ const now = new Date('2026-04-13T08:00:00.000Z')
36
+ fs.utimesSync(transcriptPath, now, now)
37
+ fs.utimesSync(projectPath, new Date(now.getTime() + 1000), new Date(now.getTime() + 1000))
38
+
39
+ try {
40
+ const items = listKnownClaudeCodeSessions({
41
+ claudeHome,
42
+ limit: 10,
43
+ cwd: '/Users/bravf/code/promptx',
44
+ })
45
+
46
+ assert.equal(items.length, 2)
47
+ assert.deepEqual(
48
+ items.map((item) => ({
49
+ id: item.id,
50
+ cwd: item.cwd,
51
+ matchedCwd: item.matchedCwd,
52
+ })),
53
+ [
54
+ {
55
+ id: 'ses_project',
56
+ cwd: '/Users/bravf/code/promptx',
57
+ matchedCwd: true,
58
+ },
59
+ {
60
+ id: 'ses_transcript_only',
61
+ cwd: '',
62
+ matchedCwd: false,
63
+ },
64
+ ]
65
+ )
66
+ assert.match(items[0].label, /项目源码|promptx/i)
67
+ } finally {
68
+ fs.rmSync(tempRoot, { recursive: true, force: true })
69
+ }
70
+ })
71
+
72
+ test('listKnownOpenCodeSessions discovers sessions from desktop dat files', () => {
73
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-opencode-discovery-'))
74
+ const dataDir = path.join(tempRoot, 'ai.opencode.desktop')
75
+ fs.mkdirSync(dataDir, { recursive: true })
76
+
77
+ const globalPath = path.join(dataDir, 'opencode.global.dat')
78
+ const workspacePath = path.join(
79
+ dataDir,
80
+ `opencode.workspace.${Buffer.from('/Users/bravf/code/promptx').toString('base64')}.test.dat`
81
+ )
82
+
83
+ fs.writeFileSync(globalPath, JSON.stringify({
84
+ 'layout.page': JSON.stringify({
85
+ lastProjectSession: {
86
+ '/Users/bravf/code/promptx': {
87
+ directory: '/Users/bravf/code/promptx',
88
+ id: 'ses_global',
89
+ at: '2026-04-13T08:00:00.000Z',
90
+ },
91
+ },
92
+ lastSession: {
93
+ '/Users/bravf/code/promptx': 'ses_last',
94
+ },
95
+ }),
96
+ }))
97
+
98
+ fs.writeFileSync(workspacePath, JSON.stringify({
99
+ 'session:ses_global:comments': '[]',
100
+ 'session:ses_workspace:prompt': '请继续处理前端主题',
101
+ }))
102
+
103
+ try {
104
+ const items = listKnownOpenCodeSessions({
105
+ openCodeDataDir: dataDir,
106
+ openCodeDbPath: path.join(tempRoot, 'missing-opencode.db'),
107
+ cwd: '/Users/bravf/code/promptx',
108
+ limit: 10,
109
+ })
110
+
111
+ assert.deepEqual(
112
+ items.map((item) => ({
113
+ id: item.id,
114
+ cwd: item.cwd,
115
+ matchedCwd: item.matchedCwd,
116
+ })),
117
+ [
118
+ { id: 'ses_global', cwd: '/Users/bravf/code/promptx', matchedCwd: true },
119
+ { id: 'ses_workspace', cwd: '/Users/bravf/code/promptx', matchedCwd: true },
120
+ { id: 'ses_last', cwd: '/Users/bravf/code/promptx', matchedCwd: true },
121
+ ]
122
+ )
123
+ assert.match(items[1].label, /前端主题/)
124
+ } finally {
125
+ fs.rmSync(tempRoot, { recursive: true, force: true })
126
+ }
127
+ })
@@ -0,0 +1,433 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ createClaudeNormalizationState,
6
+ extractClaudeAssistantText,
7
+ extractClaudeResultText,
8
+ extractClaudeSessionId,
9
+ normalizeClaudeEvent,
10
+ normalizeClaudeEvents,
11
+ } from './claudeCodeRunner.js'
12
+
13
+ test('extractClaudeAssistantText joins nested text parts', () => {
14
+ const text = extractClaudeAssistantText({
15
+ type: 'assistant',
16
+ message: {
17
+ content: [
18
+ { type: 'text', text: '第一段' },
19
+ { type: 'text', text: '第二段' },
20
+ ],
21
+ },
22
+ })
23
+
24
+ assert.equal(text, '第一段\n第二段')
25
+ })
26
+
27
+ test('extractClaudeSessionId reads common session id fields', () => {
28
+ assert.equal(extractClaudeSessionId({ session_id: 'claude-session-1' }), 'claude-session-1')
29
+ assert.equal(extractClaudeSessionId({ result: { session_id: 'claude-session-2' } }), 'claude-session-2')
30
+ })
31
+
32
+ test('normalizeClaudeEvent maps assistant output to agent message', () => {
33
+ assert.deepEqual(
34
+ normalizeClaudeEvent({
35
+ type: 'assistant',
36
+ message: {
37
+ content: [{ type: 'text', text: '已完成修改' }],
38
+ },
39
+ }),
40
+ {
41
+ type: 'item.completed',
42
+ item: {
43
+ type: 'agent_message',
44
+ text: '已完成修改',
45
+ },
46
+ }
47
+ )
48
+ })
49
+
50
+ test('normalizeClaudeEvent maps result output to turn completion', () => {
51
+ assert.deepEqual(
52
+ normalizeClaudeEvent({
53
+ type: 'result',
54
+ result: '最终回复',
55
+ }),
56
+ {
57
+ type: 'turn.completed',
58
+ result: '最终回复',
59
+ }
60
+ )
61
+
62
+ assert.equal(extractClaudeResultText({ result: '最终回复' }), '最终回复')
63
+ })
64
+
65
+ test('normalizeClaudeEvents maps system init to thread start', () => {
66
+ assert.deepEqual(
67
+ normalizeClaudeEvents({
68
+ type: 'system',
69
+ subtype: 'init',
70
+ session_id: 'claude-session-init',
71
+ }),
72
+ [{
73
+ type: 'thread.started',
74
+ thread_id: 'claude-session-init',
75
+ }]
76
+ )
77
+ })
78
+
79
+ test('normalizeClaudeEvents maps fatal auth api_retry to error event', () => {
80
+ assert.deepEqual(
81
+ normalizeClaudeEvents({
82
+ type: 'system',
83
+ subtype: 'api_retry',
84
+ attempt: 1,
85
+ max_retries: 10,
86
+ error_status: 401,
87
+ error: 'authentication_failed',
88
+ }),
89
+ [{
90
+ type: 'error',
91
+ message: 'Claude Code 认证失败(HTTP 401 authentication_failed)。请重新登录 Claude Code,或检查当前环境中的认证令牌配置。',
92
+ }]
93
+ )
94
+ })
95
+
96
+ test('normalizeClaudeEvents maps transient api_retry to reconnecting error event', () => {
97
+ assert.deepEqual(
98
+ normalizeClaudeEvents({
99
+ type: 'system',
100
+ subtype: 'api_retry',
101
+ attempt: 2,
102
+ max_retries: 10,
103
+ error_status: 503,
104
+ error: 'overloaded',
105
+ }),
106
+ [{
107
+ type: 'error',
108
+ message: 'Reconnecting... 2/10 (HTTP 503 overloaded)',
109
+ }]
110
+ )
111
+ })
112
+
113
+ test('normalizeClaudeEvents maps thinking, tool use and text blocks', () => {
114
+ const state = createClaudeNormalizationState()
115
+
116
+ assert.deepEqual(
117
+ normalizeClaudeEvents({
118
+ type: 'assistant',
119
+ message: {
120
+ content: [
121
+ { type: 'thinking', thinking: '先看看目录结构' },
122
+ { type: 'tool_use', id: 'tool-1', name: 'Bash', input: { command: 'ls -1' } },
123
+ { type: 'text', text: '已查看完成' },
124
+ ],
125
+ },
126
+ }, state),
127
+ [
128
+ {
129
+ type: 'item.started',
130
+ item: {
131
+ type: 'reasoning',
132
+ text: '先看看目录结构',
133
+ },
134
+ },
135
+ {
136
+ type: 'item.started',
137
+ item: {
138
+ type: 'command_execution',
139
+ command: 'Bash: ls -1',
140
+ status: 'in_progress',
141
+ },
142
+ },
143
+ {
144
+ type: 'item.completed',
145
+ item: {
146
+ type: 'agent_message',
147
+ text: '已查看完成',
148
+ },
149
+ },
150
+ ]
151
+ )
152
+ })
153
+
154
+ test('normalizeClaudeEvents maps tool results back to remembered tool call', () => {
155
+ const state = createClaudeNormalizationState()
156
+ normalizeClaudeEvents({
157
+ type: 'assistant',
158
+ message: {
159
+ content: [
160
+ { type: 'tool_use', id: 'tool-2', name: 'Bash', input: { command: 'pwd' } },
161
+ ],
162
+ },
163
+ }, state)
164
+
165
+ assert.deepEqual(
166
+ normalizeClaudeEvents({
167
+ type: 'user',
168
+ message: {
169
+ content: [
170
+ { type: 'tool_result', tool_use_id: 'tool-2', content: '/tmp/demo', is_error: false },
171
+ ],
172
+ },
173
+ }, state),
174
+ [{
175
+ type: 'item.completed',
176
+ item: {
177
+ type: 'command_execution',
178
+ command: 'Bash: pwd',
179
+ status: 'completed',
180
+ exit_code: 0,
181
+ aggregated_output: '/tmp/demo',
182
+ },
183
+ }]
184
+ )
185
+ })
186
+
187
+ test('normalizeClaudeEvents stringifies structured tool results', () => {
188
+ const state = createClaudeNormalizationState()
189
+ normalizeClaudeEvents({
190
+ type: 'assistant',
191
+ message: {
192
+ content: [
193
+ { type: 'tool_use', id: 'tool-3', name: 'Read', input: { file_path: '/tmp/demo.txt' } },
194
+ ],
195
+ },
196
+ }, state)
197
+
198
+ assert.deepEqual(
199
+ normalizeClaudeEvents({
200
+ type: 'user',
201
+ message: {
202
+ content: [
203
+ {
204
+ type: 'tool_result',
205
+ tool_use_id: 'tool-3',
206
+ content: [
207
+ { type: 'text', text: '<path>/tmp/demo.txt</path>' },
208
+ { type: 'text', text: '<type>file</type>' },
209
+ ],
210
+ },
211
+ ],
212
+ },
213
+ }, state),
214
+ [{
215
+ type: 'item.completed',
216
+ item: {
217
+ type: 'command_execution',
218
+ command: 'Read: /tmp/demo.txt',
219
+ status: 'completed',
220
+ exit_code: 0,
221
+ aggregated_output: '<path>/tmp/demo.txt</path>\n<type>file</type>',
222
+ },
223
+ }]
224
+ )
225
+ })
226
+
227
+ test('normalizeClaudeEvents keeps full TodoWrite input for downstream todo parsing', () => {
228
+ const state = createClaudeNormalizationState()
229
+
230
+ const events = normalizeClaudeEvents({
231
+ type: 'assistant',
232
+ message: {
233
+ content: [
234
+ {
235
+ type: 'tool_use',
236
+ id: 'tool-todo-1',
237
+ name: 'TodoWrite',
238
+ input: {
239
+ todos: [
240
+ { content: '定位 Codex CLI 相关源码', activeForm: '正在定位 Codex CLI 相关源码', status: 'in_progress' },
241
+ { content: '阅读核心模块与数据流', activeForm: '正在阅读核心模块与数据流', status: 'pending' },
242
+ { content: '整理架构与关键设计', activeForm: '正在整理架构与关键设计', status: 'pending' },
243
+ ],
244
+ },
245
+ },
246
+ ],
247
+ },
248
+ }, state)
249
+
250
+ assert.equal(events[0]?.type, 'item.started')
251
+ assert.match(events[0]?.item?.command || '', /TodoWrite: \{"todos":\[/)
252
+ assert.doesNotMatch(events[0]?.item?.command || '', /\.\.\.$/)
253
+ assert.match(events[0]?.item?.command || '', /"activeForm":"正在阅读核心模块与数据流"/)
254
+ })
255
+
256
+ test('normalizeClaudeEvents maps Agent sub-agents into collaboration events', () => {
257
+ const state = createClaudeNormalizationState()
258
+
259
+ assert.deepEqual(
260
+ normalizeClaudeEvents({
261
+ type: 'assistant',
262
+ message: {
263
+ content: [
264
+ {
265
+ type: 'tool_use',
266
+ id: 'agent-tool-1',
267
+ name: 'Agent',
268
+ input: {
269
+ description: 'Analyze a.js exports',
270
+ subagent_type: 'general-purpose',
271
+ prompt: 'Analyze a.js in the current directory.',
272
+ model: 'sonnet',
273
+ },
274
+ },
275
+ ],
276
+ },
277
+ }, state),
278
+ [{
279
+ type: 'item.started',
280
+ item: {
281
+ type: 'collab_tool_call',
282
+ tool: 'spawn_agent',
283
+ receiver_thread_ids: [],
284
+ prompt: 'Analyze a.js in the current directory.',
285
+ agents_states: {},
286
+ },
287
+ }]
288
+ )
289
+
290
+ assert.deepEqual(
291
+ normalizeClaudeEvents({
292
+ type: 'system',
293
+ subtype: 'task_started',
294
+ tool_use_id: 'agent-tool-1',
295
+ task_id: 'task-a',
296
+ description: 'Analyze a.js exports',
297
+ }, state),
298
+ [{
299
+ type: 'item.completed',
300
+ item: {
301
+ type: 'collab_tool_call',
302
+ tool: 'spawn_agent',
303
+ receiver_thread_ids: ['task-a'],
304
+ prompt: 'Analyze a.js in the current directory.',
305
+ agents_states: {
306
+ 'task-a': {
307
+ status: 'running',
308
+ message: '',
309
+ title: 'Analyze a.js exports',
310
+ role: 'general-purpose',
311
+ target: 'a.js',
312
+ model: 'sonnet',
313
+ task_id: 'task-a',
314
+ },
315
+ },
316
+ },
317
+ }]
318
+ )
319
+
320
+ assert.deepEqual(
321
+ normalizeClaudeEvents({
322
+ type: 'user',
323
+ message: {
324
+ content: [
325
+ {
326
+ type: 'tool_result',
327
+ tool_use_id: 'agent-tool-1',
328
+ content: 'found 2 exports',
329
+ is_error: false,
330
+ },
331
+ ],
332
+ },
333
+ }, state),
334
+ [{
335
+ type: 'item.completed',
336
+ item: {
337
+ type: 'collab_tool_call',
338
+ tool: 'wait',
339
+ receiver_thread_ids: ['task-a'],
340
+ prompt: 'Analyze a.js in the current directory.',
341
+ agents_states: {
342
+ 'task-a': {
343
+ status: 'completed',
344
+ message: 'found 2 exports',
345
+ title: 'Analyze a.js exports',
346
+ role: 'general-purpose',
347
+ target: 'a.js',
348
+ model: 'sonnet',
349
+ task_id: 'task-a',
350
+ },
351
+ },
352
+ },
353
+ }]
354
+ )
355
+ })
356
+
357
+ test('normalizeClaudeEvents maps task_completed and ignores duplicate tool_result for Agent sub-agents', () => {
358
+ const state = createClaudeNormalizationState()
359
+
360
+ normalizeClaudeEvents({
361
+ type: 'assistant',
362
+ message: {
363
+ content: [
364
+ {
365
+ type: 'tool_use',
366
+ id: 'agent-tool-2',
367
+ name: 'Agent',
368
+ input: {
369
+ description: 'Analyze b.js exports',
370
+ subagent_type: 'general-purpose',
371
+ prompt: 'Analyze b.js in the current directory.',
372
+ model: 'sonnet',
373
+ },
374
+ },
375
+ ],
376
+ },
377
+ }, state)
378
+
379
+ normalizeClaudeEvents({
380
+ type: 'system',
381
+ subtype: 'task_started',
382
+ tool_use_id: 'agent-tool-2',
383
+ task_id: 'task-b',
384
+ description: 'Analyze b.js exports',
385
+ }, state)
386
+
387
+ assert.deepEqual(
388
+ normalizeClaudeEvents({
389
+ type: 'system',
390
+ subtype: 'task_completed',
391
+ task_id: 'task-b',
392
+ result: 'found 2 exports',
393
+ description: 'Analyze b.js exports',
394
+ }, state),
395
+ [{
396
+ type: 'item.completed',
397
+ item: {
398
+ type: 'collab_tool_call',
399
+ tool: 'wait',
400
+ receiver_thread_ids: ['task-b'],
401
+ prompt: 'Analyze b.js in the current directory.',
402
+ agents_states: {
403
+ 'task-b': {
404
+ status: 'completed',
405
+ message: 'found 2 exports',
406
+ title: 'Analyze b.js exports',
407
+ role: 'general-purpose',
408
+ target: 'b.js',
409
+ model: 'sonnet',
410
+ task_id: 'task-b',
411
+ },
412
+ },
413
+ },
414
+ }]
415
+ )
416
+
417
+ assert.deepEqual(
418
+ normalizeClaudeEvents({
419
+ type: 'user',
420
+ message: {
421
+ content: [
422
+ {
423
+ type: 'tool_result',
424
+ tool_use_id: 'agent-tool-2',
425
+ content: 'duplicate result',
426
+ is_error: false,
427
+ },
428
+ ],
429
+ },
430
+ }, state),
431
+ []
432
+ )
433
+ })