@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,563 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { createRunDispatchService } from './runDispatchService.js'
5
+
6
+ test('runDispatchService keeps queued status when runner accepts queued run', async () => {
7
+ const broadcasts = []
8
+ const service = createRunDispatchService({
9
+ broadcastServerEvent(type, payload = {}) {
10
+ broadcasts.push({ type, ...payload })
11
+ },
12
+ createCodexRun(payload = {}) {
13
+ return {
14
+ id: 'run-1',
15
+ status: payload.status || 'queued',
16
+ }
17
+ },
18
+ decorateCodexSession(session) {
19
+ return {
20
+ ...session,
21
+ running: false,
22
+ }
23
+ },
24
+ getCodexRunById() {
25
+ return {
26
+ id: 'run-1',
27
+ status: 'queued',
28
+ }
29
+ },
30
+ getPromptxCodexSessionById() {
31
+ return {
32
+ id: 'session-1',
33
+ engine: 'codex',
34
+ cwd: '/tmp/demo',
35
+ title: 'Demo',
36
+ codexThreadId: '',
37
+ engineSessionId: '',
38
+ engineThreadId: '',
39
+ engineMeta: {},
40
+ createdAt: '2026-03-22T00:00:00.000Z',
41
+ updatedAt: '2026-03-22T00:00:00.000Z',
42
+ }
43
+ },
44
+ getRunningCodexRunBySessionId() {
45
+ return null
46
+ },
47
+ getTaskBySlug() {
48
+ return {
49
+ slug: 'task-1',
50
+ expired: false,
51
+ }
52
+ },
53
+ logger: {
54
+ warn() {},
55
+ },
56
+ runnerClient: {
57
+ async startRun() {
58
+ return {
59
+ status: 'queued',
60
+ }
61
+ },
62
+ },
63
+ updateCodexRunFromRunnerStatus(_runId, patch = {}) {
64
+ return {
65
+ id: 'run-1',
66
+ status: patch.status || 'queued',
67
+ }
68
+ },
69
+ updateTaskCodexSession() {},
70
+ })
71
+
72
+ const result = await service.startTaskRunForTask({
73
+ taskSlug: 'task-1',
74
+ sessionId: 'session-1',
75
+ prompt: 'hello',
76
+ promptBlocks: [],
77
+ })
78
+
79
+ assert.equal(result.runnerDispatchPending, false)
80
+ assert.equal(result.run?.status, 'queued')
81
+ assert.ok(broadcasts.some((item) => item.type === 'runs.changed' && item.status === 'queued'))
82
+ })
83
+
84
+ test('runDispatchService dispatches shell command runs with project cwd', async () => {
85
+ let createdRunPayload = null
86
+ let runnerPayload = null
87
+ let selectedSessionIds = []
88
+ const service = createRunDispatchService({
89
+ createCodexRun(payload = {}) {
90
+ createdRunPayload = payload
91
+ return {
92
+ id: 'run-shell-1',
93
+ status: payload.status || 'queued',
94
+ }
95
+ },
96
+ decorateCodexSession(session) {
97
+ return {
98
+ ...session,
99
+ running: false,
100
+ }
101
+ },
102
+ getCodexRunById() {
103
+ return {
104
+ id: 'run-shell-1',
105
+ status: 'queued',
106
+ }
107
+ },
108
+ getPromptxCodexSessionById(sessionId) {
109
+ selectedSessionIds.push(sessionId)
110
+ if (sessionId === 'session-1') {
111
+ return {
112
+ id: 'session-1',
113
+ engine: 'codex',
114
+ cwd: '/tmp/demo-shell',
115
+ title: 'Demo Shell',
116
+ codexThreadId: 'thread-root',
117
+ engineSessionId: '',
118
+ engineThreadId: 'thread-root',
119
+ engineMeta: { root: true },
120
+ createdAt: '2026-03-22T00:00:00.000Z',
121
+ updatedAt: '2026-03-22T00:00:00.000Z',
122
+ }
123
+ }
124
+ return {
125
+ id: 'member-claude',
126
+ engine: 'claude-code',
127
+ cwd: '/tmp/demo-shell',
128
+ title: 'Claude Member',
129
+ codexThreadId: '',
130
+ engineSessionId: 'claude-session-1',
131
+ engineThreadId: 'claude-thread-1',
132
+ engineMeta: { hidden: true },
133
+ createdAt: '2026-03-22T00:00:00.000Z',
134
+ updatedAt: '2026-03-22T00:00:00.000Z',
135
+ }
136
+ },
137
+ getRunningCodexRunBySessionId() {
138
+ return null
139
+ },
140
+ getTaskBySlug() {
141
+ return {
142
+ slug: 'task-shell-1',
143
+ expired: false,
144
+ }
145
+ },
146
+ logger: {
147
+ warn() {},
148
+ },
149
+ runnerClient: {
150
+ async startRun(payload = {}) {
151
+ runnerPayload = payload
152
+ return {
153
+ status: 'queued',
154
+ }
155
+ },
156
+ },
157
+ updateCodexRunFromRunnerStatus(_runId, patch = {}) {
158
+ return {
159
+ id: 'run-shell-1',
160
+ status: patch.status || 'queued',
161
+ }
162
+ },
163
+ updateTaskCodexSession() {},
164
+ })
165
+
166
+ await service.startTaskRunForTask({
167
+ taskSlug: 'task-shell-1',
168
+ projectSessionId: 'session-1',
169
+ sessionId: 'member-claude',
170
+ displayEngine: 'claude-code',
171
+ prompt: '!git status --short',
172
+ promptBlocks: [{ type: 'text', content: '!git status --short', meta: {} }],
173
+ commandMode: 'shell',
174
+ command: 'echo hacked',
175
+ allowShellCommand: true,
176
+ })
177
+
178
+ assert.equal(createdRunPayload?.engine, 'shell')
179
+ assert.equal(createdRunPayload?.displayEngine, 'claude-code')
180
+ assert.equal(createdRunPayload?.sessionId, 'session-1')
181
+ assert.equal(createdRunPayload?.prompt, '!git status --short')
182
+ assert.equal(runnerPayload?.engine, 'shell')
183
+ assert.equal(runnerPayload?.prompt, 'git status --short')
184
+ assert.equal(runnerPayload?.cwd, '/tmp/demo-shell')
185
+ assert.equal(runnerPayload?.sessionId, 'session-1')
186
+ assert.equal(runnerPayload?.codexThreadId, '')
187
+ assert.equal(runnerPayload?.engineSessionId, '')
188
+ assert.equal(runnerPayload?.engineThreadId, '')
189
+ assert.deepEqual(runnerPayload?.engineMeta, {})
190
+ assert.deepEqual(selectedSessionIds, ['member-claude', 'session-1', 'session-1'])
191
+ })
192
+
193
+ test('runDispatchService rejects shell runs from non-local requests', async () => {
194
+ const service = createRunDispatchService({
195
+ getPromptxCodexSessionById() {
196
+ return {
197
+ id: 'session-1',
198
+ engine: 'codex',
199
+ cwd: '/tmp/demo-shell',
200
+ title: 'Demo Shell',
201
+ }
202
+ },
203
+ getRunningCodexRunBySessionId() {
204
+ return null
205
+ },
206
+ getTaskBySlug() {
207
+ return {
208
+ slug: 'task-shell-1',
209
+ expired: false,
210
+ }
211
+ },
212
+ })
213
+
214
+ await assert.rejects(
215
+ () => service.startTaskRunForTask({
216
+ taskSlug: 'task-shell-1',
217
+ sessionId: 'session-1',
218
+ prompt: '!pwd',
219
+ promptBlocks: [{ type: 'text', content: '!pwd' }],
220
+ commandMode: 'shell',
221
+ allowShellCommand: false,
222
+ }),
223
+ (error) => error?.statusCode === 403 && error?.messageKey === 'errors.shellLocalOnly'
224
+ )
225
+ })
226
+
227
+ test('runDispatchService marks stop request as stopping and returns accepted result', async () => {
228
+ const broadcasts = []
229
+ let currentStatus = 'running'
230
+ const service = createRunDispatchService({
231
+ broadcastServerEvent(type, payload = {}) {
232
+ broadcasts.push({ type, ...payload })
233
+ },
234
+ getCodexRunById(runId) {
235
+ return {
236
+ id: runId,
237
+ taskSlug: 'task-1',
238
+ sessionId: 'session-1',
239
+ status: currentStatus,
240
+ }
241
+ },
242
+ logger: {
243
+ warn() {},
244
+ },
245
+ runnerClient: {
246
+ stopRun() {
247
+ return Promise.resolve({ accepted: true })
248
+ },
249
+ },
250
+ updateCodexRunFromRunnerStatus(runId, patch = {}) {
251
+ currentStatus = patch.status || currentStatus
252
+ return {
253
+ id: runId,
254
+ taskSlug: 'task-1',
255
+ sessionId: 'session-1',
256
+ status: currentStatus,
257
+ }
258
+ },
259
+ })
260
+
261
+ const result = await service.requestRunStop('run-1', {
262
+ isActiveRunStatus(status) {
263
+ return ['queued', 'starting', 'running', 'stopping'].includes(status)
264
+ },
265
+ reason: 'user_requested',
266
+ })
267
+
268
+ assert.equal(result?.accepted, true)
269
+ assert.equal(result?.run?.status, 'stopping')
270
+ assert.ok(broadcasts.some((item) => item.type === 'runs.changed' && item.status === 'stopping'))
271
+ assert.ok(broadcasts.some((item) => item.type === 'sessions.changed' && item.sessionId === 'session-1'))
272
+ })
273
+
274
+ test('runDispatchService rewrites codex image urls to local server for runner payload only', async () => {
275
+ let createdRunPayload = null
276
+ let runnerPayload = null
277
+ const service = createRunDispatchService({
278
+ createCodexRun(payload = {}) {
279
+ createdRunPayload = payload
280
+ return {
281
+ id: 'run-1',
282
+ status: payload.status || 'queued',
283
+ }
284
+ },
285
+ decorateCodexSession(session) {
286
+ return {
287
+ ...session,
288
+ running: false,
289
+ }
290
+ },
291
+ getCodexRunById() {
292
+ return {
293
+ id: 'run-1',
294
+ status: 'queued',
295
+ }
296
+ },
297
+ getPromptxCodexSessionById() {
298
+ return {
299
+ id: 'session-1',
300
+ engine: 'codex',
301
+ cwd: '/tmp/demo',
302
+ title: 'Demo',
303
+ codexThreadId: '',
304
+ engineSessionId: '',
305
+ engineThreadId: '',
306
+ engineMeta: {},
307
+ createdAt: '2026-03-22T00:00:00.000Z',
308
+ updatedAt: '2026-03-22T00:00:00.000Z',
309
+ }
310
+ },
311
+ getRunningCodexRunBySessionId() {
312
+ return null
313
+ },
314
+ getTaskBySlug() {
315
+ return {
316
+ slug: 'task-1',
317
+ expired: false,
318
+ }
319
+ },
320
+ localServerBaseUrl: 'http://127.0.0.1:3000',
321
+ publicServerBaseUrl: 'https://dongdong.promptx.mushayu.com',
322
+ relayUrl: 'https://dongdong.promptx.mushayu.com',
323
+ logger: {
324
+ warn() {},
325
+ },
326
+ runnerClient: {
327
+ async startRun(payload = {}) {
328
+ runnerPayload = payload
329
+ return {
330
+ status: 'queued',
331
+ }
332
+ },
333
+ },
334
+ updateCodexRunFromRunnerStatus(_runId, patch = {}) {
335
+ return {
336
+ id: 'run-1',
337
+ status: patch.status || 'queued',
338
+ }
339
+ },
340
+ updateTaskCodexSession() {},
341
+ })
342
+
343
+ await service.startTaskRunForTask({
344
+ taskSlug: 'task-1',
345
+ sessionId: 'session-1',
346
+ prompt: '看图:https://dongdong.promptx.mushayu.com/uploads/demo.png',
347
+ promptBlocks: [
348
+ {
349
+ type: 'image',
350
+ content: '/uploads/demo.png',
351
+ meta: {},
352
+ },
353
+ ],
354
+ })
355
+
356
+ assert.equal(createdRunPayload?.prompt, '看图:https://dongdong.promptx.mushayu.com/uploads/demo.png')
357
+ assert.equal(createdRunPayload?.promptBlocks?.[0]?.content, '/uploads/demo.png')
358
+ assert.equal(runnerPayload?.prompt, '看图:http://127.0.0.1:3000/uploads/demo.png')
359
+ assert.equal(runnerPayload?.promptBlocks?.[0]?.content, 'http://127.0.0.1:3000/uploads/demo.png')
360
+ })
361
+
362
+ test('runDispatchService adapts local image base url for claude dev server port', async () => {
363
+ let runnerPayload = null
364
+ const service = createRunDispatchService({
365
+ createCodexRun(payload = {}) {
366
+ return {
367
+ id: 'run-2',
368
+ status: payload.status || 'queued',
369
+ }
370
+ },
371
+ decorateCodexSession(session) {
372
+ return {
373
+ ...session,
374
+ running: false,
375
+ }
376
+ },
377
+ getCodexRunById() {
378
+ return {
379
+ id: 'run-2',
380
+ status: 'queued',
381
+ }
382
+ },
383
+ getPromptxCodexSessionById() {
384
+ return {
385
+ id: 'session-2',
386
+ engine: 'claude-code',
387
+ cwd: '/tmp/demo',
388
+ title: 'Demo',
389
+ codexThreadId: '',
390
+ engineSessionId: '',
391
+ engineThreadId: '',
392
+ engineMeta: {},
393
+ createdAt: '2026-03-22T00:00:00.000Z',
394
+ updatedAt: '2026-03-22T00:00:00.000Z',
395
+ }
396
+ },
397
+ getRunningCodexRunBySessionId() {
398
+ return null
399
+ },
400
+ getTaskBySlug() {
401
+ return {
402
+ slug: 'task-2',
403
+ expired: false,
404
+ }
405
+ },
406
+ localServerBaseUrl: 'http://127.0.0.1:3001',
407
+ publicServerBaseUrl: 'https://dongdong.promptx.mushayu.com',
408
+ relayUrl: 'https://dongdong.promptx.mushayu.com',
409
+ logger: {
410
+ warn() {},
411
+ },
412
+ runnerClient: {
413
+ async startRun(payload = {}) {
414
+ runnerPayload = payload
415
+ return {
416
+ status: 'queued',
417
+ }
418
+ },
419
+ },
420
+ updateCodexRunFromRunnerStatus(_runId, patch = {}) {
421
+ return {
422
+ id: 'run-2',
423
+ status: patch.status || 'queued',
424
+ }
425
+ },
426
+ updateTaskCodexSession() {},
427
+ })
428
+
429
+ await service.startTaskRunForTask({
430
+ taskSlug: 'task-2',
431
+ sessionId: 'session-2',
432
+ prompt: '看图:https://dongdong.promptx.mushayu.com/uploads/demo-2.png',
433
+ promptBlocks: [
434
+ {
435
+ type: 'image',
436
+ content: 'https://dongdong.promptx.mushayu.com/uploads/demo-2.png',
437
+ meta: {},
438
+ },
439
+ ],
440
+ })
441
+
442
+ assert.equal(runnerPayload?.prompt, '看图:http://127.0.0.1:3001/uploads/demo-2.png')
443
+ assert.equal(runnerPayload?.promptBlocks?.[0]?.content, 'http://127.0.0.1:3001/uploads/demo-2.png')
444
+ })
445
+
446
+ test('runDispatchService 在多 agent 项目中把 run 绑定到实际 member session,并回写 root 项目 session', async () => {
447
+ let createdRunPayload = null
448
+ let updatedTaskSession = null
449
+ let runnerPayload = null
450
+ const rootSession = {
451
+ id: 'project-root',
452
+ engine: 'codex',
453
+ cwd: '/tmp/demo',
454
+ title: 'Multi Agent Project',
455
+ codexThreadId: '',
456
+ engineSessionId: '',
457
+ engineThreadId: '',
458
+ engineMeta: {},
459
+ createdAt: '2026-03-22T00:00:00.000Z',
460
+ updatedAt: '2026-03-22T00:00:00.000Z',
461
+ agentBindings: [
462
+ { engine: 'codex', sessionRecordId: 'project-root', isDefault: true },
463
+ { engine: 'claude-code', sessionRecordId: 'member-claude', isDefault: false },
464
+ { engine: 'opencode', sessionRecordId: 'member-opencode', isDefault: false },
465
+ ],
466
+ }
467
+ const memberClaudeSession = {
468
+ id: 'member-claude',
469
+ engine: 'claude-code',
470
+ cwd: '/tmp/demo',
471
+ title: 'Multi Agent Project',
472
+ codexThreadId: '',
473
+ engineSessionId: 'claude-existing',
474
+ engineThreadId: 'claude-existing',
475
+ engineMeta: {
476
+ hidden: true,
477
+ projectRootId: 'project-root',
478
+ },
479
+ createdAt: '2026-03-22T00:00:00.000Z',
480
+ updatedAt: '2026-03-22T00:00:00.000Z',
481
+ }
482
+
483
+ const service = createRunDispatchService({
484
+ createCodexRun(payload = {}) {
485
+ createdRunPayload = payload
486
+ return {
487
+ id: 'run-member-1',
488
+ taskSlug: payload.taskSlug,
489
+ sessionId: payload.sessionId,
490
+ status: payload.status || 'queued',
491
+ }
492
+ },
493
+ decorateCodexSession(session) {
494
+ return {
495
+ ...session,
496
+ running: false,
497
+ }
498
+ },
499
+ getCodexRunById() {
500
+ return {
501
+ id: 'run-member-1',
502
+ status: 'queued',
503
+ }
504
+ },
505
+ getPromptxCodexSessionById(sessionId) {
506
+ if (sessionId === 'project-root') {
507
+ return rootSession
508
+ }
509
+ if (sessionId === 'member-claude') {
510
+ return memberClaudeSession
511
+ }
512
+ return null
513
+ },
514
+ getRunningCodexRunBySessionId() {
515
+ return null
516
+ },
517
+ getTaskBySlug() {
518
+ return {
519
+ slug: 'task-1',
520
+ expired: false,
521
+ }
522
+ },
523
+ logger: {
524
+ warn() {},
525
+ },
526
+ runnerClient: {
527
+ async startRun(payload = {}) {
528
+ runnerPayload = payload
529
+ return {
530
+ status: 'queued',
531
+ }
532
+ },
533
+ },
534
+ updateCodexRunFromRunnerStatus(_runId, patch = {}) {
535
+ return {
536
+ id: 'run-member-1',
537
+ taskSlug: 'task-1',
538
+ sessionId: createdRunPayload?.sessionId || 'member-claude',
539
+ status: patch.status || 'queued',
540
+ }
541
+ },
542
+ updateTaskCodexSession(taskSlug, sessionId) {
543
+ updatedTaskSession = { taskSlug, sessionId }
544
+ },
545
+ })
546
+
547
+ const result = await service.startTaskRunForTask({
548
+ taskSlug: 'task-1',
549
+ sessionId: 'member-claude',
550
+ projectSessionId: 'project-root',
551
+ prompt: 'hello',
552
+ promptBlocks: [],
553
+ })
554
+
555
+ assert.equal(createdRunPayload?.sessionId, 'member-claude')
556
+ assert.equal(updatedTaskSession?.taskSlug, 'task-1')
557
+ assert.equal(updatedTaskSession?.sessionId, 'project-root')
558
+ assert.equal(runnerPayload?.sessionId, 'member-claude')
559
+ assert.equal(runnerPayload?.engine, 'claude-code')
560
+ assert.equal(runnerPayload?.engineSessionId, 'claude-existing')
561
+ assert.equal(result?.run?.sessionId, 'member-claude')
562
+ assert.equal(result?.session?.id, 'project-root')
563
+ })