@cursorpool-dev/cli 0.5.6

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 (105) hide show
  1. package/bin/cursor-pool.mjs +9 -0
  2. package/bin/cursor-pool.ts +169 -0
  3. package/node_modules/@cursor-pool/extension/dist/extension.js +2910 -0
  4. package/node_modules/@cursor-pool/extension/package.json +64 -0
  5. package/node_modules/@cursor-pool/extension/resources/cursor-pool.svg +6 -0
  6. package/node_modules/@cursor-pool/extension/src/api.ts +545 -0
  7. package/node_modules/@cursor-pool/extension/src/extension.ts +104 -0
  8. package/node_modules/@cursor-pool/extension/src/index.ts +1 -0
  9. package/node_modules/@cursor-pool/extension/src/panel.ts +569 -0
  10. package/node_modules/@cursor-pool/extension/src/runtime.ts +22 -0
  11. package/node_modules/@cursor-pool/extension/test/panel.test.ts +1785 -0
  12. package/node_modules/@cursor-pool/patcher/package.json +17 -0
  13. package/node_modules/@cursor-pool/patcher/src/alwaysLocalMarker.ts +86 -0
  14. package/node_modules/@cursor-pool/patcher/src/hash.ts +7 -0
  15. package/node_modules/@cursor-pool/patcher/src/index.ts +55 -0
  16. package/node_modules/@cursor-pool/patcher/src/marker.ts +159 -0
  17. package/node_modules/@cursor-pool/patcher/src/patchCursorAgentExec.ts +154 -0
  18. package/node_modules/@cursor-pool/patcher/src/patchCursorAlwaysLocal.ts +142 -0
  19. package/node_modules/@cursor-pool/patcher/src/patchCursorWorkbenchAuthGate.ts +140 -0
  20. package/node_modules/@cursor-pool/patcher/src/restoreCursorAgentExec.ts +52 -0
  21. package/node_modules/@cursor-pool/patcher/src/restoreCursorAlwaysLocal.ts +52 -0
  22. package/node_modules/@cursor-pool/patcher/src/restoreCursorWorkbenchAuthGate.ts +70 -0
  23. package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +243 -0
  24. package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +630 -0
  25. package/node_modules/@cursor-pool/patcher/test/patchCursorAlwaysLocal.test.ts +144 -0
  26. package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +770 -0
  27. package/node_modules/@cursor-pool/patcher/test/restoreCursorAgentExec.test.ts +139 -0
  28. package/node_modules/@cursor-pool/service/package.json +17 -0
  29. package/node_modules/@cursor-pool/service/src/canary.ts +61 -0
  30. package/node_modules/@cursor-pool/service/src/diagnostics.ts +385 -0
  31. package/node_modules/@cursor-pool/service/src/entry.ts +161 -0
  32. package/node_modules/@cursor-pool/service/src/health.ts +10 -0
  33. package/node_modules/@cursor-pool/service/src/index.ts +29 -0
  34. package/node_modules/@cursor-pool/service/src/metadata.ts +22 -0
  35. package/node_modules/@cursor-pool/service/src/platformSession.ts +1178 -0
  36. package/node_modules/@cursor-pool/service/src/requestCheck.ts +81 -0
  37. package/node_modules/@cursor-pool/service/src/requestGate.ts +100 -0
  38. package/node_modules/@cursor-pool/service/src/requestGateway.ts +441 -0
  39. package/node_modules/@cursor-pool/service/src/runtime.ts +48 -0
  40. package/node_modules/@cursor-pool/service/src/server.ts +939 -0
  41. package/node_modules/@cursor-pool/service/src/takeover.ts +111 -0
  42. package/node_modules/@cursor-pool/service/test/canary.test.ts +140 -0
  43. package/node_modules/@cursor-pool/service/test/diagnostics.test.ts +506 -0
  44. package/node_modules/@cursor-pool/service/test/metadata.test.ts +63 -0
  45. package/node_modules/@cursor-pool/service/test/platformSession.test.ts +2428 -0
  46. package/node_modules/@cursor-pool/service/test/requestCheck.test.ts +152 -0
  47. package/node_modules/@cursor-pool/service/test/requestGate.test.ts +207 -0
  48. package/node_modules/@cursor-pool/service/test/requestGateway.test.ts +466 -0
  49. package/node_modules/@cursor-pool/service/test/runtime.test.ts +47 -0
  50. package/node_modules/@cursor-pool/service/test/server.test.ts +2570 -0
  51. package/node_modules/@cursor-pool/shared/package.json +17 -0
  52. package/node_modules/@cursor-pool/shared/src/clientConfig.ts +49 -0
  53. package/node_modules/@cursor-pool/shared/src/index.ts +14 -0
  54. package/node_modules/@cursor-pool/shared/src/manifest.ts +36 -0
  55. package/node_modules/@cursor-pool/shared/src/metadata.ts +19 -0
  56. package/node_modules/@cursor-pool/shared/src/paths.ts +5 -0
  57. package/node_modules/@cursor-pool/shared/src/runtime.ts +3 -0
  58. package/node_modules/@cursor-pool/shared/test/index.test.ts +56 -0
  59. package/node_modules/@cursor-pool/shared/test/manifest.test.ts +65 -0
  60. package/node_modules/@cursor-pool/shared/test/metadata.test.ts +25 -0
  61. package/node_modules/@cursor-pool/shared/test/runtime.test.ts +8 -0
  62. package/package.json +28 -0
  63. package/src/adHocResign.ts +65 -0
  64. package/src/autostart.ts +240 -0
  65. package/src/compat.ts +282 -0
  66. package/src/confirm.ts +76 -0
  67. package/src/cursor.ts +94 -0
  68. package/src/diagnostics.ts +558 -0
  69. package/src/environment.ts +18 -0
  70. package/src/extensionBundle.ts +111 -0
  71. package/src/extensionLink.ts +168 -0
  72. package/src/index.ts +23 -0
  73. package/src/install.ts +614 -0
  74. package/src/installRecord.ts +105 -0
  75. package/src/launch.ts +182 -0
  76. package/src/patchSet.ts +182 -0
  77. package/src/platform.ts +132 -0
  78. package/src/repair.ts +383 -0
  79. package/src/restore.ts +153 -0
  80. package/src/serviceCommands.ts +79 -0
  81. package/src/serviceProcess.ts +188 -0
  82. package/src/status.ts +241 -0
  83. package/src/target.ts +37 -0
  84. package/src/trial.ts +133 -0
  85. package/src/uninstall.ts +213 -0
  86. package/test/autostart.test.ts +151 -0
  87. package/test/compat.test.ts +192 -0
  88. package/test/confirm.test.ts +114 -0
  89. package/test/cursor-pool-bin.test.ts +658 -0
  90. package/test/cursor.test.ts +20 -0
  91. package/test/diagnostics.test.ts +709 -0
  92. package/test/e2e-install.test.ts +773 -0
  93. package/test/extensionBundle.test.ts +161 -0
  94. package/test/extensionLink.test.ts +209 -0
  95. package/test/install.test.ts +862 -0
  96. package/test/installRecord.test.ts +107 -0
  97. package/test/launch.test.ts +138 -0
  98. package/test/platform.test.ts +226 -0
  99. package/test/repair.test.ts +575 -0
  100. package/test/restore.test.ts +211 -0
  101. package/test/serviceCommands.test.ts +135 -0
  102. package/test/serviceProcess.test.ts +280 -0
  103. package/test/status.test.ts +615 -0
  104. package/test/target.test.ts +49 -0
  105. package/test/trial.test.ts +146 -0
@@ -0,0 +1,466 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createServer } from 'node:http';
3
+ import test from 'node:test';
4
+ import {
5
+ createGatewayHttpForwarder,
6
+ createGatewayStore,
7
+ evaluateGatewayDecision,
8
+ completeGatewayForward,
9
+ resolveGatewayForward,
10
+ sanitizeAgentGateway,
11
+ sanitizeGatewayForward,
12
+ type AgentGateway,
13
+ type GatewayForwarder,
14
+ } from '../src/requestGateway';
15
+
16
+ async function createApiServer(handler: Parameters<typeof createServer>[0]) {
17
+ const server = createServer(handler);
18
+ await new Promise<void>((resolve) => {
19
+ server.listen(0, '127.0.0.1', resolve);
20
+ });
21
+ const address = server.address();
22
+ assert.equal(typeof address, 'object');
23
+ return {
24
+ baseUrl: `http://127.0.0.1:${address?.port}`,
25
+ close: () => new Promise<void>((resolve, reject) => {
26
+ server.close((error) => (error ? reject(error) : resolve()));
27
+ }),
28
+ };
29
+ }
30
+
31
+ const allowedDecision = {
32
+ state: 'allowed' as const,
33
+ productId: 'prod_basic',
34
+ modeStartedAt: '2026-05-31T00:05:00.000Z',
35
+ };
36
+
37
+ test('sanitizeAgentGateway keeps only safe metadata fields', () => {
38
+ const gateway = sanitizeAgentGateway(
39
+ {
40
+ requestId: 'req-gateway-1',
41
+ requestType: 'bad-type',
42
+ source: 'cursor-agent-exec',
43
+ model: 'gpt-test',
44
+ prompt: 'discard this',
45
+ messages: [{ role: 'user', content: 'discard' }],
46
+ headers: { authorization: 'Bearer secret' },
47
+ routeToken: 'rt_secret_value',
48
+ apiKey: 'sk-secret',
49
+ },
50
+ {
51
+ runtimeId: 'runtime-1',
52
+ decision: {
53
+ state: 'accepted',
54
+ productId: 'prod_basic',
55
+ modeStartedAt: '2026-05-31T00:05:00.000Z',
56
+ route: { state: 'ready', expiresAt: '2999-05-31T00:10:00.000Z' },
57
+ },
58
+ now: () => '2026-05-31T00:20:00.000Z',
59
+ },
60
+ );
61
+
62
+ assert.deepEqual(gateway, {
63
+ requestId: 'req-gateway-1',
64
+ requestType: 'agent',
65
+ source: 'cursor-agent-exec',
66
+ model: 'gpt-test',
67
+ receivedAt: '2026-05-31T00:20:00.000Z',
68
+ runtimeId: 'runtime-1',
69
+ decision: {
70
+ state: 'accepted',
71
+ productId: 'prod_basic',
72
+ modeStartedAt: '2026-05-31T00:05:00.000Z',
73
+ route: { state: 'ready', expiresAt: '2999-05-31T00:10:00.000Z' },
74
+ },
75
+ forward: { state: 'unknown' },
76
+ });
77
+ assert.equal(Object.hasOwn(gateway, 'prompt'), false);
78
+ assert.equal(Object.hasOwn(gateway, 'messages'), false);
79
+ assert.equal(Object.hasOwn(gateway, 'headers'), false);
80
+ assert.equal(Object.hasOwn(gateway, 'routeToken'), false);
81
+ assert.equal(Object.hasOwn(gateway, 'apiKey'), false);
82
+ });
83
+
84
+ test('sanitizeAgentGateway normalizes unsafe metadata and generates request id', () => {
85
+ const gateway = sanitizeAgentGateway(
86
+ {
87
+ requestId: '',
88
+ source: 'bad-source',
89
+ model: 'sk-secret-token',
90
+ },
91
+ {
92
+ runtimeId: 'runtime-1',
93
+ decision: { state: 'unknown' },
94
+ now: () => '2026-05-31T00:20:00.000Z',
95
+ requestId: () => 'generated-request-id',
96
+ },
97
+ );
98
+
99
+ assert.deepEqual(gateway, {
100
+ requestId: 'generated-request-id',
101
+ requestType: 'agent',
102
+ source: 'unknown',
103
+ model: 'unknown',
104
+ receivedAt: '2026-05-31T00:20:00.000Z',
105
+ runtimeId: 'runtime-1',
106
+ decision: { state: 'unknown' },
107
+ forward: { state: 'unknown' },
108
+ });
109
+ });
110
+
111
+ test('sanitizeAgentGateway rejects unsafe request ids before persistence', () => {
112
+ const unsafeInputs = [
113
+ 'req-1 apiKey=secret',
114
+ 'rt_secret_value',
115
+ 'sk-live-secret-token',
116
+ 'req-1\nprompt=leak',
117
+ ];
118
+
119
+ for (const requestId of unsafeInputs) {
120
+ const gateway = sanitizeAgentGateway(
121
+ {
122
+ requestId,
123
+ source: 'manual-check',
124
+ model: 'gpt-test',
125
+ },
126
+ {
127
+ runtimeId: 'runtime-1',
128
+ decision: { state: 'unknown' },
129
+ now: () => '2026-05-31T00:20:00.000Z',
130
+ requestId: () => 'generated-request-id',
131
+ },
132
+ );
133
+
134
+ assert.equal(gateway.requestId, 'generated-request-id');
135
+ assert.doesNotMatch(JSON.stringify(gateway), /apiKey|secret|rt_secret_value|sk-live|prompt|leak/);
136
+ }
137
+ });
138
+
139
+ test('evaluateGatewayDecision maps gate and route states', () => {
140
+ assert.deepEqual(
141
+ evaluateGatewayDecision(allowedDecision, {
142
+ state: 'ready',
143
+ expiresAt: '2999-05-31T00:10:00.000Z',
144
+ }),
145
+ {
146
+ state: 'accepted',
147
+ productId: 'prod_basic',
148
+ modeStartedAt: '2026-05-31T00:05:00.000Z',
149
+ route: { state: 'ready', expiresAt: '2999-05-31T00:10:00.000Z' },
150
+ },
151
+ );
152
+ assert.deepEqual(
153
+ evaluateGatewayDecision(allowedDecision, { state: 'missing' }),
154
+ {
155
+ state: 'route-missing',
156
+ productId: 'prod_basic',
157
+ modeStartedAt: '2026-05-31T00:05:00.000Z',
158
+ route: { state: 'missing' },
159
+ },
160
+ );
161
+ assert.deepEqual(
162
+ evaluateGatewayDecision(allowedDecision, {
163
+ state: 'expired',
164
+ expiresAt: '2026-05-31T00:04:00.000Z',
165
+ }),
166
+ {
167
+ state: 'route-expired',
168
+ productId: 'prod_basic',
169
+ modeStartedAt: '2026-05-31T00:05:00.000Z',
170
+ route: { state: 'expired', expiresAt: '2026-05-31T00:04:00.000Z' },
171
+ },
172
+ );
173
+ assert.deepEqual(
174
+ evaluateGatewayDecision(
175
+ { state: 'blocked', reason: 'logged-out' },
176
+ { state: 'ready', expiresAt: '2999-05-31T00:10:00.000Z' },
177
+ ),
178
+ {
179
+ state: 'blocked',
180
+ reason: 'logged-out',
181
+ route: { state: 'missing' },
182
+ },
183
+ );
184
+ assert.deepEqual(
185
+ evaluateGatewayDecision(
186
+ {
187
+ state: 'blocked',
188
+ reason: 'mode-released',
189
+ releaseReason: 'invalid-token',
190
+ releasedAt: '2026-05-31T00:10:00.000Z',
191
+ },
192
+ { state: 'missing' },
193
+ ),
194
+ {
195
+ state: 'blocked',
196
+ reason: 'mode-released',
197
+ releaseReason: 'invalid-token',
198
+ releasedAt: '2026-05-31T00:10:00.000Z',
199
+ route: { state: 'missing' },
200
+ },
201
+ );
202
+ });
203
+
204
+ test('createGatewayStore records and returns only the latest gateway', () => {
205
+ const store = createGatewayStore();
206
+ const first: AgentGateway = {
207
+ requestId: 'req-1',
208
+ requestType: 'agent',
209
+ source: 'manual-check',
210
+ model: 'unknown',
211
+ receivedAt: '2026-05-31T00:20:00.000Z',
212
+ runtimeId: 'runtime-1',
213
+ decision: { state: 'unknown' },
214
+ forward: { state: 'unknown' },
215
+ };
216
+ const second: AgentGateway = {
217
+ ...first,
218
+ requestId: 'req-2',
219
+ decision: {
220
+ state: 'route-missing',
221
+ productId: 'prod_basic',
222
+ modeStartedAt: '2026-05-31T00:05:00.000Z',
223
+ route: { state: 'missing' },
224
+ },
225
+ };
226
+
227
+ assert.equal(store.latest(), null);
228
+ assert.deepEqual(store.record(first), first);
229
+ assert.deepEqual(store.record(second), second);
230
+ assert.deepEqual(store.latest(), second);
231
+ });
232
+
233
+ test('resolveGatewayForward skips non-accepted gateway decisions', async () => {
234
+ let calls = 0;
235
+ const forwarder: GatewayForwarder = async () => {
236
+ calls += 1;
237
+ return {
238
+ state: 'forwarded',
239
+ upstreamRequestId: 'gw_req_1',
240
+ acceptedAt: '2026-05-31T00:30:00.000Z',
241
+ routeExpiresAt: '2999-05-31T00:10:00.000Z',
242
+ };
243
+ };
244
+
245
+ const forward = await resolveGatewayForward({
246
+ gateway: {
247
+ requestId: 'req-1',
248
+ requestType: 'agent',
249
+ source: 'manual-check',
250
+ model: 'gpt-test',
251
+ receivedAt: '2026-05-31T00:20:00.000Z',
252
+ runtimeId: 'runtime-1',
253
+ decision: {
254
+ state: 'route-missing',
255
+ productId: 'prod_basic',
256
+ modeStartedAt: '2026-05-31T00:05:00.000Z',
257
+ route: { state: 'missing' },
258
+ },
259
+ forward: { state: 'unknown' },
260
+ },
261
+ routeToken: 'rt_secret_value',
262
+ productId: 'prod_basic',
263
+ model: 'gpt-test',
264
+ requestId: 'req-1',
265
+ forwarder,
266
+ });
267
+
268
+ assert.equal(calls, 0);
269
+ assert.deepEqual(forward, { state: 'skipped', reason: 'not-accepted' });
270
+ });
271
+
272
+ test('resolveGatewayForward returns not-configured when no forwarder is injected', async () => {
273
+ const gateway: AgentGateway = {
274
+ requestId: 'req-1',
275
+ requestType: 'agent',
276
+ source: 'manual-check',
277
+ model: 'gpt-test',
278
+ receivedAt: '2026-05-31T00:20:00.000Z',
279
+ runtimeId: 'runtime-1',
280
+ decision: {
281
+ state: 'accepted',
282
+ productId: 'prod_basic',
283
+ modeStartedAt: '2026-05-31T00:05:00.000Z',
284
+ route: { state: 'ready', expiresAt: '2999-05-31T00:10:00.000Z' },
285
+ },
286
+ forward: { state: 'unknown' },
287
+ };
288
+
289
+ assert.deepEqual(await resolveGatewayForward({
290
+ gateway,
291
+ routeToken: 'rt_secret_value',
292
+ poolSessionId: 'pool_session_1',
293
+ productId: 'prod_basic',
294
+ model: 'gpt-test',
295
+ requestId: 'req-1',
296
+ }), { state: 'not-configured' });
297
+ });
298
+
299
+ test('createGatewayHttpForwarder sends client-response settlement mode', async () => {
300
+ let posted: Record<string, unknown> | null = null;
301
+ const api = await createApiServer((request, response) => {
302
+ assert.equal(request.url, '/api/client/gateway/agent');
303
+ const chunks: Buffer[] = [];
304
+ request.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
305
+ request.on('end', () => {
306
+ posted = JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>;
307
+ response.writeHead(200, { 'content-type': 'application/json' });
308
+ response.end(JSON.stringify({
309
+ state: 'forwarded',
310
+ upstreamRequestId: 'gw_req_1',
311
+ acceptedAt: '2026-05-31T00:30:00.000Z',
312
+ routeExpiresAt: '2999-05-31T00:10:00.000Z',
313
+ usageRequestId: 'ur_1',
314
+ }));
315
+ });
316
+ });
317
+
318
+ try {
319
+ const forwarder = createGatewayHttpForwarder({ apiBaseUrl: api.baseUrl, deviceToken: 'cp_dev_secret' });
320
+ const result = await forwarder({
321
+ gateway: {
322
+ requestId: 'req-1',
323
+ requestType: 'agent',
324
+ source: 'manual-check',
325
+ model: 'gpt-test',
326
+ receivedAt: '2026-05-31T00:20:00.000Z',
327
+ runtimeId: 'runtime-1',
328
+ decision: {
329
+ state: 'accepted',
330
+ productId: 'prod_basic',
331
+ modeStartedAt: '2026-05-31T00:05:00.000Z',
332
+ route: { state: 'ready', expiresAt: '2999-05-31T00:10:00.000Z' },
333
+ },
334
+ forward: { state: 'unknown' },
335
+ },
336
+ routeToken: 'rt_secret_value',
337
+ poolSessionId: 'pool_session_1',
338
+ productId: 'prod_basic',
339
+ model: 'gpt-test',
340
+ requestId: 'req-1',
341
+ settlementMode: 'client_response',
342
+ });
343
+
344
+ assert.equal(result.state, 'forwarded');
345
+ assert.equal(posted?.settlementMode, 'client_response');
346
+ } finally {
347
+ await api.close();
348
+ }
349
+ });
350
+
351
+ test('completeGatewayForward confirms client response settlement by request id', async () => {
352
+ let posted: Record<string, unknown> | null = null;
353
+ const api = await createApiServer((request, response) => {
354
+ assert.equal(request.url, '/api/client/gateway/agent/req-1/complete');
355
+ const chunks: Buffer[] = [];
356
+ request.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
357
+ request.on('end', () => {
358
+ posted = JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>;
359
+ response.writeHead(200, { 'content-type': 'application/json' });
360
+ response.end(JSON.stringify({
361
+ state: 'charged',
362
+ usageRequestId: 'ur_1',
363
+ chargeStatus: 'charged',
364
+ chargeCredits: 1,
365
+ }));
366
+ });
367
+ });
368
+
369
+ try {
370
+ const result = await completeGatewayForward({
371
+ apiBaseUrl: api.baseUrl,
372
+ deviceToken: 'cp_dev_secret',
373
+ requestId: 'req-1',
374
+ routeToken: 'rt_secret_value',
375
+ poolSessionId: 'pool_session_1',
376
+ productId: 'prod_basic',
377
+ });
378
+
379
+ assert.deepEqual(result, {
380
+ state: 'charged',
381
+ usageRequestId: 'ur_1',
382
+ chargeStatus: 'charged',
383
+ chargeCredits: 1,
384
+ });
385
+ assert.deepEqual(posted, {
386
+ routeToken: 'rt_secret_value',
387
+ poolSessionId: 'pool_session_1',
388
+ productId: 'prod_basic',
389
+ });
390
+ } finally {
391
+ await api.close();
392
+ }
393
+ });
394
+
395
+ test('sanitizeGatewayForward keeps safe forwarded metadata only', () => {
396
+ assert.deepEqual(
397
+ sanitizeGatewayForward({
398
+ state: 'forwarded',
399
+ upstreamRequestId: 'gw_req_1',
400
+ acceptedAt: '2026-05-31T00:30:00.000Z',
401
+ routeExpiresAt: '2999-05-31T00:10:00.000Z',
402
+ routeToken: 'rt_secret_value',
403
+ } as never),
404
+ {
405
+ state: 'forwarded',
406
+ upstreamRequestId: 'gw_req_1',
407
+ acceptedAt: '2026-05-31T00:30:00.000Z',
408
+ },
409
+ );
410
+ });
411
+
412
+ test('sanitizeGatewayForward keeps insufficient-credits reject reason', () => {
413
+ assert.deepEqual(
414
+ sanitizeGatewayForward({
415
+ state: 'rejected',
416
+ reason: 'insufficient-credits',
417
+ }),
418
+ {
419
+ state: 'rejected',
420
+ reason: 'insufficient-credits',
421
+ },
422
+ );
423
+ });
424
+
425
+ test('sanitizeGatewayForward downgrades unsafe forward metadata to unknown', () => {
426
+ assert.deepEqual(
427
+ sanitizeGatewayForward({
428
+ state: 'forwarded',
429
+ upstreamRequestId: 'gw_req_1 token=rt_secret_value',
430
+ acceptedAt: 'not-a-date',
431
+ }),
432
+ { state: 'unknown' },
433
+ );
434
+ assert.deepEqual(sanitizeGatewayForward({ state: 'made-up' }), { state: 'unknown' });
435
+ });
436
+
437
+ test('resolveGatewayForward maps thrown errors to network-error without message', async () => {
438
+ const gateway: AgentGateway = {
439
+ requestId: 'req-1',
440
+ requestType: 'agent',
441
+ source: 'manual-check',
442
+ model: 'gpt-test',
443
+ receivedAt: '2026-05-31T00:20:00.000Z',
444
+ runtimeId: 'runtime-1',
445
+ decision: {
446
+ state: 'accepted',
447
+ productId: 'prod_basic',
448
+ modeStartedAt: '2026-05-31T00:05:00.000Z',
449
+ route: { state: 'ready', expiresAt: '2999-05-31T00:10:00.000Z' },
450
+ },
451
+ forward: { state: 'unknown' },
452
+ };
453
+
454
+ const forward = await resolveGatewayForward({
455
+ gateway,
456
+ routeToken: 'rt_secret_value',
457
+ productId: 'prod_basic',
458
+ model: 'gpt-test',
459
+ requestId: 'req-1',
460
+ forwarder: async () => {
461
+ throw new Error('secret provider stack');
462
+ },
463
+ });
464
+
465
+ assert.deepEqual(forward, { state: 'network-error' });
466
+ });
@@ -0,0 +1,47 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+ import { readRuntimeInfo, writeRuntimeInfo } from '../src/runtime';
7
+
8
+ test('runtime info is written to and read from an injected runtime file', async () => {
9
+ const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-runtime-'));
10
+ const runtimeFile = join(tempDir, 'nested', 'runtime.json');
11
+
12
+ try {
13
+ const runtime = {
14
+ host: '127.0.0.1',
15
+ port: 3210,
16
+ runtimeId: 'runtime-test',
17
+ };
18
+
19
+ await writeRuntimeInfo(runtime, { runtimeFile });
20
+ assert.deepEqual(await readRuntimeInfo({ runtimeFile }), runtime);
21
+ } finally {
22
+ await rm(tempDir, { recursive: true, force: true });
23
+ }
24
+ });
25
+
26
+ test('readRuntimeInfo returns null when the runtime file does not exist', async () => {
27
+ const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-runtime-'));
28
+
29
+ try {
30
+ assert.equal(await readRuntimeInfo({ runtimeFile: join(tempDir, 'missing.json') }), null);
31
+ } finally {
32
+ await rm(tempDir, { recursive: true, force: true });
33
+ }
34
+ });
35
+
36
+ test('readRuntimeInfo returns null for incomplete runtime JSON', async () => {
37
+ const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-runtime-'));
38
+ const runtimeFile = join(tempDir, 'runtime.json');
39
+
40
+ try {
41
+ await writeFile(runtimeFile, '{"host":"127.0.0.1"', 'utf8');
42
+
43
+ assert.equal(await readRuntimeInfo({ runtimeFile }), null);
44
+ } finally {
45
+ await rm(tempDir, { recursive: true, force: true });
46
+ }
47
+ });