@muyichengshayu/promptx 0.2.16 → 0.2.17
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 +4 -0
- package/apps/runner/src/codex.js +114 -16
- package/apps/runner/src/engines/claudeCodeRunner.test.js +467 -0
- package/apps/runner/src/engines/kimiCodeRunner.test.js +127 -0
- package/apps/runner/src/engines/openCodeRunner.test.js +236 -0
- package/apps/runner/src/engines/runnerContract.test.js +510 -0
- package/apps/runner/src/engines/shellRunner.test.js +46 -0
- package/apps/runner/src/runManager.test.js +913 -0
- package/apps/runner/src/serverClient.test.js +93 -0
- package/apps/server/src/agentSessionDiscovery.test.js +572 -0
- package/apps/server/src/appPaths.test.js +52 -0
- package/apps/server/src/assetRoutes.test.js +168 -0
- package/apps/server/src/codex.test.js +518 -0
- package/apps/server/src/codexRoutes.test.js +376 -0
- package/apps/server/src/codexRuns.test.js +160 -0
- package/apps/server/src/codexSessions.test.js +369 -0
- package/apps/server/src/db.test.js +182 -0
- package/apps/server/src/gitDiff.test.js +542 -0
- package/apps/server/src/gitDiffClient.test.js +140 -0
- package/apps/server/src/internalRoutes.test.js +134 -0
- package/apps/server/src/maintenance.test.js +154 -0
- package/apps/server/src/processControl.test.js +147 -0
- package/apps/server/src/relayClient.test.js +478 -0
- package/apps/server/src/relayConfig.test.js +73 -0
- package/apps/server/src/relayProtocol.test.js +49 -0
- package/apps/server/src/relayServer.test.js +798 -0
- package/apps/server/src/relayTenants.test.js +137 -0
- package/apps/server/src/relayUsageStore.test.js +65 -0
- package/apps/server/src/repository.test.js +150 -0
- package/apps/server/src/runDispatchService.test.js +563 -0
- package/apps/server/src/runEventIngest.test.js +225 -0
- package/apps/server/src/runRecovery.test.js +73 -0
- package/apps/server/src/runnerClient.test.js +80 -0
- package/apps/server/src/runnerDispatch.test.js +136 -0
- package/apps/server/src/systemConfig.test.js +112 -0
- package/apps/server/src/systemRoutes.test.js +319 -0
- package/apps/server/src/taskRoutes.test.js +775 -0
- package/apps/server/src/upload.test.js +30 -0
- package/apps/server/src/webAppRoutes.test.js +67 -0
- package/apps/server/src/workspaceFiles.test.js +279 -0
- package/package.json +14 -21
- package/packages/shared/src/dailyLogStream.test.js +29 -0
- package/packages/shared/src/shellCommands.test.js +45 -0
|
@@ -0,0 +1,93 @@
|
|
|
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 { createServerClient } from './serverClient.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('serverClient attaches internal auth header and posts JSON payload', async () => {
|
|
22
|
+
const { server, baseUrl } = await startJsonServer(async (request, response) => {
|
|
23
|
+
assert.equal(request.headers[getInternalAuthHeaderName()], getInternalAuthToken())
|
|
24
|
+
|
|
25
|
+
const chunks = []
|
|
26
|
+
for await (const chunk of request) {
|
|
27
|
+
chunks.push(chunk)
|
|
28
|
+
}
|
|
29
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
30
|
+
assert.equal(body.items.length, 1)
|
|
31
|
+
|
|
32
|
+
response.writeHead(200, { 'Content-Type': 'application/json' })
|
|
33
|
+
response.end(JSON.stringify({ ok: true }))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const client = createServerClient({ baseUrl, timeoutMs: 1000 })
|
|
38
|
+
const payload = await client.postEvents([{ id: 1 }], { runnerId: 'runner-1' })
|
|
39
|
+
assert.deepEqual(payload, { ok: true })
|
|
40
|
+
} finally {
|
|
41
|
+
await new Promise((resolve) => server.close(resolve))
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('serverClient fails fast on timeout', async () => {
|
|
46
|
+
const { server, baseUrl } = await startJsonServer((_request, response) => {
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
response.writeHead(200, { 'Content-Type': 'application/json' })
|
|
49
|
+
response.end(JSON.stringify({ ok: true }))
|
|
50
|
+
}, 700)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const client = createServerClient({ baseUrl, timeoutMs: 500 })
|
|
55
|
+
await assert.rejects(
|
|
56
|
+
() => client.postStatus({ ok: true }),
|
|
57
|
+
(error) => error?.statusCode === 504 && /超时/.test(String(error?.message || ''))
|
|
58
|
+
)
|
|
59
|
+
} finally {
|
|
60
|
+
await new Promise((resolve) => server.close(resolve))
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('serverClient reads system config with internal auth header', async () => {
|
|
65
|
+
const { server, baseUrl } = await startJsonServer((request, response) => {
|
|
66
|
+
assert.equal(request.method, 'GET')
|
|
67
|
+
assert.equal(request.url, '/internal/system-config')
|
|
68
|
+
assert.equal(request.headers[getInternalAuthHeaderName()], getInternalAuthToken())
|
|
69
|
+
|
|
70
|
+
response.writeHead(200, { 'Content-Type': 'application/json' })
|
|
71
|
+
response.end(JSON.stringify({
|
|
72
|
+
config: {
|
|
73
|
+
runner: {
|
|
74
|
+
maxConcurrentRuns: 4,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const client = createServerClient({ baseUrl, timeoutMs: 1000 })
|
|
82
|
+
const payload = await client.getSystemConfig()
|
|
83
|
+
assert.deepEqual(payload, {
|
|
84
|
+
config: {
|
|
85
|
+
runner: {
|
|
86
|
+
maxConcurrentRuns: 4,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
} finally {
|
|
91
|
+
await new Promise((resolve) => server.close(resolve))
|
|
92
|
+
}
|
|
93
|
+
})
|
|
@@ -0,0 +1,572 @@
|
|
|
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
|
+
import {
|
|
8
|
+
decodeClaudeProjectPath,
|
|
9
|
+
listKnownClaudeCodeSessions,
|
|
10
|
+
listKnownKimiCodeSessions,
|
|
11
|
+
listKnownOpenCodeSessions,
|
|
12
|
+
} from './agentSessionDiscovery.js'
|
|
13
|
+
|
|
14
|
+
test('decodeClaudeProjectPath decodes unix-style project keys', () => {
|
|
15
|
+
assert.equal(
|
|
16
|
+
decodeClaudeProjectPath('-Users-bravf-code-promptx'),
|
|
17
|
+
'/Users/bravf/code/promptx'
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('listKnownClaudeCodeSessions merges transcripts and project files', () => {
|
|
22
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-discovery-'))
|
|
23
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
24
|
+
const transcriptDir = path.join(claudeHome, 'transcripts')
|
|
25
|
+
const projectsDir = path.join(claudeHome, 'projects', '-Users-bravf-code-promptx')
|
|
26
|
+
|
|
27
|
+
fs.mkdirSync(transcriptDir, { recursive: true })
|
|
28
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
29
|
+
|
|
30
|
+
const transcriptPath = path.join(transcriptDir, 'ses_transcript_only.jsonl')
|
|
31
|
+
const projectPath = path.join(projectsDir, 'ses_project.jsonl')
|
|
32
|
+
|
|
33
|
+
fs.writeFileSync(transcriptPath, `${JSON.stringify({ type: 'user', message: { text: '请继续修复 PromptX' } })}\n`)
|
|
34
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: '帮我看下项目源码' } })}\n`)
|
|
35
|
+
|
|
36
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
37
|
+
fs.utimesSync(transcriptPath, now, now)
|
|
38
|
+
fs.utimesSync(projectPath, new Date(now.getTime() + 1000), new Date(now.getTime() + 1000))
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const items = listKnownClaudeCodeSessions({
|
|
42
|
+
claudeHome,
|
|
43
|
+
limit: 10,
|
|
44
|
+
cwd: '/Users/bravf/code/promptx',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
assert.equal(items.length, 2)
|
|
48
|
+
assert.deepEqual(
|
|
49
|
+
items.map((item) => ({
|
|
50
|
+
id: item.id,
|
|
51
|
+
cwd: item.cwd,
|
|
52
|
+
matchedCwd: item.matchedCwd,
|
|
53
|
+
})),
|
|
54
|
+
[
|
|
55
|
+
{
|
|
56
|
+
id: 'ses_project',
|
|
57
|
+
cwd: '/Users/bravf/code/promptx',
|
|
58
|
+
matchedCwd: true,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'ses_transcript_only',
|
|
62
|
+
cwd: '',
|
|
63
|
+
matchedCwd: false,
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
assert.match(items[0].label, /项目源码|promptx/i)
|
|
68
|
+
} finally {
|
|
69
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('listKnownClaudeCodeSessions reads real cwd from session file when project key contains dots', () => {
|
|
74
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-dot-discovery-'))
|
|
75
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
76
|
+
const projectsDir = path.join(claudeHome, 'projects', '-Users-bravf--claude')
|
|
77
|
+
|
|
78
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
79
|
+
|
|
80
|
+
const projectPath = path.join(projectsDir, 'ses_dot.jsonl')
|
|
81
|
+
fs.writeFileSync(
|
|
82
|
+
projectPath,
|
|
83
|
+
`${JSON.stringify({ type: 'user', message: { text: '看下配置' }, cwd: '/Users/bravf/.claude' })}
|
|
84
|
+
`
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
88
|
+
fs.utimesSync(projectPath, now, now)
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const items = listKnownClaudeCodeSessions({
|
|
92
|
+
claudeHome,
|
|
93
|
+
limit: 10,
|
|
94
|
+
cwd: '/Users/bravf/.claude',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
assert.equal(items.length, 1)
|
|
98
|
+
assert.deepEqual(
|
|
99
|
+
{
|
|
100
|
+
id: items[0].id,
|
|
101
|
+
cwd: items[0].cwd,
|
|
102
|
+
matchedCwd: items[0].matchedCwd,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'ses_dot',
|
|
106
|
+
cwd: '/Users/bravf/.claude',
|
|
107
|
+
matchedCwd: true,
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
} finally {
|
|
111
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('listKnownClaudeCodeSessions reads cwd from large Claude jsonl files', () => {
|
|
116
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-large-discovery-'))
|
|
117
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
118
|
+
const projectsDir = path.join(claudeHome, 'projects', '-Users-bravf--config')
|
|
119
|
+
|
|
120
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
121
|
+
|
|
122
|
+
const projectPath = path.join(projectsDir, 'ses_large.jsonl')
|
|
123
|
+
const firstLine = `${JSON.stringify({ type: 'user', message: { text: '读取大文件历史' }, cwd: '/Users/bravf/.config' })}\n`
|
|
124
|
+
fs.writeFileSync(projectPath, `${firstLine}${'x'.repeat(300 * 1024)}\n`)
|
|
125
|
+
|
|
126
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
127
|
+
fs.utimesSync(projectPath, now, now)
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const items = listKnownClaudeCodeSessions({
|
|
131
|
+
claudeHome,
|
|
132
|
+
limit: 10,
|
|
133
|
+
cwd: '/Users/bravf/.config',
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
assert.equal(items.length, 1)
|
|
137
|
+
assert.deepEqual(
|
|
138
|
+
{
|
|
139
|
+
id: items[0].id,
|
|
140
|
+
cwd: items[0].cwd,
|
|
141
|
+
matchedCwd: items[0].matchedCwd,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'ses_large',
|
|
145
|
+
cwd: '/Users/bravf/.config',
|
|
146
|
+
matchedCwd: true,
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
assert.match(items[0].label, /读取大文件历史|config/i)
|
|
150
|
+
} finally {
|
|
151
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('listKnownClaudeCodeSessions matches encoded project key when session file has no cwd', () => {
|
|
156
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-encoded-cwd-discovery-'))
|
|
157
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
158
|
+
const projectsDir = path.join(claudeHome, 'projects', '-Users-bravf--claude')
|
|
159
|
+
|
|
160
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
161
|
+
|
|
162
|
+
const projectPath = path.join(projectsDir, 'ses_encoded_cwd.jsonl')
|
|
163
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: '继续之前的任务' } })}\n`)
|
|
164
|
+
|
|
165
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
166
|
+
fs.utimesSync(projectPath, now, now)
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const items = listKnownClaudeCodeSessions({
|
|
170
|
+
claudeHome,
|
|
171
|
+
limit: 10,
|
|
172
|
+
cwd: '/Users/bravf/.claude',
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
assert.equal(items.length, 1)
|
|
176
|
+
assert.deepEqual(
|
|
177
|
+
{
|
|
178
|
+
id: items[0].id,
|
|
179
|
+
cwd: items[0].cwd,
|
|
180
|
+
matchedCwd: items[0].matchedCwd,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: 'ses_encoded_cwd',
|
|
184
|
+
cwd: '/Users/bravf/.claude',
|
|
185
|
+
matchedCwd: true,
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
} finally {
|
|
189
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('listKnownClaudeCodeSessions matches encoded project key with trailing cwd slash', () => {
|
|
194
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-trailing-cwd-discovery-'))
|
|
195
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
196
|
+
const projectsDir = path.join(claudeHome, 'projects', '-Users-bravf--claude')
|
|
197
|
+
|
|
198
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
199
|
+
|
|
200
|
+
const projectPath = path.join(projectsDir, 'ses_trailing_cwd.jsonl')
|
|
201
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: '继续之前的任务' } })}\n`)
|
|
202
|
+
|
|
203
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
204
|
+
fs.utimesSync(projectPath, now, now)
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const items = listKnownClaudeCodeSessions({
|
|
208
|
+
claudeHome,
|
|
209
|
+
limit: 10,
|
|
210
|
+
cwd: '/Users/bravf/.claude/',
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
assert.equal(items.length, 1)
|
|
214
|
+
assert.deepEqual(
|
|
215
|
+
{
|
|
216
|
+
id: items[0].id,
|
|
217
|
+
cwd: items[0].cwd,
|
|
218
|
+
matchedCwd: items[0].matchedCwd,
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 'ses_trailing_cwd',
|
|
222
|
+
cwd: '/Users/bravf/.claude',
|
|
223
|
+
matchedCwd: true,
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
} finally {
|
|
227
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('listKnownClaudeCodeSessions scans target project before global file limit', () => {
|
|
232
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-target-first-discovery-'))
|
|
233
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
234
|
+
const projectsRoot = path.join(claudeHome, 'projects')
|
|
235
|
+
const targetDir = path.join(projectsRoot, '-zzzz--claude')
|
|
236
|
+
|
|
237
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
238
|
+
|
|
239
|
+
for (let index = 0; index < 805; index += 1) {
|
|
240
|
+
const fillerDir = path.join(projectsRoot, `-Users-bravf-code-filler-${String(index).padStart(3, '0')}`)
|
|
241
|
+
fs.mkdirSync(fillerDir, { recursive: true })
|
|
242
|
+
fs.writeFileSync(
|
|
243
|
+
path.join(fillerDir, `ses_filler_${index}.jsonl`),
|
|
244
|
+
`${JSON.stringify({ type: 'user', message: { text: `无关历史 ${index}` } })}\n`
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const projectPath = path.join(targetDir, 'ses_target_first.jsonl')
|
|
249
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: '目标项目历史' } })}\n`)
|
|
250
|
+
|
|
251
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
252
|
+
fs.utimesSync(projectPath, now, now)
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const items = listKnownClaudeCodeSessions({
|
|
256
|
+
claudeHome,
|
|
257
|
+
limit: 10,
|
|
258
|
+
cwd: '/zzzz/.claude',
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
assert.equal(items[0]?.id, 'ses_target_first')
|
|
262
|
+
assert.equal(items[0]?.cwd, '/zzzz/.claude')
|
|
263
|
+
assert.equal(items[0]?.matchedCwd, true)
|
|
264
|
+
} finally {
|
|
265
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('listKnownClaudeCodeSessions matches realpath project key for symlink cwd', () => {
|
|
270
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-realpath-discovery-'))
|
|
271
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
272
|
+
const realCwd = path.join(tempRoot, 'real-workspace')
|
|
273
|
+
const linkCwd = path.join(tempRoot, 'link-workspace')
|
|
274
|
+
|
|
275
|
+
fs.mkdirSync(realCwd, { recursive: true })
|
|
276
|
+
fs.symlinkSync(realCwd, linkCwd, 'dir')
|
|
277
|
+
|
|
278
|
+
const realProjectKey = fs.realpathSync.native(realCwd).replace(/[/:.]/g, '-')
|
|
279
|
+
const projectsDir = path.join(claudeHome, 'projects', realProjectKey)
|
|
280
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
281
|
+
|
|
282
|
+
const projectPath = path.join(projectsDir, 'ses_realpath_cwd.jsonl')
|
|
283
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: 'symlink 历史' } })}\n`)
|
|
284
|
+
|
|
285
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
286
|
+
fs.utimesSync(projectPath, now, now)
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const items = listKnownClaudeCodeSessions({
|
|
290
|
+
claudeHome,
|
|
291
|
+
limit: 10,
|
|
292
|
+
cwd: linkCwd,
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
assert.equal(items[0]?.id, 'ses_realpath_cwd')
|
|
296
|
+
assert.equal(items[0]?.cwd, linkCwd)
|
|
297
|
+
assert.equal(items[0]?.matchedCwd, true)
|
|
298
|
+
} finally {
|
|
299
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('listKnownClaudeCodeSessions treats jsonl realpath cwd as matching symlink cwd', () => {
|
|
304
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-jsonl-realpath-discovery-'))
|
|
305
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
306
|
+
const realCwd = path.join(tempRoot, 'real-workspace')
|
|
307
|
+
const linkCwd = path.join(tempRoot, 'link-workspace')
|
|
308
|
+
|
|
309
|
+
fs.mkdirSync(realCwd, { recursive: true })
|
|
310
|
+
fs.symlinkSync(realCwd, linkCwd, 'dir')
|
|
311
|
+
|
|
312
|
+
const nativeRealCwd = fs.realpathSync.native(realCwd)
|
|
313
|
+
const realProjectKey = nativeRealCwd.replace(/[/:.]/g, '-')
|
|
314
|
+
const projectsDir = path.join(claudeHome, 'projects', realProjectKey)
|
|
315
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
316
|
+
|
|
317
|
+
const projectPath = path.join(projectsDir, 'ses_jsonl_realpath_cwd.jsonl')
|
|
318
|
+
fs.writeFileSync(
|
|
319
|
+
projectPath,
|
|
320
|
+
`${JSON.stringify({ type: 'user', message: { text: 'jsonl realpath 历史' }, cwd: nativeRealCwd })}\n`
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
324
|
+
fs.utimesSync(projectPath, now, now)
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const items = listKnownClaudeCodeSessions({
|
|
328
|
+
claudeHome,
|
|
329
|
+
limit: 10,
|
|
330
|
+
cwd: linkCwd,
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
assert.equal(items[0]?.id, 'ses_jsonl_realpath_cwd')
|
|
334
|
+
assert.equal(items[0]?.cwd, linkCwd)
|
|
335
|
+
assert.equal(items[0]?.matchedCwd, true)
|
|
336
|
+
} finally {
|
|
337
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('listKnownClaudeCodeSessions matches Windows encoded project key case-insensitively', () => {
|
|
342
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-windows-cwd-discovery-'))
|
|
343
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
344
|
+
const projectsDir = path.join(claudeHome, 'projects', 'C--Users-bravf--claude')
|
|
345
|
+
|
|
346
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
347
|
+
|
|
348
|
+
const projectPath = path.join(projectsDir, 'ses_windows_cwd.jsonl')
|
|
349
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: '继续 Windows 任务' } })}\n`)
|
|
350
|
+
|
|
351
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
352
|
+
fs.utimesSync(projectPath, now, now)
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const items = listKnownClaudeCodeSessions({
|
|
356
|
+
claudeHome,
|
|
357
|
+
limit: 10,
|
|
358
|
+
cwd: 'c:\\Users\\bravf\\.claude\\',
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
assert.equal(items.length, 1)
|
|
362
|
+
assert.deepEqual(
|
|
363
|
+
{
|
|
364
|
+
id: items[0].id,
|
|
365
|
+
cwd: items[0].cwd,
|
|
366
|
+
matchedCwd: items[0].matchedCwd,
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
id: 'ses_windows_cwd',
|
|
370
|
+
cwd: 'c:/Users/bravf/.claude',
|
|
371
|
+
matchedCwd: true,
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
} finally {
|
|
375
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('listKnownClaudeCodeSessions scans uppercase Windows project key before global file limit', () => {
|
|
380
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-windows-target-first-discovery-'))
|
|
381
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
382
|
+
const projectsRoot = path.join(claudeHome, 'projects')
|
|
383
|
+
const targetDir = path.join(projectsRoot, 'C--Users-bravf--claude')
|
|
384
|
+
|
|
385
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
386
|
+
|
|
387
|
+
for (let index = 0; index < 805; index += 1) {
|
|
388
|
+
const fillerDir = path.join(projectsRoot, `A--Users-bravf-code-filler-${String(index).padStart(3, '0')}`)
|
|
389
|
+
fs.mkdirSync(fillerDir, { recursive: true })
|
|
390
|
+
fs.writeFileSync(
|
|
391
|
+
path.join(fillerDir, `ses_windows_filler_${index}.jsonl`),
|
|
392
|
+
`${JSON.stringify({ type: 'user', message: { text: `无关 Windows 历史 ${index}` } })}\n`
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const projectPath = path.join(targetDir, 'ses_windows_target_first.jsonl')
|
|
397
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: 'Windows 目标项目历史' } })}\n`)
|
|
398
|
+
|
|
399
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
400
|
+
fs.utimesSync(projectPath, now, now)
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const items = listKnownClaudeCodeSessions({
|
|
404
|
+
claudeHome,
|
|
405
|
+
limit: 10,
|
|
406
|
+
cwd: 'c:\\Users\\bravf\\.claude\\',
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
assert.equal(items[0]?.id, 'ses_windows_target_first')
|
|
410
|
+
assert.equal(items[0]?.cwd, 'c:/Users/bravf/.claude')
|
|
411
|
+
assert.equal(items[0]?.matchedCwd, true)
|
|
412
|
+
} finally {
|
|
413
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
test('listKnownClaudeCodeSessions reads cwd from message.cwd when top-level cwd is missing', () => {
|
|
418
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-msgcwd-discovery-'))
|
|
419
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
420
|
+
const projectsDir = path.join(claudeHome, 'projects', '-Users-bravf--config')
|
|
421
|
+
|
|
422
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
423
|
+
|
|
424
|
+
const projectPath = path.join(projectsDir, 'ses_msgcwd.jsonl')
|
|
425
|
+
fs.writeFileSync(
|
|
426
|
+
projectPath,
|
|
427
|
+
`${JSON.stringify({ type: 'user', message: { text: '编辑配置', cwd: '/Users/bravf/.config' } })}
|
|
428
|
+
`
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
432
|
+
fs.utimesSync(projectPath, now, now)
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const items = listKnownClaudeCodeSessions({
|
|
436
|
+
claudeHome,
|
|
437
|
+
limit: 10,
|
|
438
|
+
cwd: '/Users/bravf/.config',
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
assert.equal(items.length, 1)
|
|
442
|
+
assert.deepEqual(
|
|
443
|
+
{
|
|
444
|
+
id: items[0].id,
|
|
445
|
+
cwd: items[0].cwd,
|
|
446
|
+
matchedCwd: items[0].matchedCwd,
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: 'ses_msgcwd',
|
|
450
|
+
cwd: '/Users/bravf/.config',
|
|
451
|
+
matchedCwd: true,
|
|
452
|
+
}
|
|
453
|
+
)
|
|
454
|
+
} finally {
|
|
455
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
456
|
+
}
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
test('listKnownOpenCodeSessions discovers sessions from desktop dat files', () => {
|
|
460
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-opencode-discovery-'))
|
|
461
|
+
const dataDir = path.join(tempRoot, 'ai.opencode.desktop')
|
|
462
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
463
|
+
|
|
464
|
+
const globalPath = path.join(dataDir, 'opencode.global.dat')
|
|
465
|
+
const workspacePath = path.join(
|
|
466
|
+
dataDir,
|
|
467
|
+
`opencode.workspace.${Buffer.from('/Users/bravf/code/promptx').toString('base64')}.test.dat`
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
fs.writeFileSync(globalPath, JSON.stringify({
|
|
471
|
+
'layout.page': JSON.stringify({
|
|
472
|
+
lastProjectSession: {
|
|
473
|
+
'/Users/bravf/code/promptx': {
|
|
474
|
+
directory: '/Users/bravf/code/promptx',
|
|
475
|
+
id: 'ses_global',
|
|
476
|
+
at: '2026-04-13T08:00:00.000Z',
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
lastSession: {
|
|
480
|
+
'/Users/bravf/code/promptx': 'ses_last',
|
|
481
|
+
},
|
|
482
|
+
}),
|
|
483
|
+
}))
|
|
484
|
+
|
|
485
|
+
fs.writeFileSync(workspacePath, JSON.stringify({
|
|
486
|
+
'session:ses_global:comments': '[]',
|
|
487
|
+
'session:ses_workspace:prompt': '请继续处理前端主题',
|
|
488
|
+
}))
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
const items = listKnownOpenCodeSessions({
|
|
492
|
+
openCodeDataDir: dataDir,
|
|
493
|
+
openCodeDbPath: path.join(tempRoot, 'missing-opencode.db'),
|
|
494
|
+
cwd: '/Users/bravf/code/promptx',
|
|
495
|
+
limit: 10,
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
assert.deepEqual(
|
|
499
|
+
items.map((item) => ({
|
|
500
|
+
id: item.id,
|
|
501
|
+
cwd: item.cwd,
|
|
502
|
+
matchedCwd: item.matchedCwd,
|
|
503
|
+
})),
|
|
504
|
+
[
|
|
505
|
+
{ id: 'ses_global', cwd: '/Users/bravf/code/promptx', matchedCwd: true },
|
|
506
|
+
{ id: 'ses_workspace', cwd: '/Users/bravf/code/promptx', matchedCwd: true },
|
|
507
|
+
{ id: 'ses_last', cwd: '/Users/bravf/code/promptx', matchedCwd: true },
|
|
508
|
+
]
|
|
509
|
+
)
|
|
510
|
+
assert.match(items[1].label, /前端主题/)
|
|
511
|
+
} finally {
|
|
512
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
513
|
+
}
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
test('listKnownKimiCodeSessions merges kimi.json work dirs with session state', () => {
|
|
517
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-kimi-discovery-'))
|
|
518
|
+
const kimiHome = path.join(tempRoot, '.kimi')
|
|
519
|
+
const sessionDir = path.join(kimiHome, 'sessions', 'hash-1', 'kimi-session-1')
|
|
520
|
+
fs.mkdirSync(sessionDir, { recursive: true })
|
|
521
|
+
|
|
522
|
+
const kimiJsonPath = path.join(kimiHome, 'kimi.json')
|
|
523
|
+
const statePath = path.join(sessionDir, 'state.json')
|
|
524
|
+
const contextPath = path.join(sessionDir, 'context.jsonl')
|
|
525
|
+
const kimiJsonTime = new Date('2026-04-13T08:00:00.000Z')
|
|
526
|
+
const stateTime = new Date('2026-04-13T09:00:00.000Z')
|
|
527
|
+
|
|
528
|
+
fs.writeFileSync(kimiJsonPath, JSON.stringify({
|
|
529
|
+
work_dirs: [
|
|
530
|
+
{
|
|
531
|
+
path: '/Users/bravf/code/promptx',
|
|
532
|
+
last_session_id: 'kimi-session-1',
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
}))
|
|
536
|
+
fs.writeFileSync(statePath, JSON.stringify({
|
|
537
|
+
custom_title: '继续 Kimi 项目',
|
|
538
|
+
}))
|
|
539
|
+
fs.writeFileSync(contextPath, `${JSON.stringify({ role: 'user', content: '帮我修复 Kimi 接入' })}\n`)
|
|
540
|
+
fs.utimesSync(kimiJsonPath, kimiJsonTime, kimiJsonTime)
|
|
541
|
+
fs.utimesSync(statePath, stateTime, stateTime)
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
const items = listKnownKimiCodeSessions({
|
|
545
|
+
kimiHome,
|
|
546
|
+
limit: 10,
|
|
547
|
+
cwd: '/Users/bravf/code/promptx',
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
assert.equal(items.length, 1)
|
|
551
|
+
assert.deepEqual(
|
|
552
|
+
{
|
|
553
|
+
id: items[0].id,
|
|
554
|
+
label: items[0].label,
|
|
555
|
+
cwd: items[0].cwd,
|
|
556
|
+
updatedAt: items[0].updatedAt,
|
|
557
|
+
updatedAtSource: items[0].updatedAtSource,
|
|
558
|
+
matchedCwd: items[0].matchedCwd,
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
id: 'kimi-session-1',
|
|
562
|
+
label: '继续 Kimi 项目',
|
|
563
|
+
cwd: '/Users/bravf/code/promptx',
|
|
564
|
+
updatedAt: stateTime.toISOString(),
|
|
565
|
+
updatedAtSource: 'explicit',
|
|
566
|
+
matchedCwd: true,
|
|
567
|
+
}
|
|
568
|
+
)
|
|
569
|
+
} finally {
|
|
570
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
571
|
+
}
|
|
572
|
+
})
|