@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,542 +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 { execFileSync } from 'node:child_process'
6
- import test from 'node:test'
7
-
8
- function git(cwd, args = []) {
9
- return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8' }).trim()
10
- }
11
-
12
- test('git diff review returns task and run scoped file changes for git workspaces', async () => {
13
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-git-diff-'))
14
- const repoDir = path.join(tempDir, 'repo')
15
- fs.mkdirSync(repoDir, { recursive: true })
16
-
17
- git(repoDir, ['init'])
18
- git(repoDir, ['config', 'user.email', 'promptx@example.com'])
19
- git(repoDir, ['config', 'user.name', 'PromptX'])
20
-
21
- fs.writeFileSync(path.join(repoDir, 'tracked.txt'), 'base\n')
22
- git(repoDir, ['add', 'tracked.txt'])
23
- git(repoDir, ['commit', '-m', 'init'])
24
- const branchName = git(repoDir, ['symbolic-ref', '--short', 'HEAD'])
25
-
26
- const originalCwd = process.cwd()
27
- const originalDataDir = process.env.PROMPTX_DATA_DIR
28
- const dataDir = path.join(tempDir, 'data')
29
- fs.mkdirSync(dataDir, { recursive: true })
30
- process.chdir(tempDir)
31
- process.env.PROMPTX_DATA_DIR = dataDir
32
-
33
- try {
34
- const { run } = await import('./db.js')
35
- const {
36
- __getGitDiffCacheMetricsForTest,
37
- __resetGitDiffCachesForTest,
38
- captureRunGitBaseline,
39
- captureRunGitFinalSnapshot,
40
- captureTaskGitBaseline,
41
- getGitDiffCacheDebugSnapshot,
42
- getTaskGitDiffReview,
43
- getWorkspaceGitDiffReviewByCwd,
44
- getWorkspaceGitDiffStatusSummaryByCwd,
45
- } = await import(`./gitDiff.js?test=${Date.now()}`)
46
-
47
- const now = new Date().toISOString()
48
- run(
49
- `INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
50
- VALUES (?, ?, '', '', '', ?, 'private', NULL, ?, ?)`,
51
- ['task-1', 'token-1', 'session-1', now, now]
52
- )
53
- run(
54
- `INSERT INTO codex_sessions (id, title, cwd, codex_thread_id, created_at, updated_at)
55
- VALUES (?, ?, ?, '', ?, ?)`,
56
- ['session-1', 'Repo Session', repoDir, now, now]
57
- )
58
- run(
59
- `INSERT INTO codex_runs (id, task_slug, session_id, prompt, status, response_message, error_message, created_at, updated_at, started_at, finished_at)
60
- VALUES (?, ?, ?, '', 'running', '', '', ?, ?, ?, NULL)`,
61
- ['run-1', 'task-1', 'session-1', now, now, now]
62
- )
63
-
64
- captureTaskGitBaseline('task-1', repoDir)
65
- captureRunGitBaseline('run-1', repoDir)
66
-
67
- fs.writeFileSync(path.join(repoDir, 'tracked.txt'), 'after\n')
68
- fs.writeFileSync(path.join(repoDir, 'new-file.txt'), 'hello\n')
69
- captureRunGitFinalSnapshot('run-1', repoDir)
70
-
71
- const workspaceDiff = getTaskGitDiffReview('task-1', { scope: 'workspace' })
72
- const workspaceDiffFast = getTaskGitDiffReview('task-1', { scope: 'workspace', includeStats: false })
73
- const workspaceDiffByCwd = getWorkspaceGitDiffReviewByCwd(repoDir)
74
- const workspaceStatusSummary = getWorkspaceGitDiffStatusSummaryByCwd(repoDir)
75
- const taskDiffFast = getTaskGitDiffReview('task-1', { scope: 'task', includeStats: false })
76
- const taskDiffSummaryOnly = getTaskGitDiffReview('task-1', {
77
- scope: 'task',
78
- includeFiles: false,
79
- includeStats: true,
80
- })
81
- const taskDiff = getTaskGitDiffReview('task-1', { scope: 'task' })
82
- const taskTrackedDetail = getTaskGitDiffReview('task-1', { scope: 'task', filePath: 'tracked.txt' })
83
- const taskNewFileDetail = getTaskGitDiffReview('task-1', { scope: 'task', filePath: 'new-file.txt' })
84
- const runDiff = getTaskGitDiffReview('task-1', { scope: 'run', runId: 'run-1' })
85
-
86
- assert.equal(workspaceDiff.supported, true)
87
- assert.equal(workspaceDiff.branch, branchName)
88
- assert.deepEqual(workspaceDiff.summary, { fileCount: 2, additions: 2, deletions: 1, statsComplete: true })
89
- assert.deepEqual(workspaceDiff.files.map((file) => `${file.status}:${file.path}`), ['A:new-file.txt', 'M:tracked.txt'])
90
- assert.equal(workspaceDiffFast.supported, true)
91
- assert.deepEqual(workspaceDiffFast.summary, {
92
- fileCount: 2,
93
- additions: null,
94
- deletions: null,
95
- statsComplete: false,
96
- })
97
- assert.deepEqual(workspaceDiffFast.files.map((file) => `${file.status}:${file.path}`), ['A:new-file.txt', 'M:tracked.txt'])
98
- assert.equal(workspaceDiffFast.files.find((file) => file.path === 'tracked.txt')?.statsLoaded, false)
99
- assert.equal(workspaceDiffFast.files.find((file) => file.path === 'tracked.txt')?.patchLoaded, false)
100
- assert.deepEqual(workspaceDiffByCwd.summary, workspaceDiff.summary)
101
- assert.deepEqual(workspaceDiffByCwd.files.map((file) => `${file.status}:${file.path}`), workspaceDiff.files.map((file) => `${file.status}:${file.path}`))
102
- assert.equal(workspaceStatusSummary.supported, true)
103
- assert.equal(workspaceStatusSummary.branch, branchName)
104
- assert.deepEqual(workspaceStatusSummary.summary, {
105
- fileCount: 2,
106
- additions: 0,
107
- deletions: 0,
108
- statsComplete: false,
109
- })
110
- assert.deepEqual(workspaceStatusSummary.files, [])
111
-
112
- assert.equal(taskDiff.supported, true)
113
- assert.equal(taskDiff.branch, branchName)
114
- assert.equal(taskDiff.summary.fileCount, 2)
115
- assert.deepEqual(taskDiff.summary, { fileCount: 2, additions: 2, deletions: 1, statsComplete: true })
116
- assert.deepEqual(taskDiff.files.map((file) => `${file.status}:${file.path}`), ['A:new-file.txt', 'M:tracked.txt'])
117
- assert.equal(taskDiff.baseline?.branch, branchName)
118
- assert.equal(taskDiff.baseline?.headShort, taskDiff.baseline?.headOid.slice(0, 7))
119
- assert.deepEqual(taskDiff.warnings, [])
120
- assert.equal(taskDiff.files.find((file) => file.path === 'tracked.txt')?.patchLoaded, false)
121
- assert.equal(taskDiffFast.summary.statsComplete, false)
122
- assert.equal(taskDiffFast.summary.fileCount, 2)
123
- assert.equal(taskDiffFast.files.find((file) => file.path === 'tracked.txt')?.statsLoaded, false)
124
- assert.equal(taskDiffFast.files.find((file) => file.path === 'tracked.txt')?.additions, null)
125
- assert.deepEqual(taskDiffSummaryOnly.files, [])
126
- assert.equal(taskDiffSummaryOnly.summary.statsComplete, true)
127
- assert.deepEqual(taskDiffSummaryOnly.summary, {
128
- fileCount: 2,
129
- additions: 2,
130
- deletions: 1,
131
- statsComplete: true,
132
- })
133
- assert.equal(taskTrackedDetail.summary?.statsComplete, true)
134
- assert.equal(typeof taskTrackedDetail.summary?.statsComplete, 'boolean')
135
- assert.deepEqual(taskTrackedDetail.files.map((file) => file.path), ['tracked.txt'])
136
- assert.match(taskTrackedDetail.files.find((file) => file.path === 'tracked.txt')?.patch || '', /--- a\/tracked\.txt/)
137
- assert.match(taskTrackedDetail.files.find((file) => file.path === 'tracked.txt')?.patch || '', /\+\+\+ b\/tracked\.txt/)
138
- assert.deepEqual(taskNewFileDetail.files.map((file) => file.path), ['new-file.txt'])
139
- assert.match(taskNewFileDetail.files.find((file) => file.path === 'new-file.txt')?.patch || '', /hello/)
140
- assert.deepEqual(
141
- taskDiff.files.find((file) => file.path === 'tracked.txt')
142
- ? {
143
- additions: taskDiff.files.find((file) => file.path === 'tracked.txt').additions,
144
- deletions: taskDiff.files.find((file) => file.path === 'tracked.txt').deletions,
145
- }
146
- : null,
147
- { additions: 1, deletions: 1 }
148
- )
149
-
150
- __resetGitDiffCachesForTest()
151
- getTaskGitDiffReview('task-1', { scope: 'task' })
152
- const firstCacheMetrics = __getGitDiffCacheMetricsForTest()
153
- getTaskGitDiffReview('task-1', { scope: 'task' })
154
- const secondCacheMetrics = __getGitDiffCacheMetricsForTest()
155
- const cacheSnapshot = getGitDiffCacheDebugSnapshot()
156
- assert.equal(firstCacheMetrics.reviewHits, 0)
157
- assert.equal(secondCacheMetrics.reviewHits, 1)
158
- assert.equal(cacheSnapshot.reviewCacheSize >= 1, true)
159
- assert.equal(cacheSnapshot.fileCacheSize >= 1, true)
160
-
161
- assert.equal(runDiff.supported, true)
162
- assert.equal(runDiff.branch, branchName)
163
- assert.equal(runDiff.summary.fileCount, 2)
164
- assert.deepEqual(runDiff.summary, { fileCount: 2, additions: 2, deletions: 1, statsComplete: true })
165
- assert.deepEqual(runDiff.files.map((file) => `${file.status}:${file.path}`), ['A:new-file.txt', 'M:tracked.txt'])
166
-
167
- fs.writeFileSync(path.join(repoDir, 'tracked.txt'), 'after-later\n')
168
- fs.writeFileSync(path.join(repoDir, 'new-file.txt'), 'hello later\n')
169
- fs.writeFileSync(path.join(repoDir, 'later-file.txt'), 'later\n')
170
- const stableRunDiff = getTaskGitDiffReview('task-1', { scope: 'run', runId: 'run-1' })
171
- assert.deepEqual(stableRunDiff.summary, { fileCount: 2, additions: 2, deletions: 1, statsComplete: true })
172
- assert.deepEqual(stableRunDiff.files.map((file) => `${file.status}:${file.path}`), ['A:new-file.txt', 'M:tracked.txt'])
173
-
174
- fs.writeFileSync(path.join(repoDir, 'tracked.txt'), 'after\n')
175
- fs.writeFileSync(path.join(repoDir, 'new-file.txt'), 'hello\n')
176
- fs.rmSync(path.join(repoDir, 'later-file.txt'), { force: true })
177
-
178
- git(repoDir, ['add', 'tracked.txt', 'new-file.txt'])
179
- git(repoDir, ['commit', '-m', 'persist tracked and new file changes'])
180
-
181
- const committedWorkspaceDiff = getTaskGitDiffReview('task-1', { scope: 'workspace' })
182
- const committedTaskDiff = getTaskGitDiffReview('task-1', { scope: 'task' })
183
- const committedTaskTrackedDetail = getTaskGitDiffReview('task-1', { scope: 'task', filePath: 'tracked.txt' })
184
- assert.equal(committedWorkspaceDiff.supported, true)
185
- assert.deepEqual(committedWorkspaceDiff.summary, { fileCount: 0, additions: 0, deletions: 0, statsComplete: true })
186
- assert.deepEqual(committedWorkspaceDiff.files, [])
187
- assert.equal(committedTaskDiff.supported, true)
188
- assert.deepEqual(committedTaskDiff.summary, { fileCount: 2, additions: 2, deletions: 1, statsComplete: true })
189
- assert.deepEqual(committedTaskDiff.files.map((file) => `${file.status}:${file.path}`), ['A:new-file.txt', 'M:tracked.txt'])
190
- assert.equal(committedTaskDiff.files.find((file) => file.path === 'tracked.txt')?.patchLoaded, false)
191
- assert.deepEqual(committedTaskTrackedDetail.files.map((file) => file.path), ['tracked.txt'])
192
- assert.match(committedTaskTrackedDetail.files.find((file) => file.path === 'tracked.txt')?.patch || '', /--- a\/tracked\.txt/)
193
- assert.match(committedTaskTrackedDetail.files.find((file) => file.path === 'tracked.txt')?.patch || '', /\+\+\+ b\/tracked\.txt/)
194
- assert.match(committedTaskTrackedDetail.files.find((file) => file.path === 'tracked.txt')?.patch || '', /after/)
195
- } finally {
196
- process.chdir(originalCwd)
197
- if (typeof originalDataDir === 'string') {
198
- process.env.PROMPTX_DATA_DIR = originalDataDir
199
- } else {
200
- delete process.env.PROMPTX_DATA_DIR
201
- }
202
- }
203
- })
204
-
205
- test('single file diff falls back to message when patch exceeds inline line budget', async () => {
206
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-git-diff-large-patch-'))
207
- const repoDir = path.join(tempDir, 'repo')
208
- fs.mkdirSync(repoDir, { recursive: true })
209
-
210
- git(repoDir, ['init'])
211
- git(repoDir, ['config', 'user.email', 'promptx@example.com'])
212
- git(repoDir, ['config', 'user.name', 'PromptX'])
213
-
214
- const originalContent = Array.from({ length: 520 }, (_, index) => `base-${index}`).join('\n') + '\n'
215
- const nextContent = Array.from({ length: 520 }, (_, index) => `next-${index}`).join('\n') + '\n'
216
- fs.writeFileSync(path.join(repoDir, 'huge-lines.txt'), originalContent)
217
- git(repoDir, ['add', 'huge-lines.txt'])
218
- git(repoDir, ['commit', '-m', 'init'])
219
-
220
- const originalCwd = process.cwd()
221
- const originalDataDir = process.env.PROMPTX_DATA_DIR
222
- const originalMaxPatchLines = process.env.PROMPTX_GIT_DIFF_MAX_PATCH_LINES
223
- const dataDir = path.join(tempDir, 'data')
224
- fs.mkdirSync(dataDir, { recursive: true })
225
- process.chdir(tempDir)
226
- process.env.PROMPTX_DATA_DIR = dataDir
227
- process.env.PROMPTX_GIT_DIFF_MAX_PATCH_LINES = '400'
228
-
229
- try {
230
- const { run } = await import('./db.js')
231
- const {
232
- captureTaskGitBaseline,
233
- getTaskGitDiffReview,
234
- } = await import(`./gitDiff.js?test=${Date.now()}`)
235
-
236
- const now = new Date().toISOString()
237
- run(
238
- `INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
239
- VALUES (?, ?, '', '', '', ?, 'private', NULL, ?, ?)`,
240
- ['task-large-lines', 'token-large-lines', 'session-large-lines', now, now]
241
- )
242
- run(
243
- `INSERT INTO codex_sessions (id, title, cwd, codex_thread_id, created_at, updated_at)
244
- VALUES (?, ?, ?, '', ?, ?)`,
245
- ['session-large-lines', 'Repo Session', repoDir, now, now]
246
- )
247
-
248
- captureTaskGitBaseline('task-large-lines', repoDir)
249
- fs.writeFileSync(path.join(repoDir, 'huge-lines.txt'), nextContent)
250
-
251
- const detail = getTaskGitDiffReview('task-large-lines', {
252
- scope: 'workspace',
253
- filePath: 'huge-lines.txt',
254
- })
255
- const file = detail.files.find((item) => item.path === 'huge-lines.txt')
256
-
257
- assert.equal(file?.patchLoaded, true)
258
- assert.equal(file?.tooLarge, true)
259
- assert.match(file?.patch || '', /diff --git a\/huge-lines\.txt b\/huge-lines\.txt/)
260
- assert.match(file?.patch || '', /\.\.\. 已截断,剩余 \d+ 行未展示 \.\.\./)
261
- assert.equal(file?.message, 'diff 内容较长,当前仅展示摘要预览。')
262
- } finally {
263
- process.chdir(originalCwd)
264
- if (typeof originalDataDir === 'undefined') {
265
- delete process.env.PROMPTX_DATA_DIR
266
- } else {
267
- process.env.PROMPTX_DATA_DIR = originalDataDir
268
- }
269
- if (typeof originalMaxPatchLines === 'undefined') {
270
- delete process.env.PROMPTX_GIT_DIFF_MAX_PATCH_LINES
271
- } else {
272
- process.env.PROMPTX_GIT_DIFF_MAX_PATCH_LINES = originalMaxPatchLines
273
- }
274
- fs.rmSync(tempDir, { recursive: true, force: true })
275
- }
276
- })
277
-
278
- test('binary diff review exposes preview metadata and blob payloads', async () => {
279
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-git-diff-binary-'))
280
- const repoDir = path.join(tempDir, 'repo')
281
- fs.mkdirSync(repoDir, { recursive: true })
282
-
283
- git(repoDir, ['init'])
284
- git(repoDir, ['config', 'user.email', 'promptx@example.com'])
285
- git(repoDir, ['config', 'user.name', 'PromptX'])
286
-
287
- const beforeBuffer = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x01, 0x02, 0x03])
288
- const afterBuffer = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x04, 0x05, 0x06, 0x07])
289
- const unchangedBuffer = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x09, 0x0a, 0x0b])
290
- fs.mkdirSync(path.join(repoDir, 'assets'), { recursive: true })
291
- fs.writeFileSync(path.join(repoDir, 'assets', 'logo.png'), beforeBuffer)
292
- fs.writeFileSync(path.join(repoDir, 'assets', 'unchanged.png'), unchangedBuffer)
293
- git(repoDir, ['add', 'assets/logo.png', 'assets/unchanged.png'])
294
- git(repoDir, ['commit', '-m', 'add logo'])
295
-
296
- const originalCwd = process.cwd()
297
- const originalDataDir = process.env.PROMPTX_DATA_DIR
298
- const dataDir = path.join(tempDir, 'data')
299
- fs.mkdirSync(dataDir, { recursive: true })
300
- process.chdir(tempDir)
301
- process.env.PROMPTX_DATA_DIR = dataDir
302
-
303
- try {
304
- const { run } = await import('./db.js')
305
- const {
306
- captureTaskGitBaseline,
307
- getTaskGitDiffBlob,
308
- getTaskGitDiffReview,
309
- } = await import(`./gitDiff.js?binary-test=${Date.now()}`)
310
-
311
- const now = new Date().toISOString()
312
- run(
313
- `INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
314
- VALUES (?, ?, '', '', '', ?, 'private', NULL, ?, ?)`,
315
- ['task-binary', 'token-binary', 'session-binary', now, now]
316
- )
317
- run(
318
- `INSERT INTO codex_sessions (id, title, cwd, codex_thread_id, created_at, updated_at)
319
- VALUES (?, ?, ?, '', ?, ?)`,
320
- ['session-binary', 'Binary Repo', repoDir, now, now]
321
- )
322
-
323
- captureTaskGitBaseline('task-binary', repoDir)
324
- fs.writeFileSync(path.join(repoDir, 'assets', 'logo.png'), afterBuffer)
325
-
326
- const detail = getTaskGitDiffReview('task-binary', {
327
- scope: 'task',
328
- filePath: 'assets/logo.png',
329
- })
330
- const file = detail.files[0]
331
- assert.equal(file.binary, true)
332
- assert.equal(file.binaryPreview.kind, 'image')
333
- assert.equal(file.binaryPreview.mimeType, 'image/png')
334
- assert.equal(file.binaryPreview.before.exists, true)
335
- assert.equal(file.binaryPreview.after.exists, true)
336
- assert.equal(file.binaryPreview.before.size, beforeBuffer.length)
337
- assert.equal(file.binaryPreview.after.size, afterBuffer.length)
338
-
339
- const beforePayload = getTaskGitDiffBlob('task-binary', {
340
- scope: 'task',
341
- filePath: 'assets/logo.png',
342
- side: 'before',
343
- })
344
- const afterPayload = getTaskGitDiffBlob('task-binary', {
345
- scope: 'task',
346
- filePath: 'assets/logo.png',
347
- side: 'after',
348
- })
349
- const blockedPayload = getTaskGitDiffBlob('task-binary', {
350
- scope: 'task',
351
- filePath: 'assets/unchanged.png',
352
- side: 'after',
353
- })
354
- assert.equal(beforePayload.supported, true)
355
- assert.equal(afterPayload.supported, true)
356
- assert.deepEqual(beforePayload.body, beforeBuffer)
357
- assert.deepEqual(afterPayload.body, afterBuffer)
358
- assert.equal(blockedPayload.supported, false)
359
- assert.equal(blockedPayload.messageKey, 'diffReview.fileNotInDiff')
360
- } finally {
361
- process.chdir(originalCwd)
362
- if (typeof originalDataDir === 'string') {
363
- process.env.PROMPTX_DATA_DIR = originalDataDir
364
- } else {
365
- delete process.env.PROMPTX_DATA_DIR
366
- }
367
- }
368
- })
369
-
370
- test('run scoped diff stays pinned to each round snapshot', async () => {
371
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-git-diff-rounds-'))
372
- const repoDir = path.join(tempDir, 'repo')
373
- fs.mkdirSync(repoDir, { recursive: true })
374
-
375
- git(repoDir, ['init'])
376
- git(repoDir, ['config', 'user.email', 'promptx@example.com'])
377
- git(repoDir, ['config', 'user.name', 'PromptX'])
378
-
379
- fs.writeFileSync(path.join(repoDir, 'tracked.txt'), 'base\n')
380
- git(repoDir, ['add', 'tracked.txt'])
381
- git(repoDir, ['commit', '-m', 'init'])
382
-
383
- const originalCwd = process.cwd()
384
- const originalDataDir = process.env.PROMPTX_DATA_DIR
385
- const dataDir = path.join(tempDir, 'data')
386
- fs.mkdirSync(dataDir, { recursive: true })
387
- process.chdir(tempDir)
388
- process.env.PROMPTX_DATA_DIR = dataDir
389
-
390
- try {
391
- const { run } = await import('./db.js')
392
- const {
393
- captureRunGitBaseline,
394
- captureRunGitFinalSnapshot,
395
- getTaskGitDiffReview,
396
- } = await import(`./gitDiff.js?rounds=${Date.now()}`)
397
-
398
- const now = new Date().toISOString()
399
- run(
400
- `INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
401
- VALUES (?, ?, '', '', '', ?, 'private', NULL, ?, ?)`,
402
- ['task-rounds', 'token-rounds', 'session-rounds', now, now]
403
- )
404
- run(
405
- `INSERT INTO codex_sessions (id, title, cwd, codex_thread_id, created_at, updated_at)
406
- VALUES (?, ?, ?, '', ?, ?)`,
407
- ['session-rounds', 'Repo Session', repoDir, now, now]
408
- )
409
-
410
- ;['run-round-1', 'run-round-2', 'run-round-3'].forEach((runId) => {
411
- run(
412
- `INSERT INTO codex_runs (id, task_slug, session_id, prompt, status, response_message, error_message, created_at, updated_at, started_at, finished_at)
413
- VALUES (?, ?, ?, '', 'completed', '', '', ?, ?, ?, ?)`,
414
- [runId, 'task-rounds', 'session-rounds', now, now, now, now]
415
- )
416
- })
417
-
418
- captureRunGitBaseline('run-round-1', repoDir)
419
- captureRunGitFinalSnapshot('run-round-1', repoDir)
420
-
421
- captureRunGitBaseline('run-round-2', repoDir)
422
- captureRunGitFinalSnapshot('run-round-2', repoDir)
423
-
424
- captureRunGitBaseline('run-round-3', repoDir)
425
- fs.writeFileSync(path.join(repoDir, 'tracked.txt'), 'round-3\n')
426
- fs.writeFileSync(path.join(repoDir, 'new-file.txt'), 'hello\n')
427
- captureRunGitFinalSnapshot('run-round-3', repoDir)
428
-
429
- fs.writeFileSync(path.join(repoDir, 'tracked.txt'), 'later-round\n')
430
- fs.writeFileSync(path.join(repoDir, 'new-file.txt'), 'hello later\n')
431
- fs.writeFileSync(path.join(repoDir, 'later-file.txt'), 'later\n')
432
-
433
- const run1Diff = getTaskGitDiffReview('task-rounds', { scope: 'run', runId: 'run-round-1' })
434
- const run2Diff = getTaskGitDiffReview('task-rounds', { scope: 'run', runId: 'run-round-2' })
435
- const run3Diff = getTaskGitDiffReview('task-rounds', { scope: 'run', runId: 'run-round-3' })
436
-
437
- assert.equal(run1Diff.supported, true)
438
- assert.deepEqual(run1Diff.summary, { fileCount: 0, additions: 0, deletions: 0, statsComplete: true })
439
- assert.deepEqual(run1Diff.files, [])
440
-
441
- assert.equal(run2Diff.supported, true)
442
- assert.deepEqual(run2Diff.summary, { fileCount: 0, additions: 0, deletions: 0, statsComplete: true })
443
- assert.deepEqual(run2Diff.files, [])
444
-
445
- assert.equal(run3Diff.supported, true)
446
- assert.deepEqual(run3Diff.summary, { fileCount: 2, additions: 2, deletions: 1, statsComplete: true })
447
- assert.deepEqual(run3Diff.files.map((file) => `${file.status}:${file.path}`), ['A:new-file.txt', 'M:tracked.txt'])
448
- } finally {
449
- process.chdir(originalCwd)
450
- if (typeof originalDataDir === 'string') {
451
- process.env.PROMPTX_DATA_DIR = originalDataDir
452
- } else {
453
- delete process.env.PROMPTX_DATA_DIR
454
- }
455
- }
456
- })
457
-
458
- test('submodule diff expands nested file entries and resolves nested file detail', async () => {
459
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-git-diff-submodule-'))
460
- const submoduleSourceDir = path.join(tempDir, 'submodule-source')
461
- const repoDir = path.join(tempDir, 'repo')
462
- fs.mkdirSync(submoduleSourceDir, { recursive: true })
463
- fs.mkdirSync(repoDir, { recursive: true })
464
-
465
- git(submoduleSourceDir, ['init'])
466
- git(submoduleSourceDir, ['config', 'user.email', 'promptx@example.com'])
467
- git(submoduleSourceDir, ['config', 'user.name', 'PromptX'])
468
- fs.mkdirSync(path.join(submoduleSourceDir, 'apps', 'web-antd', 'src', 'views', 'dashboard', 'import-fee'), { recursive: true })
469
- fs.writeFileSync(
470
- path.join(submoduleSourceDir, 'apps', 'web-antd', 'src', 'views', 'dashboard', 'import-fee', 'index.vue'),
471
- 'line-1\nline-2\nline-3\n',
472
- 'utf8'
473
- )
474
- git(submoduleSourceDir, ['add', '.'])
475
- git(submoduleSourceDir, ['commit', '-m', 'init submodule'])
476
-
477
- git(repoDir, ['init'])
478
- git(repoDir, ['config', 'user.email', 'promptx@example.com'])
479
- git(repoDir, ['config', 'user.name', 'PromptX'])
480
- git(repoDir, ['-c', 'protocol.file.allow=always', 'submodule', 'add', submoduleSourceDir, 'web'])
481
- git(repoDir, ['commit', '-m', 'add submodule'])
482
-
483
- const nestedFilePath = 'web/apps/web-antd/src/views/dashboard/import-fee/index.vue'
484
-
485
- const originalCwd = process.cwd()
486
- const originalDataDir = process.env.PROMPTX_DATA_DIR
487
- const dataDir = path.join(tempDir, 'data')
488
- fs.mkdirSync(dataDir, { recursive: true })
489
- process.chdir(tempDir)
490
- process.env.PROMPTX_DATA_DIR = dataDir
491
-
492
- try {
493
- const { run } = await import('./db.js')
494
- const {
495
- captureTaskGitBaseline,
496
- getTaskGitDiffReview,
497
- } = await import(`./gitDiff.js?submodule=${Date.now()}`)
498
-
499
- const now = new Date().toISOString()
500
- run(
501
- `INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
502
- VALUES (?, ?, '', '', '', ?, 'private', NULL, ?, ?)`,
503
- ['task-submodule', 'token-submodule', 'session-submodule', now, now]
504
- )
505
- run(
506
- `INSERT INTO codex_sessions (id, title, cwd, codex_thread_id, created_at, updated_at)
507
- VALUES (?, ?, ?, '', ?, ?)`,
508
- ['session-submodule', 'Repo Session', repoDir, now, now]
509
- )
510
-
511
- captureTaskGitBaseline('task-submodule', repoDir)
512
-
513
- fs.writeFileSync(
514
- path.join(repoDir, nestedFilePath),
515
- 'line-1\nline-2 changed\nline-3\nline-4 added\n',
516
- 'utf8'
517
- )
518
-
519
- const workspaceDiff = getTaskGitDiffReview('task-submodule', { scope: 'workspace' })
520
- assert.ok(workspaceDiff.files.some((file) => file.path === nestedFilePath))
521
-
522
- const nestedFile = workspaceDiff.files.find((file) => file.path === nestedFilePath)
523
- assert.ok(nestedFile)
524
- assert.match(nestedFile.patch || '', /line-2 changed/)
525
- assert.match(nestedFile.patch || '', /line-4 added/)
526
-
527
- const nestedDetail = getTaskGitDiffReview('task-submodule', {
528
- scope: 'workspace',
529
- filePath: nestedFilePath,
530
- })
531
- assert.deepEqual(nestedDetail.files.map((file) => file.path), [nestedFilePath])
532
- assert.match(nestedDetail.files[0]?.patch || '', /line-2 changed/)
533
- assert.match(nestedDetail.files[0]?.patch || '', /line-4 added/)
534
- } finally {
535
- process.chdir(originalCwd)
536
- if (typeof originalDataDir === 'string') {
537
- process.env.PROMPTX_DATA_DIR = originalDataDir
538
- } else {
539
- delete process.env.PROMPTX_DATA_DIR
540
- }
541
- }
542
- })
@@ -1,140 +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 { execFileSync } from 'node:child_process'
6
- import test from 'node:test'
7
-
8
- function git(cwd, args = []) {
9
- return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8' }).trim()
10
- }
11
-
12
- test('git diff subprocess client returns the same task diff payload', async () => {
13
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-git-diff-client-'))
14
- const repoDir = path.join(tempDir, 'repo')
15
- fs.mkdirSync(repoDir, { recursive: true })
16
-
17
- git(repoDir, ['init'])
18
- git(repoDir, ['config', 'user.email', 'promptx@example.com'])
19
- git(repoDir, ['config', 'user.name', 'PromptX'])
20
-
21
- fs.writeFileSync(path.join(repoDir, 'tracked.txt'), 'base\n')
22
- git(repoDir, ['add', 'tracked.txt'])
23
- git(repoDir, ['commit', '-m', 'init'])
24
-
25
- const originalCwd = process.cwd()
26
- const originalDataDir = process.env.PROMPTX_DATA_DIR
27
- const dataDir = path.join(tempDir, 'data')
28
- fs.mkdirSync(dataDir, { recursive: true })
29
- process.chdir(tempDir)
30
- process.env.PROMPTX_DATA_DIR = dataDir
31
-
32
- try {
33
- const { run } = await import(`./db.js?test=${Date.now()}`)
34
- const {
35
- captureTaskGitBaseline,
36
- getTaskGitDiffReview,
37
- } = await import(`./gitDiff.js?test=${Date.now()}`)
38
- const {
39
- __getGitDiffWorkerPidForTest,
40
- __getGitDiffWorkerPidsForTest,
41
- getGitDiffWorkerDiagnostics,
42
- getTaskGitDiffReviewInSubprocess,
43
- stopGitDiffWorker,
44
- } = await import(`./gitDiffClient.js?test=${Date.now()}`)
45
-
46
- const now = new Date().toISOString()
47
- run(
48
- `INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
49
- VALUES (?, ?, '', '', '', ?, 'private', NULL, ?, ?)`,
50
- ['task-client', 'token-client', 'session-client', now, now]
51
- )
52
- run(
53
- `INSERT INTO codex_sessions (id, title, cwd, codex_thread_id, created_at, updated_at)
54
- VALUES (?, ?, ?, '', ?, ?)`,
55
- ['session-client', 'Repo Session', repoDir, now, now]
56
- )
57
-
58
- captureTaskGitBaseline('task-client', repoDir)
59
-
60
- fs.writeFileSync(path.join(repoDir, 'tracked.txt'), 'after\n')
61
- fs.writeFileSync(path.join(repoDir, 'new-file.txt'), 'hello\n')
62
-
63
- const directPayload = getTaskGitDiffReview('task-client', { scope: 'task' })
64
- const workerPayload = await getTaskGitDiffReviewInSubprocess('task-client', { scope: 'task' })
65
- const firstWorkerPid = __getGitDiffWorkerPidForTest()
66
- const secondWorkerPayload = await getTaskGitDiffReviewInSubprocess('task-client', { scope: 'task', includeFiles: false, includeStats: true })
67
- const secondWorkerPid = __getGitDiffWorkerPidForTest()
68
- const diagnostics = getGitDiffWorkerDiagnostics()
69
-
70
- assert.deepEqual(workerPayload.summary, directPayload.summary)
71
- assert.deepEqual(
72
- workerPayload.files.map((file) => ({
73
- path: file.path,
74
- status: file.status,
75
- additions: file.additions,
76
- deletions: file.deletions,
77
- })),
78
- directPayload.files.map((file) => ({
79
- path: file.path,
80
- status: file.status,
81
- additions: file.additions,
82
- deletions: file.deletions,
83
- }))
84
- )
85
- assert.equal(firstWorkerPid > 0, true)
86
- assert.equal(secondWorkerPid, firstWorkerPid)
87
- assert.deepEqual(secondWorkerPayload.summary, {
88
- fileCount: 2,
89
- additions: 2,
90
- deletions: 1,
91
- statsComplete: true,
92
- })
93
- assert.equal(diagnostics.worker.running, true)
94
- assert.equal(diagnostics.worker.pid, firstWorkerPid)
95
- assert.equal(diagnostics.metrics.totalRequests >= 2, true)
96
- assert.equal(diagnostics.metrics.completedRequests >= 2, true)
97
- assert.equal(diagnostics.metrics.lastRequest?.status, 'completed')
98
- assert.equal(diagnostics.metrics.lastRequest?.scope, 'task')
99
-
100
- const concurrentResults = await Promise.all([
101
- getTaskGitDiffReviewInSubprocess('task-client', { scope: 'task' }),
102
- getTaskGitDiffReviewInSubprocess('task-client', { scope: 'task', includeFiles: false, includeStats: true }),
103
- ])
104
- const workerPids = __getGitDiffWorkerPidsForTest()
105
-
106
- assert.equal(concurrentResults.length, 2)
107
- assert.equal(workerPids.length >= 2, true)
108
-
109
- for (let index = 0; index < 80; index += 1) {
110
- fs.writeFileSync(path.join(repoDir, `stress-${index}.txt`), `stress-${index}\n`)
111
- }
112
-
113
- const [timeoutResult, successResult] = await Promise.allSettled([
114
- getTaskGitDiffReviewInSubprocess('task-client', { scope: 'task', timeoutMs: 1 }),
115
- getTaskGitDiffReviewInSubprocess('task-client', { scope: 'task' }),
116
- ])
117
- const timeoutDiagnostics = getGitDiffWorkerDiagnostics()
118
-
119
- assert.equal(timeoutResult.status, 'rejected')
120
- assert.equal(timeoutResult.reason?.code, 'GIT_DIFF_TIMEOUT')
121
- assert.equal(successResult.status, 'fulfilled')
122
- assert.equal(successResult.value?.summary?.fileCount >= 80, true)
123
- assert.equal(timeoutDiagnostics.metrics.timeoutRequests >= 1, true)
124
- assert.equal(timeoutDiagnostics.metrics.completedRequests >= 1, true)
125
-
126
- stopGitDiffWorker()
127
- } finally {
128
- process.chdir(originalCwd)
129
- if (typeof originalDataDir === 'undefined') {
130
- delete process.env.PROMPTX_DATA_DIR
131
- } else {
132
- process.env.PROMPTX_DATA_DIR = originalDataDir
133
- }
134
- try {
135
- fs.rmSync(tempDir, { recursive: true, force: true })
136
- } catch {
137
- // Ignore sqlite handle cleanup timing in test process.
138
- }
139
- }
140
- })