@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,518 +0,0 @@
1
- import fs from 'node:fs'
2
- import os from 'node:os'
3
- import path from 'node:path'
4
- import { createRequire } from 'node:module'
5
- import test from 'node:test'
6
- import assert from 'node:assert/strict'
7
- import Database from 'better-sqlite3'
8
- import initSqlJs from 'sql.js'
9
-
10
- const require = createRequire(import.meta.url)
11
- const wasmPath = require.resolve('sql.js/dist/sql-wasm.wasm')
12
- const SQL = await initSqlJs({
13
- locateFile: () => wasmPath,
14
- })
15
-
16
- function withEnv(overrides, fn) {
17
- const previous = new Map()
18
-
19
- for (const [key, value] of Object.entries(overrides)) {
20
- previous.set(key, process.env[key])
21
- if (typeof value === 'undefined') {
22
- delete process.env[key]
23
- } else {
24
- process.env[key] = value
25
- }
26
- }
27
-
28
- return Promise.resolve()
29
- .then(fn)
30
- .finally(() => {
31
- for (const [key, value] of previous.entries()) {
32
- if (typeof value === 'undefined') {
33
- delete process.env[key]
34
- } else {
35
- process.env[key] = value
36
- }
37
- }
38
- })
39
- }
40
-
41
- async function importFreshCodexModule() {
42
- return import(`./codex.js?test=${Date.now()}-${Math.random().toString(16).slice(2)}`)
43
- }
44
-
45
- function createFakeCodexBinary(tempDir) {
46
- const scriptPath = path.join(tempDir, process.platform === 'win32' ? 'fake-codex.js' : 'fake-codex')
47
- const script = `#!/usr/bin/env node
48
- const fs = require('node:fs')
49
-
50
- const args = process.argv.slice(2)
51
- const outputIndex = args.indexOf('--output-last-message')
52
- const outputFile = outputIndex >= 0 ? args[outputIndex + 1] : ''
53
- const resumeIndex = args.indexOf('resume')
54
- const resumeTarget = resumeIndex >= 0 ? args[resumeIndex + 1] || '' : ''
55
- const threadId = resumeTarget || 'thread-new-123'
56
-
57
- let prompt = ''
58
- process.stdin.setEncoding('utf8')
59
- process.stdin.on('data', (chunk) => {
60
- prompt += chunk
61
- })
62
- process.stdin.on('end', () => {
63
- if (prompt.includes('fail-case')) {
64
- process.stderr.write('mocked codex failure\\n')
65
- process.exit(2)
66
- return
67
- }
68
-
69
- if (prompt.includes('disconnect-case')) {
70
- process.stdout.write(JSON.stringify({
71
- type: 'error',
72
- message: 'Reconnecting... 1/5 (stream disconnected before completion: error sending request for url (https://api.codexzh.com/v1/responses))',
73
- }) + '\\n')
74
- process.stdout.write(JSON.stringify({
75
- type: 'turn.failed',
76
- error: {
77
- message: 'stream disconnected before completion: error sending request for url (https://api.codexzh.com/v1/responses)',
78
- },
79
- }) + '\\n')
80
- process.stderr.write('Warning: no last agent message; wrote empty content to tmp-file\\n')
81
- process.exit(1)
82
- return
83
- }
84
-
85
- if (prompt.includes('mojibake-case')) {
86
- const garbled = '鑾峰彇娴嬭瘯鏁版嵁'
87
- if (outputFile) {
88
- fs.writeFileSync(outputFile, garbled)
89
- }
90
- process.stdout.write(JSON.stringify({ type: 'thread.started', thread_id: threadId }) + '\\n')
91
- process.stdout.write(JSON.stringify({
92
- type: 'item.completed',
93
- item: {
94
- type: 'command_execution',
95
- command: 'Get-Content demo.txt',
96
- aggregated_output: garbled,
97
- exit_code: 0,
98
- status: 'completed',
99
- },
100
- }) + '\\n')
101
- return
102
- }
103
-
104
- if (prompt.includes('args-case')) {
105
- if (outputFile) {
106
- fs.writeFileSync(outputFile, JSON.stringify(args))
107
- }
108
- process.stdout.write(JSON.stringify({ type: 'thread.started', thread_id: threadId }) + '\\n')
109
- process.stdout.write(JSON.stringify({
110
- type: 'item.completed',
111
- item: {
112
- type: 'agent_message',
113
- text: 'args-ok',
114
- },
115
- }) + '\\n')
116
- return
117
- }
118
-
119
- process.stdout.write(JSON.stringify({ type: 'thread.started', thread_id: threadId }) + '\\n')
120
-
121
- if (prompt.includes('stream-tail-case')) {
122
- process.stdout.write(JSON.stringify({
123
- type: 'item.completed',
124
- item: {
125
- type: 'agent_message',
126
- text: 'stream tail message',
127
- },
128
- }))
129
- return
130
- }
131
-
132
- if (outputFile) {
133
- fs.writeFileSync(outputFile, \`thread:\${threadId}\\nprompt:\${prompt.trim()}\\ncwd:\${process.cwd()}\\n\`)
134
- }
135
-
136
- process.stdout.write(JSON.stringify({
137
- type: 'item.completed',
138
- item: {
139
- type: 'agent_message',
140
- text: 'ok',
141
- },
142
- }) + '\\n')
143
- })
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-codex.cmd')
153
- fs.writeFileSync(cmdPath, '@echo off\r\nnode "%~dp0fake-codex.js" %*\r\n')
154
- return cmdPath
155
- }
156
-
157
- function createWindowsCodexCommand(tempDir) {
158
- const scriptPath = path.join(tempDir, 'fake-codex.js')
159
- const cmdPath = path.join(tempDir, 'codex.cmd')
160
- const script = `const fs = require('node:fs')
161
- const args = process.argv.slice(2)
162
- const outputIndex = args.indexOf('--output-last-message')
163
- const outputFile = outputIndex >= 0 ? args[outputIndex + 1] : ''
164
- let prompt = ''
165
- process.stdin.setEncoding('utf8')
166
- process.stdin.on('data', (chunk) => {
167
- prompt += chunk
168
- })
169
- process.stdin.on('end', () => {
170
- process.stdout.write(JSON.stringify({ type: 'thread.started', thread_id: 'thread-win' }) + '\\n')
171
- if (outputFile) {
172
- fs.writeFileSync(outputFile, \`prompt:\${prompt.trim()}\\n\`)
173
- }
174
- process.stdout.write(JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'ok' } }) + '\\n')
175
- })
176
- `
177
-
178
- fs.writeFileSync(scriptPath, script)
179
- fs.writeFileSync(cmdPath, '@echo off\r\nnode "%~dp0fake-codex.js" %*\r\n')
180
- return cmdPath
181
- }
182
-
183
- function writeThreadsDb(tempHome, rows = []) {
184
- const dbPath = path.join(tempHome, 'state_5.sqlite')
185
- const db = new SQL.Database()
186
-
187
- try {
188
- db.run(`
189
- CREATE TABLE threads (
190
- id TEXT NOT NULL,
191
- cwd TEXT,
192
- title TEXT,
193
- updated_at INTEGER
194
- );
195
- `)
196
-
197
- const statement = db.prepare('INSERT INTO threads (id, cwd, title, updated_at) VALUES (?, ?, ?, ?)')
198
-
199
- try {
200
- for (const row of rows) {
201
- statement.run([
202
- row.id,
203
- row.cwd || '',
204
- row.title || '',
205
- Number(row.updated_at || 0),
206
- ])
207
- }
208
- } finally {
209
- statement.free()
210
- }
211
-
212
- fs.writeFileSync(dbPath, Buffer.from(db.export()))
213
- } finally {
214
- db.close()
215
- }
216
- }
217
-
218
- function getSessionCwd() {
219
- return process.cwd()
220
- }
221
-
222
- test('listKnownCodexWorkspaces dedupes cwd values', async () => {
223
- const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-codex-home-'))
224
- writeThreadsDb(tempHome, [
225
- { id: 'thread-1', cwd: 'D:\\code\\yuyang-web', title: 'A', updated_at: 10 },
226
- { id: 'thread-2', cwd: 'D:\\code\\promptx', title: 'B', updated_at: 9 },
227
- { id: 'thread-3', cwd: 'D:\\code\\yuyang-web', title: 'C', updated_at: 8 },
228
- ])
229
-
230
- await withEnv(
231
- {
232
- CODEX_HOME: tempHome,
233
- CODEX_BIN: undefined,
234
- },
235
- async () => {
236
- const { listKnownCodexWorkspaces } = await importFreshCodexModule()
237
- assert.deepEqual(listKnownCodexWorkspaces(), ['D:\\code\\yuyang-web', 'D:\\code\\promptx'])
238
- }
239
- )
240
- })
241
-
242
- test('listKnownCodexSessions reads latest rows from WAL-backed sqlite', async () => {
243
- const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-codex-wal-'))
244
- const dbPath = path.join(tempHome, 'state_5.sqlite')
245
- const writer = new Database(dbPath)
246
-
247
- try {
248
- writer.pragma('journal_mode = WAL')
249
- writer.exec(`
250
- CREATE TABLE threads (
251
- id TEXT NOT NULL,
252
- cwd TEXT,
253
- title TEXT,
254
- updated_at INTEGER
255
- );
256
- `)
257
-
258
- const insert = writer.prepare('INSERT INTO threads (id, cwd, title, updated_at) VALUES (?, ?, ?, ?)')
259
- insert.run('thread-old', '/tmp/demo', 'old', 100)
260
- insert.run('thread-new-1', '/tmp/demo', 'new-1', 300)
261
- insert.run('thread-new-2', '/tmp/demo', 'new-2', 200)
262
-
263
- await withEnv(
264
- {
265
- CODEX_HOME: tempHome,
266
- CODEX_BIN: undefined,
267
- },
268
- async () => {
269
- const { listKnownCodexSessions } = await importFreshCodexModule()
270
- const items = listKnownCodexSessions({ cwd: '/tmp/demo', limit: 3 })
271
- assert.deepEqual(
272
- items.map((item) => item.id),
273
- ['thread-new-1', 'thread-new-2', 'thread-old']
274
- )
275
- }
276
- )
277
- } finally {
278
- writer.close()
279
- }
280
- })
281
-
282
- test('streamPromptToCodexSession handles a tail event without newline', async () => {
283
- const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-codex-stream-'))
284
- const fakeBin = createFakeCodexBinary(tempHome)
285
-
286
- await withEnv(
287
- {
288
- CODEX_HOME: tempHome,
289
- CODEX_BIN: fakeBin,
290
- },
291
- async () => {
292
- const { streamPromptToCodexSession } = await importFreshCodexModule()
293
- const events = []
294
- const seenThreadIds = []
295
- const stream = streamPromptToCodexSession(
296
- { id: 'session-xyz', cwd: getSessionCwd() },
297
- 'stream-tail-case',
298
- {
299
- onEvent(event) {
300
- events.push(event)
301
- },
302
- onThreadStarted(threadId) {
303
- seenThreadIds.push(threadId)
304
- },
305
- }
306
- )
307
-
308
- const result = await stream.result
309
-
310
- assert.equal(result.sessionId, 'session-xyz')
311
- assert.equal(result.message, '')
312
- assert.equal(result.threadId, 'thread-new-123')
313
- assert.deepEqual(seenThreadIds, ['thread-new-123'])
314
- assert.deepEqual(
315
- events.filter((event) => event.type === 'agent_event').map((event) => event.event.type),
316
- ['thread.started', 'item.completed']
317
- )
318
- assert.equal(
319
- events.find((event) => event.type === 'agent_event' && event.event.type === 'item.completed')?.event?.item?.text,
320
- 'stream tail message'
321
- )
322
- assert.equal(events.at(-1)?.type, 'completed')
323
- }
324
- )
325
- })
326
-
327
- test('streamPromptToCodexSession emits starting status for new sessions and resuming status for existing threads', async () => {
328
- const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-codex-status-'))
329
- const fakeBin = createFakeCodexBinary(tempHome)
330
-
331
- await withEnv(
332
- {
333
- CODEX_HOME: tempHome,
334
- CODEX_BIN: fakeBin,
335
- },
336
- async () => {
337
- const { streamPromptToCodexSession } = await importFreshCodexModule()
338
-
339
- const startingEvents = []
340
- const startingStream = streamPromptToCodexSession(
341
- { id: 'session-new', cwd: getSessionCwd(), codexThreadId: '' },
342
- 'hello start',
343
- {
344
- onEvent(event) {
345
- startingEvents.push(event)
346
- },
347
- }
348
- )
349
- await startingStream.result
350
-
351
- const resumingEvents = []
352
- const resumingStream = streamPromptToCodexSession(
353
- { id: 'session-old', cwd: getSessionCwd(), codexThreadId: 'thread-existing-1' },
354
- 'hello resume',
355
- {
356
- onEvent(event) {
357
- resumingEvents.push(event)
358
- },
359
- }
360
- )
361
- await resumingStream.result
362
-
363
- assert.deepEqual(
364
- startingEvents.find((event) => event.type === 'status'),
365
- {
366
- type: 'status',
367
- stage: 'starting',
368
- message: '已创建 PromptX 项目,正在启动第一轮执行。',
369
- }
370
- )
371
-
372
- assert.deepEqual(
373
- resumingEvents.find((event) => event.type === 'status'),
374
- {
375
- type: 'status',
376
- stage: 'resuming',
377
- message: '已连接 PromptX 项目,正在继续这轮执行。',
378
- }
379
- )
380
- }
381
- )
382
- })
383
-
384
- test('streamPromptToCodexSession includes full-access and repo-check bypass args by default', async () => {
385
- const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-codex-args-'))
386
- const fakeBin = createFakeCodexBinary(tempHome)
387
-
388
- await withEnv(
389
- {
390
- CODEX_HOME: tempHome,
391
- CODEX_BIN: fakeBin,
392
- },
393
- async () => {
394
- const { streamPromptToCodexSession } = await importFreshCodexModule()
395
- const stream = streamPromptToCodexSession(
396
- { id: 'session-args', cwd: getSessionCwd(), codexThreadId: '' },
397
- 'args-case'
398
- )
399
-
400
- const result = await stream.result
401
- const args = JSON.parse(result.message)
402
-
403
- assert.deepEqual(
404
- args.slice(0, 5),
405
- ['exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '-C', getSessionCwd()]
406
- )
407
- assert.equal(args.at(-2), '--output-last-message')
408
- }
409
- )
410
- })
411
-
412
- test('streamPromptToCodexSession surfaces stderr failures', async () => {
413
- const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-codex-fail-'))
414
- const fakeBin = createFakeCodexBinary(tempHome)
415
-
416
- await withEnv(
417
- {
418
- CODEX_HOME: tempHome,
419
- CODEX_BIN: fakeBin,
420
- },
421
- async () => {
422
- const { streamPromptToCodexSession } = await importFreshCodexModule()
423
- const stream = streamPromptToCodexSession(
424
- { id: 'session-xyz', cwd: getSessionCwd() },
425
- 'fail-case'
426
- )
427
-
428
- await assert.rejects(() => stream.result, /mocked codex failure/)
429
- }
430
- )
431
- })
432
-
433
- test('streamPromptToCodexSession prefers structured codex errors over trailing stderr warnings', async () => {
434
- const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-codex-disconnect-'))
435
- const fakeBin = createFakeCodexBinary(tempHome)
436
-
437
- await withEnv(
438
- {
439
- CODEX_HOME: tempHome,
440
- CODEX_BIN: fakeBin,
441
- },
442
- async () => {
443
- const { streamPromptToCodexSession } = await importFreshCodexModule()
444
- const stream = streamPromptToCodexSession(
445
- { id: 'session-xyz', cwd: getSessionCwd() },
446
- 'disconnect-case'
447
- )
448
-
449
- await assert.rejects(
450
- () => stream.result,
451
- /stream disconnected before completion: error sending request/
452
- )
453
- }
454
- )
455
- })
456
-
457
- test('streamPromptToCodexSession repairs garbled command output', async () => {
458
- const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-codex-mojibake-'))
459
- const fakeBin = createFakeCodexBinary(tempHome)
460
-
461
- await withEnv(
462
- {
463
- CODEX_HOME: tempHome,
464
- CODEX_BIN: fakeBin,
465
- },
466
- async () => {
467
- const { streamPromptToCodexSession } = await importFreshCodexModule()
468
- const events = []
469
- const stream = streamPromptToCodexSession(
470
- { id: 'session-xyz', cwd: getSessionCwd() },
471
- 'mojibake-case',
472
- {
473
- onEvent(event) {
474
- events.push(event)
475
- },
476
- }
477
- )
478
-
479
- const streamResult = await stream.result
480
-
481
- assert.equal(streamResult.message, '获取测试数据')
482
- assert.equal(
483
- events.find((event) => event.type === 'agent_event' && event.event.type === 'item.completed')?.event?.item?.aggregated_output,
484
- '获取测试数据'
485
- )
486
- }
487
- )
488
- })
489
-
490
- test('Windows resolves codex.cmd when CODEX_BIN is omitted', async (t) => {
491
- if (process.platform !== 'win32') {
492
- t.skip('Windows-only validation')
493
- return
494
- }
495
-
496
- const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-codex-win-'))
497
- createWindowsCodexCommand(tempHome)
498
-
499
- await withEnv(
500
- {
501
- CODEX_HOME: tempHome,
502
- CODEX_BIN: undefined,
503
- PATH: `${tempHome};${process.env.PATH || ''}`,
504
- },
505
- async () => {
506
- const { streamPromptToCodexSession } = await importFreshCodexModule()
507
- const stream = streamPromptToCodexSession(
508
- { id: 'session-win', cwd: 'D:\\code\\promptx' },
509
- 'hello from windows'
510
- )
511
- const result = await stream.result
512
-
513
- assert.equal(result.sessionId, 'session-win')
514
- assert.equal(result.threadId, 'thread-win')
515
- assert.match(result.message, /prompt:hello from windows/)
516
- }
517
- )
518
- })