@muyichengshayu/promptx 0.2.7 → 0.2.8

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 (43) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/apps/runner/src/engines/claudeCodeRunner.js +69 -1
  3. package/apps/runner/src/engines/claudeCodeRunner.test.js +279 -0
  4. package/apps/runner/src/engines/openCodeRunner.test.js +73 -0
  5. package/apps/runner/src/engines/shellRunner.test.js +46 -0
  6. package/apps/runner/src/runManager.js +110 -11
  7. package/apps/runner/src/runManager.test.js +913 -0
  8. package/apps/runner/src/serverClient.test.js +93 -0
  9. package/apps/server/src/agentSessionDiscovery.test.js +127 -0
  10. package/apps/server/src/agents/claudeCodeRunner.test.js +433 -0
  11. package/apps/server/src/agents/openCodeRunner.test.js +236 -0
  12. package/apps/server/src/agents/runnerContract.test.js +382 -0
  13. package/apps/server/src/appPaths.test.js +52 -0
  14. package/apps/server/src/assetRoutes.test.js +168 -0
  15. package/apps/server/src/codex.test.js +518 -0
  16. package/apps/server/src/codexRoutes.test.js +376 -0
  17. package/apps/server/src/codexRuns.test.js +160 -0
  18. package/apps/server/src/codexSessions.test.js +369 -0
  19. package/apps/server/src/db.test.js +182 -0
  20. package/apps/server/src/gitDiff.test.js +542 -0
  21. package/apps/server/src/gitDiffClient.test.js +140 -0
  22. package/apps/server/src/internalRoutes.test.js +134 -0
  23. package/apps/server/src/maintenance.test.js +154 -0
  24. package/apps/server/src/processControl.test.js +147 -0
  25. package/apps/server/src/relayClient.test.js +478 -0
  26. package/apps/server/src/relayConfig.test.js +73 -0
  27. package/apps/server/src/relayProtocol.test.js +49 -0
  28. package/apps/server/src/relayServer.test.js +798 -0
  29. package/apps/server/src/relayTenants.test.js +137 -0
  30. package/apps/server/src/relayUsageStore.test.js +65 -0
  31. package/apps/server/src/repository.test.js +150 -0
  32. package/apps/server/src/runDispatchService.test.js +563 -0
  33. package/apps/server/src/runEventIngest.test.js +225 -0
  34. package/apps/server/src/runRecovery.test.js +73 -0
  35. package/apps/server/src/runnerClient.test.js +80 -0
  36. package/apps/server/src/runnerDispatch.test.js +136 -0
  37. package/apps/server/src/systemConfig.test.js +112 -0
  38. package/apps/server/src/systemRoutes.test.js +319 -0
  39. package/apps/server/src/taskRoutes.test.js +726 -0
  40. package/apps/server/src/upload.test.js +30 -0
  41. package/apps/server/src/webAppRoutes.test.js +67 -0
  42. package/apps/server/src/workspaceFiles.test.js +262 -0
  43. package/package.json +14 -21
@@ -0,0 +1,478 @@
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
+ })
@@ -0,0 +1,73 @@
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
+ })
@@ -0,0 +1,49 @@
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
+ })