@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,478 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { EventEmitter } from 'node:events'
|
|
3
|
-
import test from 'node:test'
|
|
4
|
-
import { setTimeout as delay } from 'node:timers/promises'
|
|
5
|
-
import { WebSocketServer } from 'ws'
|
|
6
|
-
|
|
7
|
-
import { createRelayClient } from './relayClient.js'
|
|
8
|
-
|
|
9
|
-
async function withRelayServer(run) {
|
|
10
|
-
const server = new WebSocketServer({ port: 0 })
|
|
11
|
-
|
|
12
|
-
await new Promise((resolve) => server.once('listening', resolve))
|
|
13
|
-
const { port } = server.address()
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
await run({
|
|
17
|
-
relayWsUrl: `ws://127.0.0.1:${port}`,
|
|
18
|
-
server,
|
|
19
|
-
})
|
|
20
|
-
} finally {
|
|
21
|
-
server.clients.forEach((client) => {
|
|
22
|
-
try {
|
|
23
|
-
client.terminate()
|
|
24
|
-
} catch {
|
|
25
|
-
// ignore
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
await new Promise((resolve) => server.close(resolve))
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function listenJsonMessages(socket, handler) {
|
|
33
|
-
socket.on('message', (payload, isBinary) => {
|
|
34
|
-
if (isBinary) {
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let message = null
|
|
39
|
-
try {
|
|
40
|
-
message = JSON.parse(payload.toString('utf8'))
|
|
41
|
-
} catch {
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
handler(message)
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function waitFor(check, timeoutMs = 2_000) {
|
|
50
|
-
const startedAt = Date.now()
|
|
51
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
52
|
-
const value = check()
|
|
53
|
-
if (value) {
|
|
54
|
-
return value
|
|
55
|
-
}
|
|
56
|
-
await delay(20)
|
|
57
|
-
}
|
|
58
|
-
throw new Error('waitFor timeout')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function createSilentLogger(logs = []) {
|
|
62
|
-
return {
|
|
63
|
-
info(...args) {
|
|
64
|
-
logs.push(['info', args])
|
|
65
|
-
},
|
|
66
|
-
warn(...args) {
|
|
67
|
-
logs.push(['warn', args])
|
|
68
|
-
},
|
|
69
|
-
error(...args) {
|
|
70
|
-
logs.push(['error', args])
|
|
71
|
-
},
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
class FakeSocket extends EventEmitter {
|
|
76
|
-
constructor() {
|
|
77
|
-
super()
|
|
78
|
-
this.readyState = 0
|
|
79
|
-
this.sentPayloads = []
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
send(payload) {
|
|
83
|
-
this.sentPayloads.push(JSON.parse(String(payload || '{}')))
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
close(code = 1000, reason = '') {
|
|
87
|
-
this.readyState = 3
|
|
88
|
-
this.emit('close', code, Buffer.from(String(reason || '')))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
terminate() {
|
|
92
|
-
this.close(1006, 'terminated')
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
test('relay client becomes connected only after hello ack', async () => {
|
|
97
|
-
await withRelayServer(async ({ relayWsUrl, server }) => {
|
|
98
|
-
server.on('connection', (socket) => {
|
|
99
|
-
listenJsonMessages(socket, (message) => {
|
|
100
|
-
if (message?.type === 'hello') {
|
|
101
|
-
socket.send(JSON.stringify({
|
|
102
|
-
type: 'hello.ack',
|
|
103
|
-
ok: true,
|
|
104
|
-
deviceId: message.deviceId,
|
|
105
|
-
}))
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
const logs = []
|
|
111
|
-
const client = createRelayClient({
|
|
112
|
-
relayUrl: relayWsUrl.replace(/^ws/, 'http'),
|
|
113
|
-
deviceId: 'my-device',
|
|
114
|
-
deviceToken: 'secret',
|
|
115
|
-
logger: createSilentLogger(logs),
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
client.start()
|
|
120
|
-
await waitFor(() => client.getStatus().connected === true)
|
|
121
|
-
|
|
122
|
-
const status = client.getStatus()
|
|
123
|
-
assert.equal(status.connected, true)
|
|
124
|
-
assert.equal(status.lastError, '')
|
|
125
|
-
assert.equal(Boolean(status.lastConnectedAt), true)
|
|
126
|
-
assert.equal(Boolean(status.lastHeartbeatAt), true)
|
|
127
|
-
assert.equal(status.lastCloseReason, '')
|
|
128
|
-
assert.equal(status.pendingRequestCount, 0)
|
|
129
|
-
assert.equal(status.socketReadyState, 1)
|
|
130
|
-
assert.equal(status.recentEvents.some((event) => event.type === 'auth_ok'), true)
|
|
131
|
-
assert.equal(logs.some(([level, args]) => level === 'info' && String(args.at(-1)).includes('连接已就绪')), true)
|
|
132
|
-
} finally {
|
|
133
|
-
client.stop()
|
|
134
|
-
}
|
|
135
|
-
})
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
test('relay client records heartbeat timestamp after server ping', async () => {
|
|
139
|
-
await withRelayServer(async ({ relayWsUrl, server }) => {
|
|
140
|
-
server.on('connection', (socket) => {
|
|
141
|
-
listenJsonMessages(socket, (message) => {
|
|
142
|
-
if (message?.type === 'hello') {
|
|
143
|
-
socket.send(JSON.stringify({
|
|
144
|
-
type: 'hello.ack',
|
|
145
|
-
ok: true,
|
|
146
|
-
deviceId: message.deviceId,
|
|
147
|
-
}))
|
|
148
|
-
|
|
149
|
-
setTimeout(() => {
|
|
150
|
-
if (socket.readyState === 1) {
|
|
151
|
-
socket.ping()
|
|
152
|
-
}
|
|
153
|
-
}, 40)
|
|
154
|
-
}
|
|
155
|
-
})
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
const client = createRelayClient({
|
|
159
|
-
relayUrl: relayWsUrl.replace(/^ws/, 'http'),
|
|
160
|
-
deviceId: 'my-device',
|
|
161
|
-
deviceToken: 'secret',
|
|
162
|
-
logger: createSilentLogger(),
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
client.start()
|
|
167
|
-
await waitFor(() => client.getStatus().connected === true)
|
|
168
|
-
const initialHeartbeatAt = client.getStatus().lastHeartbeatAt
|
|
169
|
-
await waitFor(() => client.getStatus().lastHeartbeatAt && client.getStatus().lastHeartbeatAt !== initialHeartbeatAt)
|
|
170
|
-
|
|
171
|
-
assert.equal(Boolean(client.getStatus().lastHeartbeatAt), true)
|
|
172
|
-
} finally {
|
|
173
|
-
client.stop()
|
|
174
|
-
}
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
test('relay client pauses reconnect after non-retryable reject reason', async () => {
|
|
179
|
-
await withRelayServer(async ({ relayWsUrl, server }) => {
|
|
180
|
-
let connectionCount = 0
|
|
181
|
-
server.on('connection', (socket) => {
|
|
182
|
-
connectionCount += 1
|
|
183
|
-
listenJsonMessages(socket, (message) => {
|
|
184
|
-
if (message?.type === 'hello') {
|
|
185
|
-
socket.close(1008, 'invalid_device')
|
|
186
|
-
}
|
|
187
|
-
})
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
const client = createRelayClient({
|
|
191
|
-
relayUrl: relayWsUrl.replace(/^ws/, 'http'),
|
|
192
|
-
deviceId: 'my-device',
|
|
193
|
-
deviceToken: 'secret',
|
|
194
|
-
reconnectDelayStrategy: () => 20,
|
|
195
|
-
logger: createSilentLogger(),
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
client.start()
|
|
200
|
-
await waitFor(() => client.getStatus().lastCloseReason !== '')
|
|
201
|
-
await delay(120)
|
|
202
|
-
|
|
203
|
-
const status = client.getStatus()
|
|
204
|
-
assert.equal(status.connected, false)
|
|
205
|
-
assert.equal(status.lastCloseCode, 1008)
|
|
206
|
-
assert.equal(status.lastCloseReason, '设备 ID 不匹配')
|
|
207
|
-
assert.match(status.lastError, /设备 ID 不匹配/)
|
|
208
|
-
assert.equal(status.reconnectPaused, true)
|
|
209
|
-
assert.equal(status.reconnectPausedReason, '设备 ID 不匹配')
|
|
210
|
-
assert.equal(connectionCount, 1)
|
|
211
|
-
assert.equal(status.recentEvents.some((event) => event.type === 'reconnect_paused'), true)
|
|
212
|
-
} finally {
|
|
213
|
-
client.stop()
|
|
214
|
-
}
|
|
215
|
-
})
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
test('relay client resets reconnect count after reconnect succeeds', async () => {
|
|
219
|
-
await withRelayServer(async ({ relayWsUrl, server }) => {
|
|
220
|
-
let connectionCount = 0
|
|
221
|
-
server.on('connection', (socket) => {
|
|
222
|
-
connectionCount += 1
|
|
223
|
-
const currentConnection = connectionCount
|
|
224
|
-
listenJsonMessages(socket, (message) => {
|
|
225
|
-
if (message?.type !== 'hello') {
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
socket.send(JSON.stringify({
|
|
230
|
-
type: 'hello.ack',
|
|
231
|
-
ok: true,
|
|
232
|
-
deviceId: message.deviceId,
|
|
233
|
-
}))
|
|
234
|
-
|
|
235
|
-
if (currentConnection === 1) {
|
|
236
|
-
setTimeout(() => {
|
|
237
|
-
if (socket.readyState === 1) {
|
|
238
|
-
socket.close(1012, 'server_restart')
|
|
239
|
-
}
|
|
240
|
-
}, 30)
|
|
241
|
-
}
|
|
242
|
-
})
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
const client = createRelayClient({
|
|
246
|
-
relayUrl: relayWsUrl.replace(/^ws/, 'http'),
|
|
247
|
-
deviceId: 'my-device',
|
|
248
|
-
deviceToken: 'secret',
|
|
249
|
-
reconnectDelayStrategy: () => 20,
|
|
250
|
-
logger: createSilentLogger(),
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
client.start()
|
|
255
|
-
await waitFor(() => connectionCount >= 2)
|
|
256
|
-
await waitFor(() => client.getStatus().connected === true && client.getStatus().reconnectCount === 0)
|
|
257
|
-
|
|
258
|
-
const status = client.getStatus()
|
|
259
|
-
assert.equal(status.connected, true)
|
|
260
|
-
assert.equal(status.reconnectCount, 0)
|
|
261
|
-
assert.equal(status.nextReconnectDelayMs, 0)
|
|
262
|
-
assert.equal(status.reconnectPaused, false)
|
|
263
|
-
assert.equal(status.recentEvents.some((event) => event.type === 'reconnect_scheduled'), true)
|
|
264
|
-
} finally {
|
|
265
|
-
client.stop()
|
|
266
|
-
}
|
|
267
|
-
})
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
test('relay client self-heals when heartbeat becomes stale', async () => {
|
|
271
|
-
await withRelayServer(async ({ relayWsUrl, server }) => {
|
|
272
|
-
let connectionCount = 0
|
|
273
|
-
server.on('connection', (socket) => {
|
|
274
|
-
connectionCount += 1
|
|
275
|
-
const currentConnection = connectionCount
|
|
276
|
-
listenJsonMessages(socket, (message) => {
|
|
277
|
-
if (message?.type !== 'hello') {
|
|
278
|
-
return
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
socket.send(JSON.stringify({
|
|
282
|
-
type: 'hello.ack',
|
|
283
|
-
ok: true,
|
|
284
|
-
deviceId: message.deviceId,
|
|
285
|
-
}))
|
|
286
|
-
|
|
287
|
-
if (currentConnection >= 2) {
|
|
288
|
-
setTimeout(() => {
|
|
289
|
-
if (socket.readyState === 1) {
|
|
290
|
-
socket.ping()
|
|
291
|
-
}
|
|
292
|
-
}, 20)
|
|
293
|
-
}
|
|
294
|
-
})
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
const client = createRelayClient({
|
|
298
|
-
relayUrl: relayWsUrl.replace(/^ws/, 'http'),
|
|
299
|
-
deviceId: 'my-device',
|
|
300
|
-
deviceToken: 'secret',
|
|
301
|
-
reconnectDelayStrategy: () => 20,
|
|
302
|
-
healthCheckIntervalMs: 20,
|
|
303
|
-
heartbeatTimeoutMs: 60,
|
|
304
|
-
logger: createSilentLogger(),
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
try {
|
|
308
|
-
client.start()
|
|
309
|
-
await waitFor(() => connectionCount >= 2, 3_000)
|
|
310
|
-
await waitFor(() => client.getStatus().connected === true)
|
|
311
|
-
|
|
312
|
-
const status = client.getStatus()
|
|
313
|
-
assert.equal(status.connected, true)
|
|
314
|
-
assert.equal(connectionCount >= 2, true)
|
|
315
|
-
assert.equal(status.recentEvents.some((event) => event.type === 'heartbeat_stale'), true)
|
|
316
|
-
assert.equal(status.recentEvents.some((event) => event.type === 'reconnect_requested' && event.source === 'heartbeat_timeout'), true)
|
|
317
|
-
} finally {
|
|
318
|
-
client.stop()
|
|
319
|
-
}
|
|
320
|
-
})
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
test('relay client detects resume-like clock jump and reconnects', async () => {
|
|
324
|
-
await withRelayServer(async ({ relayWsUrl, server }) => {
|
|
325
|
-
let connectionCount = 0
|
|
326
|
-
let fakeNow = Date.now()
|
|
327
|
-
|
|
328
|
-
server.on('connection', (socket) => {
|
|
329
|
-
connectionCount += 1
|
|
330
|
-
const currentConnection = connectionCount
|
|
331
|
-
listenJsonMessages(socket, (message) => {
|
|
332
|
-
if (message?.type !== 'hello') {
|
|
333
|
-
return
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
socket.send(JSON.stringify({
|
|
337
|
-
type: 'hello.ack',
|
|
338
|
-
ok: true,
|
|
339
|
-
deviceId: message.deviceId,
|
|
340
|
-
}))
|
|
341
|
-
|
|
342
|
-
if (currentConnection >= 2) {
|
|
343
|
-
setTimeout(() => {
|
|
344
|
-
if (socket.readyState === 1) {
|
|
345
|
-
socket.ping()
|
|
346
|
-
}
|
|
347
|
-
}, 20)
|
|
348
|
-
}
|
|
349
|
-
})
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
const client = createRelayClient({
|
|
353
|
-
relayUrl: relayWsUrl.replace(/^ws/, 'http'),
|
|
354
|
-
deviceId: 'my-device',
|
|
355
|
-
deviceToken: 'secret',
|
|
356
|
-
reconnectDelayStrategy: () => 20,
|
|
357
|
-
healthCheckIntervalMs: 20,
|
|
358
|
-
heartbeatTimeoutMs: 60,
|
|
359
|
-
sleepResumeThresholdMs: 40,
|
|
360
|
-
getNow: () => fakeNow,
|
|
361
|
-
logger: createSilentLogger(),
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
client.start()
|
|
366
|
-
await waitFor(() => client.getStatus().connected === true)
|
|
367
|
-
|
|
368
|
-
fakeNow += 500
|
|
369
|
-
|
|
370
|
-
await waitFor(() => connectionCount >= 2, 3_000)
|
|
371
|
-
await waitFor(() => client.getStatus().connected === true)
|
|
372
|
-
|
|
373
|
-
const status = client.getStatus()
|
|
374
|
-
assert.equal(status.connected, true)
|
|
375
|
-
assert.equal(status.recentEvents.some((event) => event.type === 'system_resume_detected'), true)
|
|
376
|
-
assert.equal(status.recentEvents.some((event) => event.type === 'reconnect_requested' && event.source === 'heartbeat_timeout'), true)
|
|
377
|
-
} finally {
|
|
378
|
-
client.stop()
|
|
379
|
-
}
|
|
380
|
-
})
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
test('relay client supports manual reconnect', async () => {
|
|
384
|
-
await withRelayServer(async ({ relayWsUrl, server }) => {
|
|
385
|
-
let connectionCount = 0
|
|
386
|
-
server.on('connection', (socket) => {
|
|
387
|
-
connectionCount += 1
|
|
388
|
-
listenJsonMessages(socket, (message) => {
|
|
389
|
-
if (message?.type === 'hello') {
|
|
390
|
-
socket.send(JSON.stringify({
|
|
391
|
-
type: 'hello.ack',
|
|
392
|
-
ok: true,
|
|
393
|
-
deviceId: message.deviceId,
|
|
394
|
-
}))
|
|
395
|
-
}
|
|
396
|
-
})
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
const client = createRelayClient({
|
|
400
|
-
relayUrl: relayWsUrl.replace(/^ws/, 'http'),
|
|
401
|
-
deviceId: 'my-device',
|
|
402
|
-
deviceToken: 'secret',
|
|
403
|
-
reconnectDelayStrategy: () => 20,
|
|
404
|
-
logger: createSilentLogger(),
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
try {
|
|
408
|
-
client.start()
|
|
409
|
-
await waitFor(() => client.getStatus().connected === true)
|
|
410
|
-
|
|
411
|
-
const reconnectResult = client.reconnect()
|
|
412
|
-
assert.equal(reconnectResult, true)
|
|
413
|
-
|
|
414
|
-
await waitFor(() => connectionCount >= 2, 3_000)
|
|
415
|
-
await waitFor(() => client.getStatus().connected === true)
|
|
416
|
-
|
|
417
|
-
const status = client.getStatus()
|
|
418
|
-
assert.equal(status.connected, true)
|
|
419
|
-
assert.equal(status.recentEvents.some((event) => event.type === 'reconnect_requested' && event.source === 'manual'), true)
|
|
420
|
-
} finally {
|
|
421
|
-
client.stop()
|
|
422
|
-
}
|
|
423
|
-
})
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
test('relay client ignores stale close events from previous socket instances', async () => {
|
|
427
|
-
const sockets = []
|
|
428
|
-
const client = createRelayClient({
|
|
429
|
-
relayUrl: 'http://relay.example.com',
|
|
430
|
-
deviceId: 'my-device',
|
|
431
|
-
deviceToken: 'secret',
|
|
432
|
-
healthCheckIntervalMs: 60_000,
|
|
433
|
-
logger: createSilentLogger(),
|
|
434
|
-
createWebSocket() {
|
|
435
|
-
const socket = new FakeSocket()
|
|
436
|
-
sockets.push(socket)
|
|
437
|
-
return socket
|
|
438
|
-
},
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
try {
|
|
442
|
-
client.start()
|
|
443
|
-
assert.equal(sockets.length, 1)
|
|
444
|
-
|
|
445
|
-
const firstSocket = sockets[0]
|
|
446
|
-
firstSocket.readyState = 1
|
|
447
|
-
firstSocket.emit('open')
|
|
448
|
-
firstSocket.emit('message', Buffer.from(JSON.stringify({
|
|
449
|
-
type: 'hello.ack',
|
|
450
|
-
ok: true,
|
|
451
|
-
deviceId: 'my-device',
|
|
452
|
-
})), false)
|
|
453
|
-
|
|
454
|
-
assert.equal(client.getStatus().connected, true)
|
|
455
|
-
|
|
456
|
-
firstSocket.readyState = 2
|
|
457
|
-
client.start()
|
|
458
|
-
assert.equal(sockets.length, 2)
|
|
459
|
-
|
|
460
|
-
const secondSocket = sockets[1]
|
|
461
|
-
secondSocket.readyState = 1
|
|
462
|
-
secondSocket.emit('open')
|
|
463
|
-
secondSocket.emit('message', Buffer.from(JSON.stringify({
|
|
464
|
-
type: 'hello.ack',
|
|
465
|
-
ok: true,
|
|
466
|
-
deviceId: 'my-device',
|
|
467
|
-
})), false)
|
|
468
|
-
|
|
469
|
-
firstSocket.emit('close', 1000, Buffer.from('stale_socket_closed'))
|
|
470
|
-
|
|
471
|
-
const status = client.getStatus()
|
|
472
|
-
assert.equal(status.connected, true)
|
|
473
|
-
assert.equal(status.socketReadyState, 1)
|
|
474
|
-
assert.equal(status.recentEvents.some((event) => event.type === 'stale_close_ignored'), true)
|
|
475
|
-
} finally {
|
|
476
|
-
client.stop()
|
|
477
|
-
}
|
|
478
|
-
})
|
|
@@ -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('relay config module reads and writes stored config under promptx data dir', async () => {
|
|
8
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-relay-config-'))
|
|
9
|
-
const originalPromptxHome = process.env.PROMPTX_HOME
|
|
10
|
-
process.env.PROMPTX_HOME = tempDir
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const relayConfigModule = await import(`./relayConfig.js?test=${Date.now()}`)
|
|
14
|
-
const saved = relayConfigModule.writeStoredRelayConfig({
|
|
15
|
-
relayUrl: ' https://relay.example.com ',
|
|
16
|
-
deviceId: ' my-device ',
|
|
17
|
-
deviceToken: ' abc ',
|
|
18
|
-
allowRemoteShell: true,
|
|
19
|
-
enabled: true,
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
assert.deepEqual(saved, {
|
|
23
|
-
relayUrl: 'https://relay.example.com',
|
|
24
|
-
deviceId: 'my-device',
|
|
25
|
-
deviceToken: 'abc',
|
|
26
|
-
allowRemoteShell: true,
|
|
27
|
-
enabled: true,
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
const loaded = relayConfigModule.readStoredRelayConfig()
|
|
31
|
-
assert.deepEqual(loaded, saved)
|
|
32
|
-
assert.equal(fs.existsSync(relayConfigModule.getRelayConfigPath()), true)
|
|
33
|
-
} finally {
|
|
34
|
-
if (typeof originalPromptxHome === 'string') {
|
|
35
|
-
process.env.PROMPTX_HOME = originalPromptxHome
|
|
36
|
-
} else {
|
|
37
|
-
delete process.env.PROMPTX_HOME
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('relay config module lets env override allowRemoteShell', async () => {
|
|
43
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-relay-config-env-'))
|
|
44
|
-
const originalPromptxHome = process.env.PROMPTX_HOME
|
|
45
|
-
const originalAllowRemoteShell = process.env.PROMPTX_RELAY_ALLOW_REMOTE_SHELL
|
|
46
|
-
process.env.PROMPTX_HOME = tempDir
|
|
47
|
-
process.env.PROMPTX_RELAY_ALLOW_REMOTE_SHELL = '1'
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const relayConfigModule = await import(`./relayConfig.js?test=${Date.now()}`)
|
|
51
|
-
relayConfigModule.writeStoredRelayConfig({
|
|
52
|
-
relayUrl: 'https://relay.example.com',
|
|
53
|
-
deviceId: 'my-device',
|
|
54
|
-
deviceToken: 'abc',
|
|
55
|
-
allowRemoteShell: false,
|
|
56
|
-
enabled: true,
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
const loaded = relayConfigModule.getRelayConfigForClient()
|
|
60
|
-
assert.equal(loaded.allowRemoteShell, true)
|
|
61
|
-
} finally {
|
|
62
|
-
if (typeof originalPromptxHome === 'string') {
|
|
63
|
-
process.env.PROMPTX_HOME = originalPromptxHome
|
|
64
|
-
} else {
|
|
65
|
-
delete process.env.PROMPTX_HOME
|
|
66
|
-
}
|
|
67
|
-
if (typeof originalAllowRemoteShell === 'string') {
|
|
68
|
-
process.env.PROMPTX_RELAY_ALLOW_REMOTE_SHELL = originalAllowRemoteShell
|
|
69
|
-
} else {
|
|
70
|
-
delete process.env.PROMPTX_RELAY_ALLOW_REMOTE_SHELL
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
})
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import test from 'node:test'
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
buildRelayWebSocketUrl,
|
|
6
|
-
chunkBuffer,
|
|
7
|
-
constantTimeEqual,
|
|
8
|
-
parseCookieHeader,
|
|
9
|
-
sanitizeProxyHeaders,
|
|
10
|
-
} from './relayProtocol.js'
|
|
11
|
-
|
|
12
|
-
test('buildRelayWebSocketUrl converts base URL to relay websocket endpoint', () => {
|
|
13
|
-
assert.equal(buildRelayWebSocketUrl('https://relay.example.com/base?q=1'), 'wss://relay.example.com/relay/connect')
|
|
14
|
-
assert.equal(buildRelayWebSocketUrl('http://127.0.0.1:3030/'), 'ws://127.0.0.1:3030/relay/connect')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('chunkBuffer splits buffers by max size', () => {
|
|
18
|
-
const chunks = chunkBuffer(Buffer.from('abcdefghij'), 4)
|
|
19
|
-
assert.deepEqual(chunks.map((item) => item.toString('utf8')), ['abcd', 'efgh', 'ij'])
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('sanitizeProxyHeaders strips hop-by-hop headers and keeps content headers', () => {
|
|
23
|
-
assert.deepEqual(
|
|
24
|
-
sanitizeProxyHeaders({
|
|
25
|
-
Host: 'localhost:3000',
|
|
26
|
-
Connection: 'keep-alive',
|
|
27
|
-
'Content-Type': 'application/json',
|
|
28
|
-
Accept: 'application/json',
|
|
29
|
-
Cookie: 'secret=1',
|
|
30
|
-
}, ['cookie']),
|
|
31
|
-
{
|
|
32
|
-
'content-type': 'application/json',
|
|
33
|
-
accept: 'application/json',
|
|
34
|
-
}
|
|
35
|
-
)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
test('parseCookieHeader parses browser cookies', () => {
|
|
39
|
-
assert.deepEqual(parseCookieHeader('foo=bar; hello=world%20ok'), {
|
|
40
|
-
foo: 'bar',
|
|
41
|
-
hello: 'world ok',
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
test('constantTimeEqual compares exact token text', () => {
|
|
46
|
-
assert.equal(constantTimeEqual('abc', 'abc'), true)
|
|
47
|
-
assert.equal(constantTimeEqual('abc', 'abcd'), false)
|
|
48
|
-
assert.equal(constantTimeEqual('abc', 'abx'), false)
|
|
49
|
-
})
|