@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.
- package/CHANGELOG.md +11 -0
- package/apps/server/src/agentSessionDiscovery.js +180 -7
- package/apps/web/dist/assets/{CodexSessionManagerDialog-Dic9kMHK.js → CodexSessionManagerDialog-y7O-JTxP.js} +1 -1
- package/apps/web/dist/assets/{TaskDiffReviewDialog-CKiZdXqi.js → TaskDiffReviewDialog-CTr_zoAn.js} +1 -1
- package/apps/web/dist/assets/{WorkbenchSettingsDialog-CP0z90bm.js → WorkbenchSettingsDialog-Bf2DCuN_.js} +1 -1
- package/apps/web/dist/assets/{WorkbenchView-D1oxqNr4.css → WorkbenchView-CK1snPBz.css} +1 -1
- package/apps/web/dist/assets/WorkbenchView-Gq3mmtsK.js +60 -0
- package/apps/web/dist/assets/index-Co1Ssha9.js +2 -0
- package/apps/web/dist/index.html +1 -1
- package/package.json +21 -14
- package/apps/runner/src/engines/claudeCodeRunner.test.js +0 -467
- package/apps/runner/src/engines/kimiCodeRunner.test.js +0 -127
- package/apps/runner/src/engines/openCodeRunner.test.js +0 -236
- package/apps/runner/src/engines/runnerContract.test.js +0 -449
- package/apps/runner/src/engines/shellRunner.test.js +0 -46
- package/apps/runner/src/runManager.test.js +0 -913
- package/apps/runner/src/serverClient.test.js +0 -93
- package/apps/server/src/agentSessionDiscovery.test.js +0 -186
- package/apps/server/src/appPaths.test.js +0 -52
- package/apps/server/src/assetRoutes.test.js +0 -168
- package/apps/server/src/codex.test.js +0 -518
- package/apps/server/src/codexRoutes.test.js +0 -376
- package/apps/server/src/codexRuns.test.js +0 -160
- package/apps/server/src/codexSessions.test.js +0 -369
- package/apps/server/src/db.test.js +0 -182
- package/apps/server/src/gitDiff.test.js +0 -542
- package/apps/server/src/gitDiffClient.test.js +0 -140
- package/apps/server/src/internalRoutes.test.js +0 -134
- package/apps/server/src/maintenance.test.js +0 -154
- package/apps/server/src/processControl.test.js +0 -147
- package/apps/server/src/relayClient.test.js +0 -478
- package/apps/server/src/relayConfig.test.js +0 -73
- package/apps/server/src/relayProtocol.test.js +0 -49
- package/apps/server/src/relayServer.test.js +0 -798
- package/apps/server/src/relayTenants.test.js +0 -137
- package/apps/server/src/relayUsageStore.test.js +0 -65
- package/apps/server/src/repository.test.js +0 -150
- package/apps/server/src/runDispatchService.test.js +0 -563
- package/apps/server/src/runEventIngest.test.js +0 -225
- package/apps/server/src/runRecovery.test.js +0 -73
- package/apps/server/src/runnerClient.test.js +0 -80
- package/apps/server/src/runnerDispatch.test.js +0 -136
- package/apps/server/src/systemConfig.test.js +0 -112
- package/apps/server/src/systemRoutes.test.js +0 -319
- package/apps/server/src/taskRoutes.test.js +0 -775
- package/apps/server/src/upload.test.js +0 -30
- package/apps/server/src/webAppRoutes.test.js +0 -67
- package/apps/server/src/workspaceFiles.test.js +0 -279
- package/apps/web/dist/assets/WorkbenchView-noayQwj4.js +0 -60
- package/apps/web/dist/assets/index-HLkdzIYF.js +0 -2
- package/packages/shared/src/dailyLogStream.test.js +0 -29
- package/packages/shared/src/shellCommands.test.js +0 -45
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import test from 'node:test'
|
|
6
|
-
|
|
7
|
-
test('runEventIngest 会写入事件、同步 session 更新并推进 run 状态', async () => {
|
|
8
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-run-ingest-'))
|
|
9
|
-
const originalCwd = process.cwd()
|
|
10
|
-
const originalDataDir = process.env.PROMPTX_DATA_DIR
|
|
11
|
-
const dataDir = path.join(tempDir, 'data')
|
|
12
|
-
fs.mkdirSync(dataDir, { recursive: true })
|
|
13
|
-
process.chdir(tempDir)
|
|
14
|
-
process.env.PROMPTX_DATA_DIR = dataDir
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
const suffix = `test=${Date.now()}`
|
|
18
|
-
const { run } = await import(`./db.js?${suffix}`)
|
|
19
|
-
const { createRunEventIngestService } = await import(`./runEventIngest.js?${suffix}`)
|
|
20
|
-
const { getCodexRunById, listCodexRunEvents } = await import(`./codexRuns.js?${suffix}`)
|
|
21
|
-
const { getPromptxCodexSessionById } = await import(`./codexSessions.js?${suffix}`)
|
|
22
|
-
|
|
23
|
-
const now = new Date().toISOString()
|
|
24
|
-
run(
|
|
25
|
-
`INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
|
|
26
|
-
VALUES (?, ?, '', '', '', ?, 'private', NULL, ?, ?)`,
|
|
27
|
-
['task-1', 'token-1', 'session-1', now, now]
|
|
28
|
-
)
|
|
29
|
-
run(
|
|
30
|
-
`INSERT INTO codex_sessions (id, title, engine, cwd, codex_thread_id, engine_session_id, engine_thread_id, engine_meta_json, created_at, updated_at)
|
|
31
|
-
VALUES (?, ?, 'codex', ?, '', '', '', '{}', ?, ?)`,
|
|
32
|
-
['session-1', 'Session 1', tempDir, now, now]
|
|
33
|
-
)
|
|
34
|
-
run(
|
|
35
|
-
`INSERT INTO codex_sessions (id, title, engine, cwd, codex_thread_id, engine_session_id, engine_thread_id, engine_meta_json, created_at, updated_at)
|
|
36
|
-
VALUES (?, ?, 'claude-code', ?, '', 'claude-1', 'claude-1', ?, ?, ?)`,
|
|
37
|
-
['session-2', 'Session 1', tempDir, JSON.stringify({ hidden: true, projectRootId: 'session-1' }), now, now]
|
|
38
|
-
)
|
|
39
|
-
run(
|
|
40
|
-
`INSERT INTO codex_sessions (id, title, engine, cwd, codex_thread_id, engine_session_id, engine_thread_id, engine_meta_json, created_at, updated_at)
|
|
41
|
-
VALUES (?, ?, 'opencode', ?, '', 'opencode-1', 'opencode-1', ?, ?, ?)`,
|
|
42
|
-
['session-3', 'Session 1', tempDir, JSON.stringify({ hidden: true, projectRootId: 'session-1' }), now, now]
|
|
43
|
-
)
|
|
44
|
-
run(
|
|
45
|
-
`INSERT INTO codex_runs (id, task_slug, session_id, engine, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at)
|
|
46
|
-
VALUES (?, ?, ?, 'codex', ?, '[]', 'queued', '', '', ?, ?, NULL, NULL)`,
|
|
47
|
-
['run-1', 'task-1', 'session-1', 'hello', now, now]
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
const broadcasts = []
|
|
51
|
-
const ingest = createRunEventIngestService({
|
|
52
|
-
broadcastServerEvent(type, payload = {}) {
|
|
53
|
-
broadcasts.push({ type, ...payload })
|
|
54
|
-
},
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
const eventsResult = ingest.ingestEvents([
|
|
58
|
-
{
|
|
59
|
-
runId: 'run-1',
|
|
60
|
-
seq: 1,
|
|
61
|
-
type: 'session.updated',
|
|
62
|
-
ts: now,
|
|
63
|
-
payload: {
|
|
64
|
-
type: 'session.updated',
|
|
65
|
-
session: {
|
|
66
|
-
id: 'session-1',
|
|
67
|
-
codexThreadId: 'thread-1',
|
|
68
|
-
engineThreadId: 'thread-1',
|
|
69
|
-
updatedAt: now,
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
runId: 'run-1',
|
|
75
|
-
seq: 2,
|
|
76
|
-
type: 'stdout',
|
|
77
|
-
ts: now,
|
|
78
|
-
payload: {
|
|
79
|
-
type: 'stdout',
|
|
80
|
-
text: 'hello world',
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
])
|
|
84
|
-
|
|
85
|
-
assert.equal(eventsResult.ok, true)
|
|
86
|
-
assert.equal(listCodexRunEvents('run-1')?.length, 2)
|
|
87
|
-
assert.equal(getPromptxCodexSessionById('session-1')?.engineThreadId, 'thread-1')
|
|
88
|
-
assert.deepEqual(
|
|
89
|
-
listCodexRunEvents('run-1')?.[0]?.payload?.session?.agentBindings?.map((item) => item.engine),
|
|
90
|
-
['codex', 'claude-code', 'opencode']
|
|
91
|
-
)
|
|
92
|
-
assert.deepEqual(
|
|
93
|
-
broadcasts
|
|
94
|
-
.filter((item) => item.type === 'run.event' && item.runId === 'run-1')
|
|
95
|
-
.map((item) => item.event?.payload?.session?.agentBindings?.map((binding) => binding.engine))
|
|
96
|
-
.find(Boolean),
|
|
97
|
-
['codex', 'claude-code', 'opencode']
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
const runningRun = ingest.ingestStatus({
|
|
101
|
-
runId: 'run-1',
|
|
102
|
-
status: 'running',
|
|
103
|
-
startedAt: now,
|
|
104
|
-
heartbeatAt: now,
|
|
105
|
-
})
|
|
106
|
-
assert.equal(runningRun?.status, 'running')
|
|
107
|
-
|
|
108
|
-
const completedRun = ingest.ingestStatus({
|
|
109
|
-
runId: 'run-1',
|
|
110
|
-
status: 'completed',
|
|
111
|
-
responseMessage: 'done',
|
|
112
|
-
finishedAt: now,
|
|
113
|
-
heartbeatAt: now,
|
|
114
|
-
session: {
|
|
115
|
-
id: 'session-1',
|
|
116
|
-
codexThreadId: 'thread-1',
|
|
117
|
-
engineThreadId: 'thread-1',
|
|
118
|
-
updatedAt: now,
|
|
119
|
-
},
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
const storedRun = getCodexRunById('run-1')
|
|
123
|
-
assert.equal(completedRun?.status, 'completed')
|
|
124
|
-
assert.equal(storedRun?.status, 'completed')
|
|
125
|
-
assert.equal(storedRun?.responseMessage, 'done')
|
|
126
|
-
assert.equal(getPromptxCodexSessionById('session-1')?.engineThreadId, 'thread-1')
|
|
127
|
-
assert.ok(broadcasts.some((item) => item.type === 'run.event' && item.runId === 'run-1'))
|
|
128
|
-
assert.ok(broadcasts.some((item) => item.type === 'runs.changed' && item.runId === 'run-1'))
|
|
129
|
-
assert.ok(broadcasts.some((item) => item.type === 'sessions.changed' && item.sessionId === 'session-1'))
|
|
130
|
-
|
|
131
|
-
const lateHeartbeatAt = new Date(Date.now() + 1000).toISOString()
|
|
132
|
-
const staleRun = ingest.ingestStatus({
|
|
133
|
-
runId: 'run-1',
|
|
134
|
-
status: 'running',
|
|
135
|
-
responseMessage: 'should-be-ignored',
|
|
136
|
-
heartbeatAt: lateHeartbeatAt,
|
|
137
|
-
session: {
|
|
138
|
-
id: 'session-1',
|
|
139
|
-
codexThreadId: 'thread-1',
|
|
140
|
-
engineThreadId: 'thread-1',
|
|
141
|
-
updatedAt: lateHeartbeatAt,
|
|
142
|
-
},
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
const storedRunAfterLateHeartbeat = getCodexRunById('run-1')
|
|
146
|
-
assert.equal(staleRun?.status, 'completed')
|
|
147
|
-
assert.equal(storedRunAfterLateHeartbeat?.status, 'completed')
|
|
148
|
-
assert.equal(storedRunAfterLateHeartbeat?.responseMessage, 'done')
|
|
149
|
-
} finally {
|
|
150
|
-
process.chdir(originalCwd)
|
|
151
|
-
if (typeof originalDataDir === 'string') {
|
|
152
|
-
process.env.PROMPTX_DATA_DIR = originalDataDir
|
|
153
|
-
} else {
|
|
154
|
-
delete process.env.PROMPTX_DATA_DIR
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
test('runEventIngest 忽略 shell session 的身份字段补丁', async () => {
|
|
160
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-run-ingest-shell-'))
|
|
161
|
-
const originalCwd = process.cwd()
|
|
162
|
-
const originalDataDir = process.env.PROMPTX_DATA_DIR
|
|
163
|
-
const dataDir = path.join(tempDir, 'data')
|
|
164
|
-
fs.mkdirSync(dataDir, { recursive: true })
|
|
165
|
-
process.chdir(tempDir)
|
|
166
|
-
process.env.PROMPTX_DATA_DIR = dataDir
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
const suffix = `test=${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
170
|
-
const { get, run } = await import(`./db.js?${suffix}`)
|
|
171
|
-
const { createRunEventIngestService } = await import(`./runEventIngest.js?${suffix}`)
|
|
172
|
-
|
|
173
|
-
const now = new Date().toISOString()
|
|
174
|
-
const sessionId = 'session-shell-only'
|
|
175
|
-
const taskSlug = 'task-shell-only'
|
|
176
|
-
const runId = 'run-shell-only'
|
|
177
|
-
run(
|
|
178
|
-
`INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
|
|
179
|
-
VALUES (?, ?, '', '', '', ?, 'private', NULL, ?, ?)`,
|
|
180
|
-
[taskSlug, 'token-shell', sessionId, now, now]
|
|
181
|
-
)
|
|
182
|
-
run(
|
|
183
|
-
`INSERT INTO codex_sessions (id, title, engine, cwd, codex_thread_id, engine_session_id, engine_thread_id, engine_meta_json, created_at, updated_at)
|
|
184
|
-
VALUES (?, ?, 'codex', ?, ?, '', ?, '{}', ?, ?)`,
|
|
185
|
-
[sessionId, 'Session 1', tempDir, 'thread-original', 'thread-original', now, now]
|
|
186
|
-
)
|
|
187
|
-
run(
|
|
188
|
-
`INSERT INTO codex_runs (id, task_slug, session_id, engine, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at)
|
|
189
|
-
VALUES (?, ?, ?, 'shell', ?, '[]', 'queued', '', '', ?, ?, NULL, NULL)`,
|
|
190
|
-
[runId, taskSlug, sessionId, '!pwd', now, now]
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
const ingest = createRunEventIngestService()
|
|
194
|
-
ingest.ingestStatus({
|
|
195
|
-
runId,
|
|
196
|
-
status: 'completed',
|
|
197
|
-
responseMessage: '/tmp/demo',
|
|
198
|
-
finishedAt: now,
|
|
199
|
-
heartbeatAt: now,
|
|
200
|
-
session: {
|
|
201
|
-
id: sessionId,
|
|
202
|
-
engine: 'shell',
|
|
203
|
-
codexThreadId: 'thread-shell-overwrite',
|
|
204
|
-
engineThreadId: 'thread-shell-overwrite',
|
|
205
|
-
updatedAt: now,
|
|
206
|
-
},
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
const row = get(
|
|
210
|
-
`SELECT codex_thread_id, engine_thread_id
|
|
211
|
-
FROM codex_sessions
|
|
212
|
-
WHERE id = ?`,
|
|
213
|
-
[sessionId]
|
|
214
|
-
)
|
|
215
|
-
assert.equal(row?.codex_thread_id, 'thread-original')
|
|
216
|
-
assert.equal(row?.engine_thread_id, 'thread-original')
|
|
217
|
-
} finally {
|
|
218
|
-
process.chdir(originalCwd)
|
|
219
|
-
if (typeof originalDataDir === 'string') {
|
|
220
|
-
process.env.PROMPTX_DATA_DIR = originalDataDir
|
|
221
|
-
} else {
|
|
222
|
-
delete process.env.PROMPTX_DATA_DIR
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
})
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import test from 'node:test'
|
|
6
|
-
|
|
7
|
-
test('runRecovery 会回收失联的 active run,并按状态落到 error / stop_timeout', async () => {
|
|
8
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-run-recovery-'))
|
|
9
|
-
const originalCwd = process.cwd()
|
|
10
|
-
const originalDataDir = process.env.PROMPTX_DATA_DIR
|
|
11
|
-
const dataDir = path.join(tempDir, 'data')
|
|
12
|
-
fs.mkdirSync(dataDir, { recursive: true })
|
|
13
|
-
process.chdir(tempDir)
|
|
14
|
-
process.env.PROMPTX_DATA_DIR = dataDir
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
const suffix = `test=${Date.now()}`
|
|
18
|
-
const { run } = await import(`./db.js?${suffix}`)
|
|
19
|
-
const { getCodexRunById } = await import(`./codexRuns.js?${suffix}`)
|
|
20
|
-
const { createRunRecoveryService } = await import(`./runRecovery.js?${suffix}`)
|
|
21
|
-
|
|
22
|
-
const staleAt = new Date(Date.now() - 60_000).toISOString()
|
|
23
|
-
run(
|
|
24
|
-
`INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
|
|
25
|
-
VALUES (?, ?, '', '', '', ?, 'private', NULL, ?, ?)`,
|
|
26
|
-
['task-1', 'token-1', 'session-1', staleAt, staleAt]
|
|
27
|
-
)
|
|
28
|
-
run(
|
|
29
|
-
`INSERT INTO codex_sessions (id, title, engine, cwd, codex_thread_id, engine_session_id, engine_thread_id, engine_meta_json, created_at, updated_at)
|
|
30
|
-
VALUES (?, ?, 'codex', ?, '', '', '', '{}', ?, ?)`,
|
|
31
|
-
['session-1', 'Session 1', tempDir, staleAt, staleAt]
|
|
32
|
-
)
|
|
33
|
-
run(
|
|
34
|
-
`INSERT INTO codex_runs (id, task_slug, session_id, engine, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at)
|
|
35
|
-
VALUES (?, ?, ?, 'codex', ?, '[]', 'running', '', '', ?, ?, ?, NULL)`,
|
|
36
|
-
['run-1', 'task-1', 'session-1', 'hello', staleAt, staleAt, staleAt]
|
|
37
|
-
)
|
|
38
|
-
run(
|
|
39
|
-
`INSERT INTO codex_runs (id, task_slug, session_id, engine, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at)
|
|
40
|
-
VALUES (?, ?, ?, 'codex', ?, '[]', 'stopping', '', '', ?, ?, ?, NULL)`,
|
|
41
|
-
['run-2', 'task-1', 'session-1', 'hello2', staleAt, staleAt, staleAt]
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
const recovered = []
|
|
45
|
-
const recovery = createRunRecoveryService({
|
|
46
|
-
staleThresholdMs: 5000,
|
|
47
|
-
onRecoveredRun(runRecord) {
|
|
48
|
-
recovered.push(runRecord.id)
|
|
49
|
-
},
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
const sweptRuns = recovery.sweep()
|
|
53
|
-
assert.equal(sweptRuns.length, 2)
|
|
54
|
-
assert.deepEqual(new Set(recovered), new Set(['run-1', 'run-2']))
|
|
55
|
-
assert.equal(getCodexRunById('run-1')?.status, 'error')
|
|
56
|
-
assert.equal(getCodexRunById('run-2')?.status, 'stop_timeout')
|
|
57
|
-
|
|
58
|
-
const diagnostics = recovery.getDiagnostics()
|
|
59
|
-
assert.equal(diagnostics.metrics.totalSweeps, 1)
|
|
60
|
-
assert.equal(diagnostics.metrics.totalRecovered, 2)
|
|
61
|
-
assert.equal(diagnostics.metrics.totalRecoveredToError, 1)
|
|
62
|
-
assert.equal(diagnostics.metrics.totalRecoveredToStopTimeout, 1)
|
|
63
|
-
assert.deepEqual(new Set(diagnostics.metrics.lastRecoveredRunIds), new Set(['run-1', 'run-2']))
|
|
64
|
-
assert.equal(diagnostics.config.staleThresholdMs, 5000)
|
|
65
|
-
} finally {
|
|
66
|
-
process.chdir(originalCwd)
|
|
67
|
-
if (typeof originalDataDir === 'string') {
|
|
68
|
-
process.env.PROMPTX_DATA_DIR = originalDataDir
|
|
69
|
-
} else {
|
|
70
|
-
delete process.env.PROMPTX_DATA_DIR
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
})
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import http from 'node:http'
|
|
3
|
-
import test from 'node:test'
|
|
4
|
-
|
|
5
|
-
import { getInternalAuthHeaderName, getInternalAuthToken } from './internalAuth.js'
|
|
6
|
-
import { createRunnerClient } from './runnerClient.js'
|
|
7
|
-
|
|
8
|
-
function startJsonServer(handler) {
|
|
9
|
-
return new Promise((resolve) => {
|
|
10
|
-
const server = http.createServer(handler)
|
|
11
|
-
server.listen(0, '127.0.0.1', () => {
|
|
12
|
-
const address = server.address()
|
|
13
|
-
resolve({
|
|
14
|
-
server,
|
|
15
|
-
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
16
|
-
})
|
|
17
|
-
})
|
|
18
|
-
})
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
test('runnerClient attaches internal auth header and parses JSON response', async () => {
|
|
22
|
-
const { server, baseUrl } = await startJsonServer((request, response) => {
|
|
23
|
-
assert.equal(request.headers[getInternalAuthHeaderName()], getInternalAuthToken())
|
|
24
|
-
response.writeHead(200, { 'Content-Type': 'application/json' })
|
|
25
|
-
response.end(JSON.stringify({ ok: true }))
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const client = createRunnerClient({ baseUrl, timeoutMs: 1000 })
|
|
30
|
-
const payload = await client.getDiagnostics()
|
|
31
|
-
assert.deepEqual(payload, { ok: true })
|
|
32
|
-
} finally {
|
|
33
|
-
await new Promise((resolve) => server.close(resolve))
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('runnerClient fails fast on timeout', async () => {
|
|
38
|
-
const { server, baseUrl } = await startJsonServer((_request, response) => {
|
|
39
|
-
setTimeout(() => {
|
|
40
|
-
response.writeHead(200, { 'Content-Type': 'application/json' })
|
|
41
|
-
response.end(JSON.stringify({ ok: true }))
|
|
42
|
-
}, 700)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const client = createRunnerClient({ baseUrl, timeoutMs: 500 })
|
|
47
|
-
await assert.rejects(
|
|
48
|
-
() => client.getDiagnostics(),
|
|
49
|
-
(error) => error?.statusCode === 504 && /超时/.test(String(error?.message || ''))
|
|
50
|
-
)
|
|
51
|
-
} finally {
|
|
52
|
-
await new Promise((resolve) => server.close(resolve))
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('runnerClient updates runner config with internal auth header', async () => {
|
|
57
|
-
const { server, baseUrl } = await startJsonServer(async (request, response) => {
|
|
58
|
-
assert.equal(request.method, 'PUT')
|
|
59
|
-
assert.equal(request.url, '/internal/config')
|
|
60
|
-
assert.equal(request.headers[getInternalAuthHeaderName()], getInternalAuthToken())
|
|
61
|
-
|
|
62
|
-
const chunks = []
|
|
63
|
-
for await (const chunk of request) {
|
|
64
|
-
chunks.push(chunk)
|
|
65
|
-
}
|
|
66
|
-
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
67
|
-
assert.equal(body.maxConcurrentRuns, 3)
|
|
68
|
-
|
|
69
|
-
response.writeHead(200, { 'Content-Type': 'application/json' })
|
|
70
|
-
response.end(JSON.stringify({ ok: true, config: { maxConcurrentRuns: 3 } }))
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const client = createRunnerClient({ baseUrl, timeoutMs: 1000 })
|
|
75
|
-
const payload = await client.updateConfig({ maxConcurrentRuns: 3 })
|
|
76
|
-
assert.deepEqual(payload, { ok: true, config: { maxConcurrentRuns: 3 } })
|
|
77
|
-
} finally {
|
|
78
|
-
await new Promise((resolve) => server.close(resolve))
|
|
79
|
-
}
|
|
80
|
-
})
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import test from 'node:test'
|
|
6
|
-
|
|
7
|
-
const sharedTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-runner-dispatch-'))
|
|
8
|
-
const originalCwd = process.cwd()
|
|
9
|
-
const originalDataDir = process.env.PROMPTX_DATA_DIR
|
|
10
|
-
const sharedDataDir = path.join(sharedTempDir, 'data')
|
|
11
|
-
fs.mkdirSync(sharedDataDir, { recursive: true })
|
|
12
|
-
process.chdir(sharedTempDir)
|
|
13
|
-
process.env.PROMPTX_DATA_DIR = sharedDataDir
|
|
14
|
-
|
|
15
|
-
test.after(() => {
|
|
16
|
-
process.chdir(originalCwd)
|
|
17
|
-
if (typeof originalDataDir === 'string') {
|
|
18
|
-
process.env.PROMPTX_DATA_DIR = originalDataDir
|
|
19
|
-
} else {
|
|
20
|
-
delete process.env.PROMPTX_DATA_DIR
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('extractRunnerDispatchPatch prefers runner payload status and timestamps', async () => {
|
|
25
|
-
const { extractRunnerDispatchPatch } = await import(`./runnerDispatch.js?test=${Date.now()}`)
|
|
26
|
-
|
|
27
|
-
const patch = extractRunnerDispatchPatch({
|
|
28
|
-
status: 'starting',
|
|
29
|
-
startedAt: 'outer-start',
|
|
30
|
-
run: {
|
|
31
|
-
status: 'queued',
|
|
32
|
-
startedAt: 'inner-start',
|
|
33
|
-
finishedAt: 'inner-finish',
|
|
34
|
-
},
|
|
35
|
-
}, 'queued')
|
|
36
|
-
|
|
37
|
-
assert.deepEqual(patch, {
|
|
38
|
-
status: 'queued',
|
|
39
|
-
startedAt: 'inner-start',
|
|
40
|
-
finishedAt: 'inner-finish',
|
|
41
|
-
})
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test('reconcileRunAfterRunnerDispatchError syncs run state from runner lookup', async () => {
|
|
45
|
-
const suffix = `test=${Date.now()}`
|
|
46
|
-
const { run } = await import(`./db.js?${suffix}`)
|
|
47
|
-
const { getCodexRunById } = await import(`./codexRuns.js?${suffix}`)
|
|
48
|
-
const { reconcileRunAfterRunnerDispatchError } = await import(`./runnerDispatch.js?${suffix}`)
|
|
49
|
-
|
|
50
|
-
const now = new Date().toISOString()
|
|
51
|
-
run(
|
|
52
|
-
`INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
|
|
53
|
-
VALUES (?, ?, '', '', '', ?, 'private', NULL, ?, ?)`,
|
|
54
|
-
['task-1', 'token-1', 'session-1', now, now]
|
|
55
|
-
)
|
|
56
|
-
run(
|
|
57
|
-
`INSERT INTO codex_sessions (id, title, engine, cwd, codex_thread_id, engine_session_id, engine_thread_id, engine_meta_json, created_at, updated_at)
|
|
58
|
-
VALUES (?, ?, 'codex', ?, '', '', '', '{}', ?, ?)`,
|
|
59
|
-
['session-1', 'Session 1', sharedTempDir, now, now]
|
|
60
|
-
)
|
|
61
|
-
run(
|
|
62
|
-
`INSERT INTO codex_runs (id, task_slug, session_id, engine, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at)
|
|
63
|
-
VALUES (?, ?, ?, 'codex', ?, '[]', 'queued', '', '', ?, ?, NULL, NULL)`,
|
|
64
|
-
['run-1', 'task-1', 'session-1', 'hello', now, now]
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
const reconciled = await reconcileRunAfterRunnerDispatchError({
|
|
68
|
-
runId: 'run-1',
|
|
69
|
-
error: Object.assign(new Error('runner request timed out after 5000ms'), { statusCode: 504 }),
|
|
70
|
-
runnerClient: {
|
|
71
|
-
async getRun() {
|
|
72
|
-
return {
|
|
73
|
-
run: {
|
|
74
|
-
runId: 'run-1',
|
|
75
|
-
status: 'running',
|
|
76
|
-
startedAt: now,
|
|
77
|
-
},
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
fallbackStatus: 'queued',
|
|
82
|
-
logger: {
|
|
83
|
-
warn() {},
|
|
84
|
-
},
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
assert.equal(reconciled.pending, false)
|
|
88
|
-
assert.equal(reconciled.syncedFromRunner, true)
|
|
89
|
-
assert.equal(reconciled.run?.status, 'running')
|
|
90
|
-
assert.equal(getCodexRunById('run-1')?.status, 'running')
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
test('reconcileRunAfterRunnerDispatchError keeps run queued on timeout when runner lookup is unavailable', async () => {
|
|
94
|
-
const suffix = `test=${Date.now()}`
|
|
95
|
-
const { run } = await import(`./db.js?${suffix}`)
|
|
96
|
-
const { getCodexRunById } = await import(`./codexRuns.js?${suffix}`)
|
|
97
|
-
const { reconcileRunAfterRunnerDispatchError } = await import(`./runnerDispatch.js?${suffix}`)
|
|
98
|
-
|
|
99
|
-
const now = new Date().toISOString()
|
|
100
|
-
run(
|
|
101
|
-
`INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
|
|
102
|
-
VALUES (?, ?, '', '', '', ?, 'private', NULL, ?, ?)`,
|
|
103
|
-
['task-2', 'token-2', 'session-2', now, now]
|
|
104
|
-
)
|
|
105
|
-
run(
|
|
106
|
-
`INSERT INTO codex_sessions (id, title, engine, cwd, codex_thread_id, engine_session_id, engine_thread_id, engine_meta_json, created_at, updated_at)
|
|
107
|
-
VALUES (?, ?, 'codex', ?, '', '', '', '{}', ?, ?)`,
|
|
108
|
-
['session-2', 'Session 2', sharedTempDir, now, now]
|
|
109
|
-
)
|
|
110
|
-
run(
|
|
111
|
-
`INSERT INTO codex_runs (id, task_slug, session_id, engine, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at)
|
|
112
|
-
VALUES (?, ?, ?, 'codex', ?, '[]', 'queued', '', '', ?, ?, NULL, NULL)`,
|
|
113
|
-
['run-2', 'task-2', 'session-2', 'hello', now, now]
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
const reconciled = await reconcileRunAfterRunnerDispatchError({
|
|
117
|
-
runId: 'run-2',
|
|
118
|
-
error: Object.assign(new Error('runner request timed out after 5000ms'), { statusCode: 504 }),
|
|
119
|
-
runnerClient: {
|
|
120
|
-
async getRun() {
|
|
121
|
-
const lookupError = new Error('runner lookup failed')
|
|
122
|
-
lookupError.statusCode = 503
|
|
123
|
-
throw lookupError
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
fallbackStatus: 'queued',
|
|
127
|
-
logger: {
|
|
128
|
-
warn() {},
|
|
129
|
-
},
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
assert.equal(reconciled.pending, true)
|
|
133
|
-
assert.equal(reconciled.syncedFromRunner, false)
|
|
134
|
-
assert.equal(reconciled.run?.status, 'queued')
|
|
135
|
-
assert.equal(getCodexRunById('run-2')?.status, 'queued')
|
|
136
|
-
})
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import test from 'node:test'
|
|
6
|
-
|
|
7
|
-
test('system config module reads env override and stored values', async () => {
|
|
8
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-system-config-'))
|
|
9
|
-
const originalDataDir = process.env.PROMPTX_DATA_DIR
|
|
10
|
-
const originalRunnerConcurrency = process.env.PROMPTX_RUNNER_MAX_CONCURRENT_RUNS
|
|
11
|
-
process.env.PROMPTX_DATA_DIR = tempDir
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const module = await import(`./systemConfig.js?test=${Date.now()}`)
|
|
15
|
-
const saved = module.writeStoredSystemConfig({
|
|
16
|
-
runner: {
|
|
17
|
-
maxConcurrentRuns: 3,
|
|
18
|
-
},
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
assert.equal(saved.runner.maxConcurrentRuns, 3)
|
|
22
|
-
assert.equal(fs.existsSync(module.getSystemConfigPath()), true)
|
|
23
|
-
assert.equal(module.readStoredSystemConfig().runner.maxConcurrentRuns, 3)
|
|
24
|
-
assert.equal(module.getSystemConfigManagedByEnv().runner.maxConcurrentRuns, false)
|
|
25
|
-
assert.equal(module.getSystemConfigForClient().runner.maxConcurrentRuns, 3)
|
|
26
|
-
|
|
27
|
-
process.env.PROMPTX_RUNNER_MAX_CONCURRENT_RUNS = '5'
|
|
28
|
-
const moduleWithEnv = await import(`./systemConfig.js?test=${Date.now()}-env`)
|
|
29
|
-
assert.equal(moduleWithEnv.getSystemConfigManagedByEnv().runner.maxConcurrentRuns, true)
|
|
30
|
-
assert.equal(moduleWithEnv.getSystemConfigForClient().runner.maxConcurrentRuns, 5)
|
|
31
|
-
} finally {
|
|
32
|
-
if (typeof originalDataDir === 'string') {
|
|
33
|
-
process.env.PROMPTX_DATA_DIR = originalDataDir
|
|
34
|
-
} else {
|
|
35
|
-
delete process.env.PROMPTX_DATA_DIR
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (typeof originalRunnerConcurrency === 'string') {
|
|
39
|
-
process.env.PROMPTX_RUNNER_MAX_CONCURRENT_RUNS = originalRunnerConcurrency
|
|
40
|
-
} else {
|
|
41
|
-
delete process.env.PROMPTX_RUNNER_MAX_CONCURRENT_RUNS
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test('system config module redacts trusted proxy token for client payloads', async () => {
|
|
47
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-system-config-'))
|
|
48
|
-
const originalDataDir = process.env.PROMPTX_DATA_DIR
|
|
49
|
-
process.env.PROMPTX_DATA_DIR = tempDir
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
const module = await import(`./systemConfig.js?test=${Date.now()}-redact`)
|
|
53
|
-
module.writeStoredSystemConfig({
|
|
54
|
-
runner: {
|
|
55
|
-
maxConcurrentRuns: 3,
|
|
56
|
-
},
|
|
57
|
-
remoteCommandSecurity: {
|
|
58
|
-
mode: 'trusted-proxy',
|
|
59
|
-
trustedProxyToken: 'trusted-token',
|
|
60
|
-
},
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
assert.equal(module.getSystemConfigForRuntime().remoteCommandSecurity.trustedProxyToken, 'trusted-token')
|
|
64
|
-
assert.deepEqual(module.getSystemConfigForClient().remoteCommandSecurity, {
|
|
65
|
-
mode: 'trusted-proxy',
|
|
66
|
-
trustedProxyTokenConfigured: true,
|
|
67
|
-
})
|
|
68
|
-
} finally {
|
|
69
|
-
if (typeof originalDataDir === 'string') {
|
|
70
|
-
process.env.PROMPTX_DATA_DIR = originalDataDir
|
|
71
|
-
} else {
|
|
72
|
-
delete process.env.PROMPTX_DATA_DIR
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
test('system config module clears trusted proxy token when mode is not trusted-proxy', async () => {
|
|
78
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-system-config-'))
|
|
79
|
-
const originalDataDir = process.env.PROMPTX_DATA_DIR
|
|
80
|
-
process.env.PROMPTX_DATA_DIR = tempDir
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const module = await import(`./systemConfig.js?test=${Date.now()}-clear-token`)
|
|
84
|
-
module.writeStoredSystemConfig({
|
|
85
|
-
remoteCommandSecurity: {
|
|
86
|
-
mode: 'trusted-proxy',
|
|
87
|
-
trustedProxyToken: 'trusted-token',
|
|
88
|
-
},
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const saved = module.writeStoredSystemConfig({
|
|
92
|
-
remoteCommandSecurity: {
|
|
93
|
-
mode: 'relay',
|
|
94
|
-
},
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
assert.deepEqual(saved.remoteCommandSecurity, {
|
|
98
|
-
mode: 'relay',
|
|
99
|
-
trustedProxyToken: '',
|
|
100
|
-
})
|
|
101
|
-
assert.deepEqual(module.readStoredSystemConfig().remoteCommandSecurity, {
|
|
102
|
-
mode: 'relay',
|
|
103
|
-
trustedProxyToken: '',
|
|
104
|
-
})
|
|
105
|
-
} finally {
|
|
106
|
-
if (typeof originalDataDir === 'string') {
|
|
107
|
-
process.env.PROMPTX_DATA_DIR = originalDataDir
|
|
108
|
-
} else {
|
|
109
|
-
delete process.env.PROMPTX_DATA_DIR
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
})
|