@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,775 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import Fastify from 'fastify'
3
- import test from 'node:test'
4
-
5
- import { buildInternalAuthHeaders } from './internalAuth.js'
6
- import {
7
- createEmptyWorkspaceDiffSummary,
8
- createTaskWorkspaceDiffSummaryService,
9
- registerTaskRoutes,
10
- } from './taskRoutes.js'
11
-
12
- test('task workspace diff summary service normalizes and reuses workspace summaries', () => {
13
- const lookups = []
14
- const service = createTaskWorkspaceDiffSummaryService({
15
- getPromptxCodexSessionById(sessionId) {
16
- return {
17
- s1: { id: 's1', cwd: '/repo/a' },
18
- s2: { id: 's2', cwd: '/repo/a' },
19
- }[sessionId] || null
20
- },
21
- getWorkspaceGitDiffStatusSummaryByCwd(cwd) {
22
- lookups.push(cwd)
23
- return {
24
- supported: true,
25
- summary: {
26
- fileCount: 3,
27
- additions: 8,
28
- deletions: 2,
29
- statsComplete: true,
30
- },
31
- }
32
- },
33
- listTasks() {
34
- return [
35
- { slug: 'a', codexSessionId: 's1' },
36
- { slug: 'b', codexSessionId: 's2' },
37
- { slug: 'c', codexSessionId: '' },
38
- ]
39
- },
40
- })
41
-
42
- const items = service.listTaskWorkspaceDiffSummaries()
43
- assert.deepEqual(lookups, ['/repo/a'])
44
- assert.deepEqual(items, [
45
- {
46
- slug: 'a',
47
- workspaceDiffSummary: {
48
- supported: true,
49
- fileCount: 3,
50
- additions: 8,
51
- deletions: 2,
52
- statsComplete: true,
53
- },
54
- },
55
- {
56
- slug: 'b',
57
- workspaceDiffSummary: {
58
- supported: true,
59
- fileCount: 3,
60
- additions: 8,
61
- deletions: 2,
62
- statsComplete: true,
63
- },
64
- },
65
- {
66
- slug: 'c',
67
- workspaceDiffSummary: createEmptyWorkspaceDiffSummary(),
68
- },
69
- ])
70
- })
71
-
72
- test('task workspace diff summary service can refresh requested task slugs only', () => {
73
- const lookups = []
74
- const tasksBySlug = {
75
- a: { slug: 'a', codexSessionId: 's1' },
76
- b: { slug: 'b', codexSessionId: 's2' },
77
- }
78
- const service = createTaskWorkspaceDiffSummaryService({
79
- getPromptxCodexSessionById(sessionId) {
80
- return {
81
- s1: { id: 's1', cwd: '/repo/a' },
82
- s2: { id: 's2', cwd: '/repo/b' },
83
- }[sessionId] || null
84
- },
85
- getTaskBySlug(slug) {
86
- return tasksBySlug[slug] || null
87
- },
88
- getWorkspaceGitDiffStatusSummaryByCwd(cwd) {
89
- lookups.push(cwd)
90
- return {
91
- supported: true,
92
- summary: {
93
- fileCount: cwd === '/repo/b' ? 2 : 1,
94
- additions: 0,
95
- deletions: 0,
96
- statsComplete: true,
97
- },
98
- }
99
- },
100
- listTasks() {
101
- return Object.values(tasksBySlug)
102
- },
103
- })
104
-
105
- const items = service.listTaskWorkspaceDiffSummaries(30, ['b'])
106
- assert.deepEqual(lookups, ['/repo/b'])
107
- assert.deepEqual(items, [
108
- {
109
- slug: 'b',
110
- workspaceDiffSummary: {
111
- supported: true,
112
- fileCount: 2,
113
- additions: 0,
114
- deletions: 0,
115
- statsComplete: true,
116
- },
117
- },
118
- ])
119
- })
120
-
121
- test('task routes return 202 when runner dispatch remains pending', async () => {
122
- const app = Fastify()
123
- registerTaskRoutes(app, {
124
- broadcastServerEvent: () => {},
125
- buildTaskExports: () => ({ raw: '' }),
126
- canEditTask: () => true,
127
- createTask: () => null,
128
- decorateTask: (task) => task,
129
- decorateTaskList: (items) => items,
130
- deleteTask: () => ({ error: 'not_found' }),
131
- deleteTaskCodexRuns: () => {},
132
- getPromptxCodexSessionById: () => ({ id: 'session-1' }),
133
- getRunningCodexRunByTaskSlug: () => null,
134
- getTaskBySlug: (slug) => ({ slug, expired: false }),
135
- getTaskGitDiffReviewInSubprocess: async () => ({}),
136
- listTaskCodexRunsWithOptions: () => [],
137
- listTaskWorkspaceDiffSummaries: () => [],
138
- listTasks: () => [],
139
- reorderTasks: () => ({ changed: false, items: [] }),
140
- purgeExpiredContent: () => {},
141
- removeAssetFiles: () => {},
142
- runDispatchService: {
143
- async startTaskRunForTask() {
144
- return {
145
- run: { id: 'run-1', status: 'queued' },
146
- runnerDispatchPending: true,
147
- }
148
- },
149
- },
150
- updateTask: () => null,
151
- updateTaskCodexSession: () => null,
152
- })
153
- await app.ready()
154
-
155
- try {
156
- const response = await app.inject({
157
- method: 'POST',
158
- url: '/api/tasks/task-1/codex-runs',
159
- payload: {
160
- sessionId: 'session-1',
161
- prompt: 'hello',
162
- },
163
- })
164
-
165
- assert.equal(response.statusCode, 202)
166
- assert.equal(response.json().run.id, 'run-1')
167
- } finally {
168
- await app.close()
169
- }
170
- })
171
-
172
- test('task routes block remote shell commands', async () => {
173
- const app = Fastify()
174
- registerTaskRoutes(app, {
175
- broadcastServerEvent: () => {},
176
- buildTaskExports: () => ({ raw: '' }),
177
- canEditTask: () => true,
178
- createTask: () => null,
179
- decorateTask: (task) => task,
180
- decorateTaskList: (items) => items,
181
- deleteTask: () => ({ error: 'not_found' }),
182
- deleteTaskCodexRuns: () => {},
183
- getPromptxCodexSessionById: () => ({ id: 'session-1' }),
184
- getRunningCodexRunByTaskSlug: () => null,
185
- getTaskBySlug: (slug) => ({ slug, expired: false }),
186
- getTaskGitDiffReviewInSubprocess: async () => ({}),
187
- listTaskCodexRunsWithOptions: () => [],
188
- listTaskWorkspaceDiffSummaries: () => [],
189
- listTasks: () => [],
190
- reorderTasks: () => ({ changed: false, items: [] }),
191
- purgeExpiredContent: () => {},
192
- removeAssetFiles: () => {},
193
- runDispatchService: {
194
- async startTaskRunForTask(payload = {}) {
195
- if (payload.allowShellCommand !== true) {
196
- const error = new Error('命令模式默认仅允许在本机本地界面中使用;如需对远程访问开放,请先到设置里显式开启。')
197
- error.statusCode = 403
198
- error.messageKey = 'errors.shellLocalOnly'
199
- throw error
200
- }
201
- return {
202
- run: { id: 'run-shell-1', status: 'queued' },
203
- runnerDispatchPending: false,
204
- }
205
- },
206
- },
207
- updateTask: () => null,
208
- updateTaskCodexSession: () => null,
209
- })
210
- await app.ready()
211
-
212
- try {
213
- const response = await app.inject({
214
- method: 'POST',
215
- url: '/api/tasks/task-1/codex-runs',
216
- headers: {
217
- origin: 'https://dongdong.promptx.mushayu.com',
218
- },
219
- payload: {
220
- sessionId: 'session-1',
221
- prompt: '!pwd',
222
- promptBlocks: [{ type: 'text', content: '!pwd' }],
223
- commandMode: 'shell',
224
- },
225
- })
226
-
227
- assert.equal(response.statusCode, 403)
228
- assert.equal(response.json().messageKey, 'errors.shellLocalOnly')
229
- } finally {
230
- await app.close()
231
- }
232
- })
233
-
234
- test('task routes allow relay remote shell commands when remote command security uses relay mode', async () => {
235
- let allowShellCommand = false
236
- const app = Fastify()
237
- registerTaskRoutes(app, {
238
- broadcastServerEvent: () => {},
239
- buildTaskExports: () => ({ raw: '' }),
240
- canEditTask: () => true,
241
- createTask: () => null,
242
- decorateTask: (task) => task,
243
- decorateTaskList: (items) => items,
244
- deleteTask: () => ({ error: 'not_found' }),
245
- deleteTaskCodexRuns: () => {},
246
- getPromptxCodexSessionById: () => ({ id: 'session-1' }),
247
- getRelayConfig: () => ({ allowRemoteShell: true }),
248
- getSystemConfig: () => ({
249
- remoteCommandSecurity: {
250
- mode: 'relay',
251
- trustedProxyToken: '',
252
- },
253
- }),
254
- getRunningCodexRunByTaskSlug: () => null,
255
- getTaskBySlug: (slug) => ({ slug, expired: false }),
256
- getTaskGitDiffReviewInSubprocess: async () => ({}),
257
- listTaskCodexRunsWithOptions: () => [],
258
- listTaskWorkspaceDiffSummaries: () => [],
259
- listTasks: () => [],
260
- reorderTasks: () => ({ changed: false, items: [] }),
261
- purgeExpiredContent: () => {},
262
- removeAssetFiles: () => {},
263
- runDispatchService: {
264
- async startTaskRunForTask(payload = {}) {
265
- allowShellCommand = payload.allowShellCommand === true
266
- return {
267
- run: { id: 'run-shell-1', status: 'queued' },
268
- runnerDispatchPending: false,
269
- }
270
- },
271
- },
272
- updateTask: () => null,
273
- updateTaskCodexSession: () => null,
274
- })
275
- await app.ready()
276
-
277
- try {
278
- const response = await app.inject({
279
- method: 'POST',
280
- url: '/api/tasks/task-1/codex-runs',
281
- headers: buildInternalAuthHeaders({
282
- origin: 'https://dongdong.promptx.mushayu.com',
283
- 'x-promptx-relay-request': '1',
284
- }),
285
- payload: {
286
- sessionId: 'session-1',
287
- prompt: '!pwd',
288
- promptBlocks: [{ type: 'text', content: '!pwd' }],
289
- commandMode: 'shell',
290
- },
291
- })
292
-
293
- assert.equal(response.statusCode, 201)
294
- assert.equal(allowShellCommand, true)
295
- } finally {
296
- await app.close()
297
- }
298
- })
299
-
300
- test('task routes block non-relay remote shell requests in relay security mode', async () => {
301
- const app = Fastify()
302
- registerTaskRoutes(app, {
303
- broadcastServerEvent: () => {},
304
- buildTaskExports: () => ({ raw: '' }),
305
- canEditTask: () => true,
306
- createTask: () => null,
307
- decorateTask: (task) => task,
308
- decorateTaskList: (items) => items,
309
- deleteTask: () => ({ error: 'not_found' }),
310
- deleteTaskCodexRuns: () => {},
311
- getPromptxCodexSessionById: () => ({ id: 'session-1' }),
312
- getRelayConfig: () => ({ allowRemoteShell: true }),
313
- getSystemConfig: () => ({
314
- remoteCommandSecurity: {
315
- mode: 'relay',
316
- trustedProxyToken: '',
317
- },
318
- }),
319
- getRunningCodexRunByTaskSlug: () => null,
320
- getTaskBySlug: (slug) => ({ slug, expired: false }),
321
- getTaskGitDiffReviewInSubprocess: async () => ({}),
322
- listTaskCodexRunsWithOptions: () => [],
323
- listTaskWorkspaceDiffSummaries: () => [],
324
- listTasks: () => [],
325
- reorderTasks: () => ({ changed: false, items: [] }),
326
- purgeExpiredContent: () => {},
327
- removeAssetFiles: () => {},
328
- runDispatchService: {
329
- async startTaskRunForTask(payload = {}) {
330
- if (payload.allowShellCommand !== true) {
331
- const error = new Error('命令模式默认仅允许在本机本地界面中使用;如需对远程访问开放,请先到设置里显式开启。')
332
- error.statusCode = 403
333
- error.messageKey = 'errors.shellLocalOnly'
334
- throw error
335
- }
336
- return {
337
- run: { id: 'run-shell-1', status: 'queued' },
338
- runnerDispatchPending: false,
339
- }
340
- },
341
- },
342
- updateTask: () => null,
343
- updateTaskCodexSession: () => null,
344
- })
345
- await app.ready()
346
-
347
- try {
348
- const response = await app.inject({
349
- method: 'POST',
350
- url: '/api/tasks/task-1/codex-runs',
351
- headers: {
352
- origin: 'https://dongdong.promptx.mushayu.com',
353
- },
354
- payload: {
355
- sessionId: 'session-1',
356
- prompt: '!pwd',
357
- promptBlocks: [{ type: 'text', content: '!pwd' }],
358
- commandMode: 'shell',
359
- },
360
- })
361
-
362
- assert.equal(response.statusCode, 403)
363
- assert.equal(response.json().messageKey, 'errors.shellLocalOnly')
364
- } finally {
365
- await app.close()
366
- }
367
- })
368
-
369
- test('task routes allow trusted proxy remote shell commands when configured', async () => {
370
- let allowShellCommand = false
371
- const app = Fastify()
372
- registerTaskRoutes(app, {
373
- broadcastServerEvent: () => {},
374
- buildTaskExports: () => ({ raw: '' }),
375
- canEditTask: () => true,
376
- createTask: () => null,
377
- decorateTask: (task) => task,
378
- decorateTaskList: (items) => items,
379
- deleteTask: () => ({ error: 'not_found' }),
380
- deleteTaskCodexRuns: () => {},
381
- getPromptxCodexSessionById: () => ({ id: 'session-1' }),
382
- getRelayConfig: () => ({ allowRemoteShell: false }),
383
- getSystemConfig: () => ({
384
- remoteCommandSecurity: {
385
- mode: 'trusted-proxy',
386
- trustedProxyToken: 'trusted-token',
387
- },
388
- }),
389
- getRunningCodexRunByTaskSlug: () => null,
390
- getTaskBySlug: (slug) => ({ slug, expired: false }),
391
- getTaskGitDiffReviewInSubprocess: async () => ({}),
392
- listTaskCodexRunsWithOptions: () => [],
393
- listTaskWorkspaceDiffSummaries: () => [],
394
- listTasks: () => [],
395
- reorderTasks: () => ({ changed: false, items: [] }),
396
- purgeExpiredContent: () => {},
397
- removeAssetFiles: () => {},
398
- runDispatchService: {
399
- async startTaskRunForTask(payload = {}) {
400
- allowShellCommand = payload.allowShellCommand === true
401
- return {
402
- run: { id: 'run-shell-1', status: 'queued' },
403
- runnerDispatchPending: false,
404
- }
405
- },
406
- },
407
- updateTask: () => null,
408
- updateTaskCodexSession: () => null,
409
- })
410
- await app.ready()
411
-
412
- try {
413
- const response = await app.inject({
414
- method: 'POST',
415
- url: '/api/tasks/task-1/codex-runs',
416
- headers: {
417
- origin: 'https://dev.promptx.test',
418
- 'x-promptx-trusted-proxy': '1',
419
- 'x-promptx-proxy-token': 'trusted-token',
420
- },
421
- payload: {
422
- sessionId: 'session-1',
423
- prompt: '!pwd',
424
- promptBlocks: [{ type: 'text', content: '!pwd' }],
425
- commandMode: 'shell',
426
- },
427
- })
428
-
429
- assert.equal(response.statusCode, 201)
430
- assert.equal(allowShellCommand, true)
431
- } finally {
432
- await app.close()
433
- }
434
- })
435
-
436
- test('task routes ignore legacy relay allowRemoteShell when remote command security is disabled', async () => {
437
- const app = Fastify()
438
- registerTaskRoutes(app, {
439
- broadcastServerEvent: () => {},
440
- buildTaskExports: () => ({ raw: '' }),
441
- canEditTask: () => true,
442
- createTask: () => null,
443
- decorateTask: (task) => task,
444
- decorateTaskList: (items) => items,
445
- deleteTask: () => ({ error: 'not_found' }),
446
- deleteTaskCodexRuns: () => {},
447
- getPromptxCodexSessionById: () => ({ id: 'session-1' }),
448
- getRelayConfig: () => ({ allowRemoteShell: true }),
449
- getSystemConfig: () => ({
450
- remoteCommandSecurity: {
451
- mode: 'disabled',
452
- trustedProxyToken: '',
453
- },
454
- }),
455
- getRunningCodexRunByTaskSlug: () => null,
456
- getTaskBySlug: (slug) => ({ slug, expired: false }),
457
- getTaskGitDiffReviewInSubprocess: async () => ({}),
458
- listTaskCodexRunsWithOptions: () => [],
459
- listTaskWorkspaceDiffSummaries: () => [],
460
- listTasks: () => [],
461
- reorderTasks: () => ({ changed: false, items: [] }),
462
- purgeExpiredContent: () => {},
463
- removeAssetFiles: () => {},
464
- runDispatchService: {
465
- async startTaskRunForTask(payload = {}) {
466
- if (payload.allowShellCommand !== true) {
467
- const error = new Error('当前入口未被允许执行命令。请在设置 -> 通用 -> 远程命令安全中启用对应模式,或改为本机本地访问。')
468
- error.statusCode = 403
469
- error.messageKey = 'errors.shellLocalOnly'
470
- throw error
471
- }
472
- return {
473
- run: { id: 'run-shell-1', status: 'queued' },
474
- runnerDispatchPending: false,
475
- }
476
- },
477
- },
478
- updateTask: () => null,
479
- updateTaskCodexSession: () => null,
480
- })
481
- await app.ready()
482
-
483
- try {
484
- const response = await app.inject({
485
- method: 'POST',
486
- url: '/api/tasks/task-1/codex-runs',
487
- headers: buildInternalAuthHeaders({
488
- origin: 'https://dongdong.promptx.mushayu.com',
489
- 'x-promptx-relay-request': '1',
490
- }),
491
- payload: {
492
- sessionId: 'session-1',
493
- prompt: '!pwd',
494
- promptBlocks: [{ type: 'text', content: '!pwd' }],
495
- commandMode: 'shell',
496
- },
497
- })
498
-
499
- assert.equal(response.statusCode, 403)
500
- assert.equal(response.json().messageKey, 'errors.shellLocalOnly')
501
- } finally {
502
- await app.close()
503
- }
504
- })
505
-
506
- test('task routes block trusted proxy remote shell commands when token mismatches', async () => {
507
- const app = Fastify()
508
- registerTaskRoutes(app, {
509
- broadcastServerEvent: () => {},
510
- buildTaskExports: () => ({ raw: '' }),
511
- canEditTask: () => true,
512
- createTask: () => null,
513
- decorateTask: (task) => task,
514
- decorateTaskList: (items) => items,
515
- deleteTask: () => ({ error: 'not_found' }),
516
- deleteTaskCodexRuns: () => {},
517
- getPromptxCodexSessionById: () => ({ id: 'session-1' }),
518
- getRelayConfig: () => ({ allowRemoteShell: false }),
519
- getSystemConfig: () => ({
520
- remoteCommandSecurity: {
521
- mode: 'trusted-proxy',
522
- trustedProxyToken: 'trusted-token',
523
- },
524
- }),
525
- getRunningCodexRunByTaskSlug: () => null,
526
- getTaskBySlug: (slug) => ({ slug, expired: false }),
527
- getTaskGitDiffReviewInSubprocess: async () => ({}),
528
- listTaskCodexRunsWithOptions: () => [],
529
- listTaskWorkspaceDiffSummaries: () => [],
530
- listTasks: () => [],
531
- reorderTasks: () => ({ changed: false, items: [] }),
532
- purgeExpiredContent: () => {},
533
- removeAssetFiles: () => {},
534
- runDispatchService: {
535
- async startTaskRunForTask(payload = {}) {
536
- if (payload.allowShellCommand !== true) {
537
- const error = new Error('命令模式默认仅允许在本机本地界面中使用;如需对远程访问开放,请先到设置里显式开启。')
538
- error.statusCode = 403
539
- error.messageKey = 'errors.shellLocalOnly'
540
- throw error
541
- }
542
- return {
543
- run: { id: 'run-shell-1', status: 'queued' },
544
- runnerDispatchPending: false,
545
- }
546
- },
547
- },
548
- updateTask: () => null,
549
- updateTaskCodexSession: () => null,
550
- })
551
- await app.ready()
552
-
553
- try {
554
- const response = await app.inject({
555
- method: 'POST',
556
- url: '/api/tasks/task-1/codex-runs',
557
- headers: {
558
- origin: 'https://dev.promptx.test',
559
- 'x-promptx-trusted-proxy': '1',
560
- 'x-promptx-proxy-token': 'wrong-token',
561
- },
562
- payload: {
563
- sessionId: 'session-1',
564
- prompt: '!pwd',
565
- promptBlocks: [{ type: 'text', content: '!pwd' }],
566
- commandMode: 'shell',
567
- },
568
- })
569
-
570
- assert.equal(response.statusCode, 403)
571
- assert.equal(response.json().messageKey, 'errors.shellLocalOnly')
572
- } finally {
573
- await app.close()
574
- }
575
- })
576
-
577
- test('task routes block clearing runs while task is active', async () => {
578
- const app = Fastify()
579
- registerTaskRoutes(app, {
580
- broadcastServerEvent: () => {},
581
- buildTaskExports: () => ({ raw: '' }),
582
- canEditTask: () => true,
583
- createTask: () => null,
584
- decorateTask: (task) => task,
585
- decorateTaskList: (items) => items,
586
- deleteTask: () => ({ error: 'not_found' }),
587
- deleteTaskCodexRuns: () => {},
588
- getPromptxCodexSessionById: () => null,
589
- getRunningCodexRunByTaskSlug: () => ({ id: 'run-1', status: 'running' }),
590
- getTaskBySlug: (slug) => ({ slug, expired: false }),
591
- getTaskGitDiffReviewInSubprocess: async () => ({}),
592
- listTaskCodexRunsWithOptions: () => [],
593
- listTaskWorkspaceDiffSummaries: () => [],
594
- listTasks: () => [],
595
- reorderTasks: () => ({ changed: false, items: [] }),
596
- purgeExpiredContent: () => {},
597
- removeAssetFiles: () => {},
598
- runDispatchService: {
599
- async startTaskRunForTask() {
600
- return null
601
- },
602
- },
603
- updateTask: () => null,
604
- updateTaskCodexSession: () => null,
605
- })
606
- await app.ready()
607
-
608
- try {
609
- const response = await app.inject({
610
- method: 'DELETE',
611
- url: '/api/tasks/task-1/codex-runs',
612
- })
613
-
614
- assert.equal(response.statusCode, 409)
615
- assert.match(response.json().message, /正在执行/)
616
- } finally {
617
- await app.close()
618
- }
619
- })
620
-
621
- test('task routes reorder tasks and broadcast list change', async () => {
622
- const broadcasts = []
623
- const app = Fastify()
624
- registerTaskRoutes(app, {
625
- broadcastServerEvent: (type, payload) => broadcasts.push({ type, payload }),
626
- buildTaskExports: () => ({ raw: '' }),
627
- canEditTask: () => true,
628
- createTask: () => null,
629
- decorateTask: (task) => task,
630
- decorateTaskList: (items) => items,
631
- deleteTask: () => ({ error: 'not_found' }),
632
- deleteTaskCodexRuns: () => {},
633
- getPromptxCodexSessionById: () => null,
634
- getRunningCodexRunByTaskSlug: () => null,
635
- getTaskBySlug: (slug) => ({ slug, expired: false }),
636
- getTaskGitDiffReviewInSubprocess: async () => ({}),
637
- listTaskCodexRunsWithOptions: () => [],
638
- listTaskWorkspaceDiffSummaries: () => [],
639
- listTasks: () => [],
640
- reorderTasks: (slugs) => ({
641
- changed: true,
642
- items: slugs.map((slug) => ({ slug })),
643
- }),
644
- purgeExpiredContent: () => {},
645
- removeAssetFiles: () => {},
646
- runDispatchService: {
647
- async startTaskRunForTask() {
648
- return null
649
- },
650
- },
651
- updateTask: () => null,
652
- updateTaskCodexSession: () => null,
653
- })
654
- await app.ready()
655
-
656
- try {
657
- const response = await app.inject({
658
- method: 'POST',
659
- url: '/api/tasks/reorder',
660
- payload: {
661
- slugs: ['task-b', 'task-a'],
662
- },
663
- })
664
-
665
- assert.equal(response.statusCode, 200)
666
- assert.deepEqual(response.json().items, [{ slug: 'task-b' }, { slug: 'task-a' }])
667
- assert.deepEqual(broadcasts, [{
668
- type: 'tasks.changed',
669
- payload: { reason: 'reordered' },
670
- }])
671
- } finally {
672
- await app.close()
673
- }
674
- })
675
-
676
- test('task routes reject invalid reorder payload', async () => {
677
- const app = Fastify()
678
- registerTaskRoutes(app, {
679
- broadcastServerEvent: () => {},
680
- buildTaskExports: () => ({ raw: '' }),
681
- canEditTask: () => true,
682
- createTask: () => null,
683
- decorateTask: (task) => task,
684
- decorateTaskList: (items) => items,
685
- deleteTask: () => ({ error: 'not_found' }),
686
- deleteTaskCodexRuns: () => {},
687
- getPromptxCodexSessionById: () => null,
688
- getRunningCodexRunByTaskSlug: () => null,
689
- getTaskBySlug: (slug) => ({ slug, expired: false }),
690
- getTaskGitDiffReviewInSubprocess: async () => ({}),
691
- listTaskCodexRunsWithOptions: () => [],
692
- listTaskWorkspaceDiffSummaries: () => [],
693
- listTasks: () => [],
694
- reorderTasks: () => ({ changed: false, items: [] }),
695
- purgeExpiredContent: () => {},
696
- removeAssetFiles: () => {},
697
- runDispatchService: {
698
- async startTaskRunForTask() {
699
- return null
700
- },
701
- },
702
- updateTask: () => null,
703
- updateTaskCodexSession: () => null,
704
- })
705
- await app.ready()
706
-
707
- try {
708
- const response = await app.inject({
709
- method: 'POST',
710
- url: '/api/tasks/reorder',
711
- payload: {
712
- slugs: ['', ' '],
713
- },
714
- })
715
-
716
- assert.equal(response.statusCode, 400)
717
- assert.match(response.json().message, /排序数据无效/)
718
- } finally {
719
- await app.close()
720
- }
721
- })
722
-
723
- test('task routes serve binary diff blobs', async () => {
724
- const app = Fastify()
725
- registerTaskRoutes(app, {
726
- broadcastServerEvent: () => {},
727
- buildTaskExports: () => ({ raw: '' }),
728
- canEditTask: () => true,
729
- createTask: () => null,
730
- decorateTask: (task) => task,
731
- decorateTaskList: (items) => items,
732
- deleteTask: () => ({ error: 'not_found' }),
733
- deleteTaskCodexRuns: () => {},
734
- getPromptxCodexSessionById: () => null,
735
- getRunningCodexRunByTaskSlug: () => null,
736
- getTaskBySlug: (slug) => ({ slug, expired: false }),
737
- getTaskGitDiffBlob: () => ({
738
- supported: true,
739
- statusCode: 200,
740
- mimeType: 'image/png',
741
- size: 4,
742
- hash: 'abcd1234',
743
- body: Buffer.from([0x89, 0x50, 0x4e, 0x47]),
744
- }),
745
- getTaskGitDiffReviewInSubprocess: async () => ({}),
746
- listTaskCodexRunsWithOptions: () => [],
747
- listTaskWorkspaceDiffSummaries: () => [],
748
- listTasks: () => [],
749
- reorderTasks: () => ({ changed: false, items: [] }),
750
- purgeExpiredContent: () => {},
751
- removeAssetFiles: () => {},
752
- runDispatchService: {
753
- async startTaskRunForTask() {
754
- return null
755
- },
756
- },
757
- updateTask: () => null,
758
- updateTaskCodexSession: () => null,
759
- })
760
- await app.ready()
761
-
762
- try {
763
- const response = await app.inject({
764
- method: 'GET',
765
- url: '/api/tasks/task-1/git-diff/blob?scope=task&filePath=assets/logo.png&side=after',
766
- })
767
-
768
- assert.equal(response.statusCode, 200)
769
- assert.equal(response.headers['content-type'], 'image/png')
770
- assert.equal(response.headers['x-promptx-file-size'], '4')
771
- assert.equal(response.body.length > 0, true)
772
- } finally {
773
- await app.close()
774
- }
775
- })