@muyichengshayu/promptx 0.2.13 → 0.2.15

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 (52) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/apps/server/src/agentSessionDiscovery.js +180 -7
  3. package/apps/web/dist/assets/{CodexSessionManagerDialog-Dic9kMHK.js → CodexSessionManagerDialog-y7O-JTxP.js} +1 -1
  4. package/apps/web/dist/assets/{TaskDiffReviewDialog-CKiZdXqi.js → TaskDiffReviewDialog-CTr_zoAn.js} +1 -1
  5. package/apps/web/dist/assets/{WorkbenchSettingsDialog-CP0z90bm.js → WorkbenchSettingsDialog-Bf2DCuN_.js} +1 -1
  6. package/apps/web/dist/assets/{WorkbenchView-D1oxqNr4.css → WorkbenchView-CK1snPBz.css} +1 -1
  7. package/apps/web/dist/assets/WorkbenchView-Gq3mmtsK.js +60 -0
  8. package/apps/web/dist/assets/index-Co1Ssha9.js +2 -0
  9. package/apps/web/dist/index.html +1 -1
  10. package/package.json +21 -14
  11. package/apps/runner/src/engines/claudeCodeRunner.test.js +0 -467
  12. package/apps/runner/src/engines/kimiCodeRunner.test.js +0 -127
  13. package/apps/runner/src/engines/openCodeRunner.test.js +0 -236
  14. package/apps/runner/src/engines/runnerContract.test.js +0 -449
  15. package/apps/runner/src/engines/shellRunner.test.js +0 -46
  16. package/apps/runner/src/runManager.test.js +0 -913
  17. package/apps/runner/src/serverClient.test.js +0 -93
  18. package/apps/server/src/agentSessionDiscovery.test.js +0 -186
  19. package/apps/server/src/appPaths.test.js +0 -52
  20. package/apps/server/src/assetRoutes.test.js +0 -168
  21. package/apps/server/src/codex.test.js +0 -518
  22. package/apps/server/src/codexRoutes.test.js +0 -376
  23. package/apps/server/src/codexRuns.test.js +0 -160
  24. package/apps/server/src/codexSessions.test.js +0 -369
  25. package/apps/server/src/db.test.js +0 -182
  26. package/apps/server/src/gitDiff.test.js +0 -542
  27. package/apps/server/src/gitDiffClient.test.js +0 -140
  28. package/apps/server/src/internalRoutes.test.js +0 -134
  29. package/apps/server/src/maintenance.test.js +0 -154
  30. package/apps/server/src/processControl.test.js +0 -147
  31. package/apps/server/src/relayClient.test.js +0 -478
  32. package/apps/server/src/relayConfig.test.js +0 -73
  33. package/apps/server/src/relayProtocol.test.js +0 -49
  34. package/apps/server/src/relayServer.test.js +0 -798
  35. package/apps/server/src/relayTenants.test.js +0 -137
  36. package/apps/server/src/relayUsageStore.test.js +0 -65
  37. package/apps/server/src/repository.test.js +0 -150
  38. package/apps/server/src/runDispatchService.test.js +0 -563
  39. package/apps/server/src/runEventIngest.test.js +0 -225
  40. package/apps/server/src/runRecovery.test.js +0 -73
  41. package/apps/server/src/runnerClient.test.js +0 -80
  42. package/apps/server/src/runnerDispatch.test.js +0 -136
  43. package/apps/server/src/systemConfig.test.js +0 -112
  44. package/apps/server/src/systemRoutes.test.js +0 -319
  45. package/apps/server/src/taskRoutes.test.js +0 -775
  46. package/apps/server/src/upload.test.js +0 -30
  47. package/apps/server/src/webAppRoutes.test.js +0 -67
  48. package/apps/server/src/workspaceFiles.test.js +0 -279
  49. package/apps/web/dist/assets/WorkbenchView-noayQwj4.js +0 -60
  50. package/apps/web/dist/assets/index-HLkdzIYF.js +0 -2
  51. package/packages/shared/src/dailyLogStream.test.js +0 -29
  52. package/packages/shared/src/shellCommands.test.js +0 -45
@@ -1,236 +0,0 @@
1
- import test from 'node:test'
2
- import assert from 'node:assert/strict'
3
-
4
- import {
5
- createOpenCodeNormalizationState,
6
- extractOpenCodeErrorMessage,
7
- extractOpenCodeSessionId,
8
- extractOpenCodeText,
9
- extractOpenCodeUsage,
10
- normalizeOpenCodeEvent,
11
- normalizeOpenCodeEvents,
12
- } from './openCodeRunner.js'
13
-
14
- test('runner openCodeRunner maps sub-agent task tool_use to collaboration events', () => {
15
- assert.deepEqual(
16
- normalizeOpenCodeEvents({
17
- type: 'tool_use',
18
- sessionID: 'ses_main',
19
- part: {
20
- type: 'tool',
21
- tool: 'task',
22
- state: {
23
- status: 'completed',
24
- input: {
25
- description: '分析 a.js 文件',
26
- prompt: '请分析 /tmp/demo/a.js 文件',
27
- subagent_type: 'explore',
28
- },
29
- output: 'task_id: ses_child_1\n\n<task_result>ok</task_result>',
30
- metadata: {
31
- sessionId: 'ses_child_1',
32
- model: {
33
- providerID: 'opencode',
34
- modelID: 'minimax-m2.5-free',
35
- },
36
- },
37
- },
38
- },
39
- }),
40
- [
41
- {
42
- type: 'item.completed',
43
- item: {
44
- type: 'collab_tool_call',
45
- tool: 'spawn_agent',
46
- receiver_thread_ids: ['ses_child_1'],
47
- prompt: '请分析 /tmp/demo/a.js 文件',
48
- agents_states: {
49
- ses_child_1: {
50
- status: 'completed',
51
- message: 'task_id: ses_child_1\n\n<task_result>ok</task_result>',
52
- title: '分析 a.js 文件',
53
- role: 'explore',
54
- target: 'a.js',
55
- model: 'opencode/minimax-m2.5-free',
56
- },
57
- },
58
- },
59
- },
60
- {
61
- type: 'item.completed',
62
- item: {
63
- type: 'collab_tool_call',
64
- tool: 'wait',
65
- receiver_thread_ids: ['ses_child_1'],
66
- prompt: '请分析 /tmp/demo/a.js 文件',
67
- agents_states: {
68
- ses_child_1: {
69
- status: 'completed',
70
- message: 'task_id: ses_child_1\n\n<task_result>ok</task_result>',
71
- title: '分析 a.js 文件',
72
- role: 'explore',
73
- target: 'a.js',
74
- model: 'opencode/minimax-m2.5-free',
75
- },
76
- },
77
- },
78
- },
79
- ]
80
- )
81
- })
82
-
83
- test('extractOpenCodeSessionId reads common session id fields', () => {
84
- assert.equal(extractOpenCodeSessionId({ sessionID: 'opencode-session-1' }), 'opencode-session-1')
85
- assert.equal(extractOpenCodeSessionId({ sessionId: 'opencode-session-2' }), 'opencode-session-2')
86
- })
87
-
88
- test('extractOpenCodeText trims text payload', () => {
89
- assert.equal(
90
- extractOpenCodeText({
91
- type: 'text',
92
- part: {
93
- type: 'text',
94
- text: '\n\n已完成修改\n',
95
- },
96
- }),
97
- '已完成修改'
98
- )
99
- })
100
-
101
- test('normalizeOpenCodeEvent maps text output to agent message', () => {
102
- assert.deepEqual(
103
- normalizeOpenCodeEvent({
104
- type: 'text',
105
- part: {
106
- type: 'text',
107
- text: '已完成修改',
108
- },
109
- }),
110
- {
111
- type: 'item.completed',
112
- item: {
113
- type: 'agent_message',
114
- text: '已完成修改',
115
- },
116
- }
117
- )
118
- })
119
-
120
- test('normalizeOpenCodeEvents maps first step_start to turn.started only once', () => {
121
- const state = createOpenCodeNormalizationState()
122
-
123
- assert.deepEqual(
124
- normalizeOpenCodeEvents({
125
- type: 'step_start',
126
- sessionID: 'ses_1',
127
- }, state),
128
- [{ type: 'turn.started' }]
129
- )
130
-
131
- assert.deepEqual(
132
- normalizeOpenCodeEvents({
133
- type: 'step_start',
134
- sessionID: 'ses_1',
135
- }, state),
136
- []
137
- )
138
- })
139
-
140
- test('normalizeOpenCodeEvents maps completed tool_use to command events', () => {
141
- assert.deepEqual(
142
- normalizeOpenCodeEvents({
143
- type: 'tool_use',
144
- sessionID: 'ses_2',
145
- part: {
146
- type: 'tool',
147
- tool: 'read',
148
- state: {
149
- status: 'completed',
150
- input: {
151
- filePath: '/tmp/demo.txt',
152
- },
153
- output: '<path>/tmp/demo.txt</path>',
154
- },
155
- },
156
- }),
157
- [
158
- {
159
- type: 'item.started',
160
- item: {
161
- type: 'command_execution',
162
- command: 'read: /tmp/demo.txt',
163
- status: 'in_progress',
164
- },
165
- },
166
- {
167
- type: 'item.completed',
168
- item: {
169
- type: 'command_execution',
170
- command: 'read: /tmp/demo.txt',
171
- status: 'completed',
172
- exit_code: 0,
173
- aggregated_output: '<path>/tmp/demo.txt</path>',
174
- },
175
- },
176
- ]
177
- )
178
- })
179
-
180
- test('normalizeOpenCodeEvent maps step_finish stop to turn completion with usage', () => {
181
- const event = normalizeOpenCodeEvent({
182
- type: 'step_finish',
183
- part: {
184
- type: 'step-finish',
185
- reason: 'stop',
186
- tokens: {
187
- input: 321,
188
- output: 12,
189
- cache: {
190
- read: 256,
191
- },
192
- },
193
- },
194
- })
195
-
196
- assert.deepEqual(event, {
197
- type: 'turn.completed',
198
- usage: {
199
- input_tokens: 321,
200
- output_tokens: 12,
201
- cached_input_tokens: 256,
202
- },
203
- })
204
-
205
- assert.deepEqual(extractOpenCodeUsage({
206
- part: {
207
- tokens: {
208
- input: 321,
209
- output: 12,
210
- cache: {
211
- read: 256,
212
- },
213
- },
214
- },
215
- }), {
216
- input_tokens: 321,
217
- output_tokens: 12,
218
- cached_input_tokens: 256,
219
- })
220
- })
221
-
222
- test('extractOpenCodeErrorMessage reads nested API errors', () => {
223
- assert.equal(
224
- extractOpenCodeErrorMessage({
225
- type: 'error',
226
- error: {
227
- name: 'APIError',
228
- data: {
229
- message: 'openai_error',
230
- responseBody: '{"error":{"message":"bad gateway"}}',
231
- },
232
- },
233
- }),
234
- 'openai_error'
235
- )
236
- })
@@ -1,449 +0,0 @@
1
- import fs from 'node:fs'
2
- import os from 'node:os'
3
- import path from 'node:path'
4
- import test from 'node:test'
5
- import assert from 'node:assert/strict'
6
-
7
- function withEnv(overrides, fn) {
8
- const previous = new Map()
9
-
10
- for (const [key, value] of Object.entries(overrides)) {
11
- previous.set(key, process.env[key])
12
- if (typeof value === 'undefined') {
13
- delete process.env[key]
14
- } else {
15
- process.env[key] = value
16
- }
17
- }
18
-
19
- return Promise.resolve()
20
- .then(fn)
21
- .finally(() => {
22
- for (const [key, value] of previous.entries()) {
23
- if (typeof value === 'undefined') {
24
- delete process.env[key]
25
- } else {
26
- process.env[key] = value
27
- }
28
- }
29
- })
30
- }
31
-
32
- async function importFreshRunnerModules() {
33
- const suffix = `test=${Date.now()}-${Math.random().toString(16).slice(2)}`
34
- const [
35
- { streamPromptToCodexSession },
36
- { streamPromptToClaudeCodeSession },
37
- { streamPromptToOpenCodeSession },
38
- ] = await Promise.all([
39
- import(`../codex.js?${suffix}`),
40
- import(`./claudeCodeRunner.js?${suffix}`),
41
- import(`./openCodeRunner.js?${suffix}`),
42
- ])
43
-
44
- return {
45
- streamPromptToCodexSession,
46
- streamPromptToClaudeCodeSession,
47
- streamPromptToOpenCodeSession,
48
- }
49
- }
50
-
51
- function createFakeCodexBinary(tempDir) {
52
- const scriptPath = path.join(tempDir, process.platform === 'win32' ? 'fake-codex.js' : 'fake-codex')
53
- const script = `#!/usr/bin/env node
54
- const fs = require('node:fs')
55
-
56
- const args = process.argv.slice(2)
57
- const outputIndex = args.indexOf('--output-last-message')
58
- const outputFile = outputIndex >= 0 ? args[outputIndex + 1] : ''
59
- const threadId = 'thread-contract-1'
60
-
61
- let prompt = ''
62
- process.stdin.setEncoding('utf8')
63
- process.stdin.on('data', (chunk) => {
64
- prompt += chunk
65
- })
66
- process.stdin.on('end', () => {
67
- if (outputFile) {
68
- fs.writeFileSync(outputFile, '最终回复')
69
- }
70
-
71
- process.stdout.write(JSON.stringify({ type: 'thread.started', thread_id: threadId }) + '\\n')
72
- process.stdout.write(JSON.stringify({ type: 'item.started', item: { type: 'reasoning', text: '先分析' } }) + '\\n')
73
- process.stdout.write(JSON.stringify({ type: 'item.started', item: { type: 'command_execution', command: 'Bash: pwd', status: 'in_progress' } }) + '\\n')
74
- process.stdout.write(JSON.stringify({ type: 'item.completed', item: { type: 'command_execution', command: 'Bash: pwd', status: 'completed', exit_code: 0, aggregated_output: '/tmp/demo' } }) + '\\n')
75
- process.stdout.write(JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: '已完成修改' } }) + '\\n')
76
- process.stdout.write(JSON.stringify({ type: 'turn.completed', result: '最终回复' }) + '\\n')
77
- })
78
- `
79
-
80
- fs.writeFileSync(scriptPath, script, { mode: 0o755 })
81
-
82
- if (process.platform !== 'win32') {
83
- return scriptPath
84
- }
85
-
86
- const cmdPath = path.join(tempDir, 'fake-codex.cmd')
87
- fs.writeFileSync(cmdPath, '@echo off\r\nnode "%~dp0fake-codex.js" %*\r\n')
88
- return cmdPath
89
- }
90
-
91
- function createFakeClaudeBinary(tempDir) {
92
- const scriptPath = path.join(tempDir, process.platform === 'win32' ? 'fake-claude.js' : 'fake-claude')
93
- const script = `#!/usr/bin/env node
94
- const args = process.argv.slice(2)
95
- const promptIndex = args.indexOf('-p')
96
- const prompt = promptIndex >= 0 ? args[promptIndex + 1] || '' : ''
97
- const resumeIndex = args.indexOf('--resume')
98
- const threadId = resumeIndex >= 0 ? args[resumeIndex + 1] || 'thread-contract-1' : 'thread-contract-1'
99
-
100
- if (!prompt) {
101
- process.stderr.write('missing prompt\\n')
102
- process.exit(1)
103
- return
104
- }
105
-
106
- process.stdout.write(JSON.stringify({
107
- type: 'system',
108
- subtype: 'init',
109
- session_id: threadId,
110
- }) + '\\n')
111
-
112
- process.stdout.write(JSON.stringify({
113
- type: 'assistant',
114
- message: {
115
- content: [
116
- { type: 'thinking', thinking: '先分析' },
117
- { type: 'tool_use', id: 'tool-1', name: 'Bash', input: { command: 'pwd' } },
118
- ],
119
- },
120
- }) + '\\n')
121
-
122
- process.stdout.write(JSON.stringify({
123
- type: 'user',
124
- message: {
125
- content: [
126
- { type: 'tool_result', tool_use_id: 'tool-1', content: '/tmp/demo', is_error: false },
127
- ],
128
- },
129
- }) + '\\n')
130
-
131
- process.stdout.write(JSON.stringify({
132
- type: 'assistant',
133
- message: {
134
- content: [
135
- { type: 'text', text: '已完成修改' },
136
- ],
137
- },
138
- }) + '\\n')
139
-
140
- process.stdout.write(JSON.stringify({
141
- type: 'result',
142
- result: '最终回复',
143
- }) + '\\n')
144
- `
145
-
146
- fs.writeFileSync(scriptPath, script, { mode: 0o755 })
147
-
148
- if (process.platform !== 'win32') {
149
- return scriptPath
150
- }
151
-
152
- const cmdPath = path.join(tempDir, 'fake-claude.cmd')
153
- fs.writeFileSync(cmdPath, '@echo off\r\nnode "%~dp0fake-claude.js" %*\r\n')
154
- return cmdPath
155
- }
156
-
157
- function createHangingFakeClaudeBinary(tempDir) {
158
- const scriptPath = path.join(tempDir, process.platform === 'win32' ? 'fake-claude-hang.js' : 'fake-claude-hang')
159
- const script = `#!/usr/bin/env node
160
- process.stdout.write(JSON.stringify({
161
- type: 'system',
162
- subtype: 'init',
163
- session_id: 'thread-contract-1',
164
- }) + '\\n')
165
-
166
- process.stdout.write(JSON.stringify({
167
- type: 'assistant',
168
- message: {
169
- content: [
170
- { type: 'text', text: '已完成修改' },
171
- ],
172
- },
173
- }) + '\\n')
174
-
175
- process.stdout.write(JSON.stringify({
176
- type: 'result',
177
- result: '最终回复',
178
- }) + '\\n')
179
-
180
- setInterval(() => {}, 1000)
181
- `
182
-
183
- fs.writeFileSync(scriptPath, script, { mode: 0o755 })
184
-
185
- if (process.platform !== 'win32') {
186
- return scriptPath
187
- }
188
-
189
- const cmdPath = path.join(tempDir, 'fake-claude-hang.cmd')
190
- fs.writeFileSync(cmdPath, '@echo off\r\nnode "%~dp0fake-claude-hang.js" %*\r\n')
191
- return cmdPath
192
- }
193
-
194
- function createFakeOpenCodeBinary(tempDir) {
195
- const scriptPath = path.join(tempDir, process.platform === 'win32' ? 'fake-opencode.js' : 'fake-opencode')
196
- const script = `#!/usr/bin/env node
197
- const args = process.argv.slice(2)
198
- const sessionIndex = args.indexOf('--session')
199
- const sessionId = sessionIndex >= 0 ? args[sessionIndex + 1] || 'thread-contract-1' : 'thread-contract-1'
200
- const prompt = args[args.length - 1] || ''
201
-
202
- if (!prompt) {
203
- process.stderr.write('missing prompt\\n')
204
- process.exit(1)
205
- return
206
- }
207
-
208
- process.stdout.write(JSON.stringify({
209
- type: 'step_start',
210
- sessionID: sessionId,
211
- part: {
212
- type: 'step-start',
213
- },
214
- }) + '\\n')
215
-
216
- process.stdout.write(JSON.stringify({
217
- type: 'tool_use',
218
- sessionID: sessionId,
219
- part: {
220
- type: 'tool',
221
- tool: 'read',
222
- state: {
223
- status: 'completed',
224
- input: {
225
- filePath: '/tmp/demo',
226
- },
227
- output: 'demo output',
228
- },
229
- },
230
- }) + '\\n')
231
-
232
- process.stdout.write(JSON.stringify({
233
- type: 'text',
234
- sessionID: sessionId,
235
- part: {
236
- type: 'text',
237
- text: '最终回复',
238
- },
239
- }) + '\\n')
240
-
241
- process.stdout.write(JSON.stringify({
242
- type: 'step_finish',
243
- sessionID: sessionId,
244
- part: {
245
- type: 'step-finish',
246
- reason: 'stop',
247
- tokens: {
248
- input: 100,
249
- output: 20,
250
- cache: {
251
- read: 10,
252
- },
253
- },
254
- },
255
- }) + '\\n')
256
- `
257
-
258
- fs.writeFileSync(scriptPath, script, { mode: 0o755 })
259
-
260
- if (process.platform !== 'win32') {
261
- return scriptPath
262
- }
263
-
264
- const cmdPath = path.join(tempDir, 'fake-opencode.cmd')
265
- fs.writeFileSync(cmdPath, '@echo off\r\nnode "%~dp0fake-opencode.js" %*\r\n')
266
- return cmdPath
267
- }
268
-
269
- function simplifyEvent(event = {}) {
270
- if (event.type === 'status') {
271
- return {
272
- type: 'status',
273
- stage: event.stage,
274
- message: event.message,
275
- }
276
- }
277
-
278
- if (event.type === 'completed') {
279
- return {
280
- type: 'completed',
281
- message: event.message,
282
- }
283
- }
284
-
285
- if (event.type !== 'agent_event') {
286
- return { type: event.type }
287
- }
288
-
289
- return {
290
- type: 'agent_event',
291
- event: event.event,
292
- }
293
- }
294
-
295
- async function collectRunnerContractEvents(streamSessionPrompt) {
296
- const events = []
297
- const stream = streamSessionPrompt(
298
- { id: 'session-1', cwd: process.cwd() },
299
- 'runner-contract-case',
300
- {
301
- onEvent(event) {
302
- events.push(simplifyEvent(event))
303
- },
304
- }
305
- )
306
-
307
- const result = await stream.result
308
- return {
309
- events,
310
- result,
311
- }
312
- }
313
-
314
- function projectRunnerContractPhases(events = []) {
315
- return events.map((event) => {
316
- if (event.type === 'status') {
317
- return 'status'
318
- }
319
-
320
- if (event.type === 'completed') {
321
- return 'completed'
322
- }
323
-
324
- if (event.type !== 'agent_event') {
325
- return ''
326
- }
327
-
328
- const payload = event.event || {}
329
- if (payload.type === 'thread.started') {
330
- return 'thread.started'
331
- }
332
-
333
- if (payload.type === 'turn.started') {
334
- return 'turn.started'
335
- }
336
-
337
- if (payload.type === 'item.started' && payload.item?.type === 'command_execution') {
338
- return 'command.started'
339
- }
340
-
341
- if (payload.type === 'item.completed' && payload.item?.type === 'command_execution') {
342
- return 'command.completed'
343
- }
344
-
345
- if (payload.type === 'item.completed' && payload.item?.type === 'agent_message') {
346
- return 'agent_message'
347
- }
348
-
349
- if (payload.type === 'turn.completed') {
350
- return 'turn.completed'
351
- }
352
-
353
- return ''
354
- }).filter(Boolean)
355
- }
356
-
357
- function assertOrderedSubsequence(actual = [], expected = []) {
358
- let actualIndex = 0
359
-
360
- expected.forEach((item) => {
361
- while (actualIndex < actual.length && actual[actualIndex] !== item) {
362
- actualIndex += 1
363
- }
364
-
365
- assert.ok(actualIndex < actual.length, `未找到预期阶段:${item},实际阶段:${actual.join(' -> ')}`)
366
- actualIndex += 1
367
- })
368
- }
369
-
370
- function assertRunnerContract(result, expectedThreadId) {
371
- assert.deepEqual(result.result, {
372
- sessionId: 'session-1',
373
- threadId: expectedThreadId,
374
- message: '最终回复',
375
- })
376
-
377
- const phases = projectRunnerContractPhases(result.events)
378
- assertOrderedSubsequence(phases, [
379
- 'status',
380
- 'thread.started',
381
- 'command.started',
382
- 'command.completed',
383
- 'agent_message',
384
- 'turn.completed',
385
- 'completed',
386
- ])
387
- }
388
-
389
- test('Codex / Claude Code / OpenCode runner 会产出兼容的核心事件结构', async () => {
390
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-runner-contract-'))
391
- const fakeCodexBin = createFakeCodexBinary(tempDir)
392
- const fakeClaudeBin = createFakeClaudeBinary(tempDir)
393
- const fakeOpenCodeBin = createFakeOpenCodeBinary(tempDir)
394
-
395
- await withEnv(
396
- {
397
- CODEX_BIN: fakeCodexBin,
398
- CLAUDE_CODE_BIN: fakeClaudeBin,
399
- OPENCODE_BIN: fakeOpenCodeBin,
400
- },
401
- async () => {
402
- const {
403
- streamPromptToCodexSession,
404
- streamPromptToClaudeCodeSession,
405
- streamPromptToOpenCodeSession,
406
- } = await importFreshRunnerModules()
407
-
408
- const [codexResult, claudeResult, openCodeResult] = await Promise.all([
409
- collectRunnerContractEvents(streamPromptToCodexSession),
410
- collectRunnerContractEvents(streamPromptToClaudeCodeSession),
411
- collectRunnerContractEvents(streamPromptToOpenCodeSession),
412
- ])
413
-
414
- assertRunnerContract(codexResult, 'thread-contract-1')
415
- assertRunnerContract(claudeResult, 'thread-contract-1')
416
- assertRunnerContract(openCodeResult, 'thread-contract-1')
417
- }
418
- )
419
- })
420
-
421
- test('Claude Code runner 在 result 后进程不退出时会按 grace timeout 完成', async () => {
422
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-runner-claude-hang-'))
423
- const fakeClaudeBin = createHangingFakeClaudeBinary(tempDir)
424
-
425
- await withEnv(
426
- {
427
- CLAUDE_CODE_BIN: fakeClaudeBin,
428
- PROMPTX_CLAUDE_RESULT_EXIT_GRACE_MS: '30',
429
- PROMPTX_CLAUDE_RESULT_FORCE_STOP_GRACE_MS: '30',
430
- },
431
- async () => {
432
- const { streamPromptToClaudeCodeSession } = await importFreshRunnerModules()
433
- const result = await collectRunnerContractEvents(streamPromptToClaudeCodeSession)
434
-
435
- assert.deepEqual(result.result, {
436
- sessionId: 'session-1',
437
- threadId: 'thread-contract-1',
438
- message: '最终回复',
439
- })
440
- assertOrderedSubsequence(projectRunnerContractPhases(result.events), [
441
- 'status',
442
- 'thread.started',
443
- 'agent_message',
444
- 'turn.completed',
445
- 'completed',
446
- ])
447
- }
448
- )
449
- })