@muyichengshayu/promptx 0.2.14 → 0.2.16

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 (50) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/apps/web/dist/assets/{CodexSessionManagerDialog-DD2XtFAl.js → CodexSessionManagerDialog-D1PwOD4T.js} +1 -1
  3. package/apps/web/dist/assets/{TaskDiffReviewDialog-CGLpQQ97.js → TaskDiffReviewDialog-GKKv-IkZ.js} +1 -1
  4. package/apps/web/dist/assets/{WorkbenchSettingsDialog-DxU55X3l.js → WorkbenchSettingsDialog-IWlkg3kU.js} +1 -1
  5. package/apps/web/dist/assets/{WorkbenchView-D1oxqNr4.css → WorkbenchView-CK1snPBz.css} +1 -1
  6. package/apps/web/dist/assets/WorkbenchView-dXHPTH_M.js +58 -0
  7. package/apps/web/dist/assets/{index-BLxPFky8.js → index-DaIoquOV.js} +2 -2
  8. package/apps/web/dist/index.html +1 -1
  9. package/package.json +21 -14
  10. package/apps/runner/src/engines/claudeCodeRunner.test.js +0 -467
  11. package/apps/runner/src/engines/kimiCodeRunner.test.js +0 -127
  12. package/apps/runner/src/engines/openCodeRunner.test.js +0 -236
  13. package/apps/runner/src/engines/runnerContract.test.js +0 -449
  14. package/apps/runner/src/engines/shellRunner.test.js +0 -46
  15. package/apps/runner/src/runManager.test.js +0 -913
  16. package/apps/runner/src/serverClient.test.js +0 -93
  17. package/apps/server/src/agentSessionDiscovery.test.js +0 -572
  18. package/apps/server/src/appPaths.test.js +0 -52
  19. package/apps/server/src/assetRoutes.test.js +0 -168
  20. package/apps/server/src/codex.test.js +0 -518
  21. package/apps/server/src/codexRoutes.test.js +0 -376
  22. package/apps/server/src/codexRuns.test.js +0 -160
  23. package/apps/server/src/codexSessions.test.js +0 -369
  24. package/apps/server/src/db.test.js +0 -182
  25. package/apps/server/src/gitDiff.test.js +0 -542
  26. package/apps/server/src/gitDiffClient.test.js +0 -140
  27. package/apps/server/src/internalRoutes.test.js +0 -134
  28. package/apps/server/src/maintenance.test.js +0 -154
  29. package/apps/server/src/processControl.test.js +0 -147
  30. package/apps/server/src/relayClient.test.js +0 -478
  31. package/apps/server/src/relayConfig.test.js +0 -73
  32. package/apps/server/src/relayProtocol.test.js +0 -49
  33. package/apps/server/src/relayServer.test.js +0 -798
  34. package/apps/server/src/relayTenants.test.js +0 -137
  35. package/apps/server/src/relayUsageStore.test.js +0 -65
  36. package/apps/server/src/repository.test.js +0 -150
  37. package/apps/server/src/runDispatchService.test.js +0 -563
  38. package/apps/server/src/runEventIngest.test.js +0 -225
  39. package/apps/server/src/runRecovery.test.js +0 -73
  40. package/apps/server/src/runnerClient.test.js +0 -80
  41. package/apps/server/src/runnerDispatch.test.js +0 -136
  42. package/apps/server/src/systemConfig.test.js +0 -112
  43. package/apps/server/src/systemRoutes.test.js +0 -319
  44. package/apps/server/src/taskRoutes.test.js +0 -775
  45. package/apps/server/src/upload.test.js +0 -30
  46. package/apps/server/src/webAppRoutes.test.js +0 -67
  47. package/apps/server/src/workspaceFiles.test.js +0 -279
  48. package/apps/web/dist/assets/WorkbenchView-DnrN_wUG.js +0 -60
  49. package/packages/shared/src/dailyLogStream.test.js +0 -29
  50. 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
- })