@cloudflare/sandbox 0.4.18 → 0.5.2
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/.turbo/turbo-build.log +17 -9
- package/CHANGELOG.md +64 -0
- package/Dockerfile +5 -1
- package/LICENSE +176 -0
- package/README.md +1 -1
- package/dist/dist-gVyG2H2h.js +612 -0
- package/dist/dist-gVyG2H2h.js.map +1 -0
- package/dist/index.d.ts +94 -1834
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +489 -678
- package/dist/index.js.map +1 -1
- package/dist/openai/index.d.ts +67 -0
- package/dist/openai/index.d.ts.map +1 -0
- package/dist/openai/index.js +362 -0
- package/dist/openai/index.js.map +1 -0
- package/dist/sandbox-B3vJ541e.d.ts +1729 -0
- package/dist/sandbox-B3vJ541e.d.ts.map +1 -0
- package/package.json +16 -2
- package/src/clients/base-client.ts +107 -46
- package/src/index.ts +19 -2
- package/src/openai/index.ts +465 -0
- package/src/request-handler.ts +2 -1
- package/src/sandbox.ts +684 -62
- package/src/storage-mount/credential-detection.ts +41 -0
- package/src/storage-mount/errors.ts +51 -0
- package/src/storage-mount/index.ts +17 -0
- package/src/storage-mount/provider-detection.ts +93 -0
- package/src/storage-mount/types.ts +17 -0
- package/src/version.ts +1 -1
- package/tests/base-client.test.ts +218 -0
- package/tests/get-sandbox.test.ts +24 -1
- package/tests/git-client.test.ts +7 -39
- package/tests/openai-shell-editor.test.ts +434 -0
- package/tests/port-client.test.ts +25 -35
- package/tests/process-client.test.ts +73 -107
- package/tests/sandbox.test.ts +128 -1
- package/tests/storage-mount/credential-detection.test.ts +119 -0
- package/tests/storage-mount/provider-detection.test.ts +77 -0
- package/tsconfig.json +2 -2
- package/tsdown.config.ts +3 -2
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
ProcessCleanupResult,
|
|
4
|
+
ProcessInfoResult,
|
|
5
|
+
ProcessKillResult,
|
|
6
|
+
ProcessListResult,
|
|
7
|
+
ProcessLogsResult,
|
|
8
|
+
ProcessStartResult
|
|
9
9
|
} from '../src/clients';
|
|
10
10
|
import { ProcessClient } from '../src/clients/process-client';
|
|
11
11
|
import {
|
|
@@ -37,15 +37,11 @@ describe('ProcessClient', () => {
|
|
|
37
37
|
|
|
38
38
|
describe('process lifecycle management', () => {
|
|
39
39
|
it('should start background processes successfully', async () => {
|
|
40
|
-
const mockResponse:
|
|
40
|
+
const mockResponse: ProcessStartResult = {
|
|
41
41
|
success: true,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
status: 'running',
|
|
46
|
-
pid: 12345,
|
|
47
|
-
startTime: '2023-01-01T00:00:00Z'
|
|
48
|
-
},
|
|
42
|
+
processId: 'proc-web-server',
|
|
43
|
+
command: 'npm run dev',
|
|
44
|
+
pid: 12345,
|
|
49
45
|
timestamp: '2023-01-01T00:00:00Z'
|
|
50
46
|
};
|
|
51
47
|
|
|
@@ -56,22 +52,17 @@ describe('ProcessClient', () => {
|
|
|
56
52
|
const result = await client.startProcess('npm run dev', 'session-123');
|
|
57
53
|
|
|
58
54
|
expect(result.success).toBe(true);
|
|
59
|
-
expect(result.
|
|
60
|
-
expect(result.
|
|
61
|
-
expect(result.
|
|
62
|
-
expect(result.process.id).toBe('proc-web-server');
|
|
55
|
+
expect(result.command).toBe('npm run dev');
|
|
56
|
+
expect(result.pid).toBe(12345);
|
|
57
|
+
expect(result.processId).toBe('proc-web-server');
|
|
63
58
|
});
|
|
64
59
|
|
|
65
60
|
it('should start processes with custom process IDs', async () => {
|
|
66
|
-
const mockResponse:
|
|
61
|
+
const mockResponse: ProcessStartResult = {
|
|
67
62
|
success: true,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
status: 'running',
|
|
72
|
-
pid: 54321,
|
|
73
|
-
startTime: '2023-01-01T00:00:00Z'
|
|
74
|
-
},
|
|
63
|
+
processId: 'my-api-server',
|
|
64
|
+
command: 'python app.py',
|
|
65
|
+
pid: 54321,
|
|
75
66
|
timestamp: '2023-01-01T00:00:00Z'
|
|
76
67
|
};
|
|
77
68
|
|
|
@@ -84,21 +75,16 @@ describe('ProcessClient', () => {
|
|
|
84
75
|
});
|
|
85
76
|
|
|
86
77
|
expect(result.success).toBe(true);
|
|
87
|
-
expect(result.
|
|
88
|
-
expect(result.
|
|
89
|
-
expect(result.process.status).toBe('running');
|
|
78
|
+
expect(result.processId).toBe('my-api-server');
|
|
79
|
+
expect(result.command).toBe('python app.py');
|
|
90
80
|
});
|
|
91
81
|
|
|
92
82
|
it('should handle long-running process startup', async () => {
|
|
93
|
-
const mockResponse:
|
|
83
|
+
const mockResponse: ProcessStartResult = {
|
|
94
84
|
success: true,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
status: 'running',
|
|
99
|
-
pid: 99999,
|
|
100
|
-
startTime: '2023-01-01T00:00:00Z'
|
|
101
|
-
},
|
|
85
|
+
processId: 'proc-database',
|
|
86
|
+
command: 'docker run postgres',
|
|
87
|
+
pid: 99999,
|
|
102
88
|
timestamp: '2023-01-01T00:00:05Z'
|
|
103
89
|
};
|
|
104
90
|
|
|
@@ -121,8 +107,8 @@ describe('ProcessClient', () => {
|
|
|
121
107
|
);
|
|
122
108
|
|
|
123
109
|
expect(result.success).toBe(true);
|
|
124
|
-
expect(result.
|
|
125
|
-
expect(result.
|
|
110
|
+
expect(result.processId).toBe('proc-database');
|
|
111
|
+
expect(result.command).toBe('docker run postgres');
|
|
126
112
|
});
|
|
127
113
|
|
|
128
114
|
it('should handle command not found errors', async () => {
|
|
@@ -158,7 +144,7 @@ describe('ProcessClient', () => {
|
|
|
158
144
|
|
|
159
145
|
describe('process monitoring and inspection', () => {
|
|
160
146
|
it('should list running processes', async () => {
|
|
161
|
-
const mockResponse:
|
|
147
|
+
const mockResponse: ProcessListResult = {
|
|
162
148
|
success: true,
|
|
163
149
|
processes: [
|
|
164
150
|
{
|
|
@@ -185,7 +171,6 @@ describe('ProcessClient', () => {
|
|
|
185
171
|
endTime: '2023-01-01T00:05:00Z'
|
|
186
172
|
}
|
|
187
173
|
],
|
|
188
|
-
count: 3,
|
|
189
174
|
timestamp: '2023-01-01T00:05:30Z'
|
|
190
175
|
};
|
|
191
176
|
|
|
@@ -193,10 +178,9 @@ describe('ProcessClient', () => {
|
|
|
193
178
|
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
194
179
|
);
|
|
195
180
|
|
|
196
|
-
const result = await client.listProcesses(
|
|
181
|
+
const result = await client.listProcesses();
|
|
197
182
|
|
|
198
183
|
expect(result.success).toBe(true);
|
|
199
|
-
expect(result.count).toBe(3);
|
|
200
184
|
expect(result.processes).toHaveLength(3);
|
|
201
185
|
|
|
202
186
|
const runningProcesses = result.processes.filter(
|
|
@@ -214,7 +198,7 @@ describe('ProcessClient', () => {
|
|
|
214
198
|
});
|
|
215
199
|
|
|
216
200
|
it('should get specific process details', async () => {
|
|
217
|
-
const mockResponse:
|
|
201
|
+
const mockResponse: ProcessInfoResult = {
|
|
218
202
|
success: true,
|
|
219
203
|
process: {
|
|
220
204
|
id: 'proc-analytics',
|
|
@@ -230,7 +214,7 @@ describe('ProcessClient', () => {
|
|
|
230
214
|
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
231
215
|
);
|
|
232
216
|
|
|
233
|
-
const result = await client.getProcess('proc-analytics'
|
|
217
|
+
const result = await client.getProcess('proc-analytics');
|
|
234
218
|
|
|
235
219
|
expect(result.success).toBe(true);
|
|
236
220
|
expect(result.process.id).toBe('proc-analytics');
|
|
@@ -249,16 +233,15 @@ describe('ProcessClient', () => {
|
|
|
249
233
|
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
250
234
|
);
|
|
251
235
|
|
|
252
|
-
await expect(
|
|
253
|
-
|
|
254
|
-
)
|
|
236
|
+
await expect(client.getProcess('nonexistent-proc')).rejects.toThrow(
|
|
237
|
+
ProcessNotFoundError
|
|
238
|
+
);
|
|
255
239
|
});
|
|
256
240
|
|
|
257
241
|
it('should handle empty process list', async () => {
|
|
258
|
-
const mockResponse:
|
|
242
|
+
const mockResponse: ProcessListResult = {
|
|
259
243
|
success: true,
|
|
260
244
|
processes: [],
|
|
261
|
-
count: 0,
|
|
262
245
|
timestamp: '2023-01-01T00:00:00Z'
|
|
263
246
|
};
|
|
264
247
|
|
|
@@ -266,19 +249,18 @@ describe('ProcessClient', () => {
|
|
|
266
249
|
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
267
250
|
);
|
|
268
251
|
|
|
269
|
-
const result = await client.listProcesses(
|
|
252
|
+
const result = await client.listProcesses();
|
|
270
253
|
|
|
271
254
|
expect(result.success).toBe(true);
|
|
272
|
-
expect(result.count).toBe(0);
|
|
273
255
|
expect(result.processes).toHaveLength(0);
|
|
274
256
|
});
|
|
275
257
|
});
|
|
276
258
|
|
|
277
259
|
describe('process termination', () => {
|
|
278
260
|
it('should kill individual processes', async () => {
|
|
279
|
-
const mockResponse:
|
|
261
|
+
const mockResponse: ProcessKillResult = {
|
|
280
262
|
success: true,
|
|
281
|
-
|
|
263
|
+
processId: 'test-process',
|
|
282
264
|
timestamp: '2023-01-01T00:10:00Z'
|
|
283
265
|
};
|
|
284
266
|
|
|
@@ -286,11 +268,9 @@ describe('ProcessClient', () => {
|
|
|
286
268
|
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
287
269
|
);
|
|
288
270
|
|
|
289
|
-
const result = await client.killProcess('proc-web'
|
|
271
|
+
const result = await client.killProcess('proc-web');
|
|
290
272
|
|
|
291
273
|
expect(result.success).toBe(true);
|
|
292
|
-
expect(result.message).toContain('killed successfully');
|
|
293
|
-
expect(result.message).toContain('proc-web');
|
|
294
274
|
});
|
|
295
275
|
|
|
296
276
|
it('should handle kill non-existent process', async () => {
|
|
@@ -303,16 +283,15 @@ describe('ProcessClient', () => {
|
|
|
303
283
|
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
304
284
|
);
|
|
305
285
|
|
|
306
|
-
await expect(
|
|
307
|
-
|
|
308
|
-
)
|
|
286
|
+
await expect(client.killProcess('already-dead-proc')).rejects.toThrow(
|
|
287
|
+
ProcessNotFoundError
|
|
288
|
+
);
|
|
309
289
|
});
|
|
310
290
|
|
|
311
291
|
it('should kill all processes at once', async () => {
|
|
312
|
-
const mockResponse:
|
|
292
|
+
const mockResponse: ProcessCleanupResult = {
|
|
313
293
|
success: true,
|
|
314
|
-
|
|
315
|
-
message: 'All 5 processes killed successfully',
|
|
294
|
+
cleanedCount: 0,
|
|
316
295
|
timestamp: '2023-01-01T00:15:00Z'
|
|
317
296
|
};
|
|
318
297
|
|
|
@@ -320,18 +299,15 @@ describe('ProcessClient', () => {
|
|
|
320
299
|
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
321
300
|
);
|
|
322
301
|
|
|
323
|
-
const result = await client.killAllProcesses(
|
|
302
|
+
const result = await client.killAllProcesses();
|
|
324
303
|
|
|
325
304
|
expect(result.success).toBe(true);
|
|
326
|
-
expect(result.killedCount).toBe(5);
|
|
327
|
-
expect(result.message).toContain('All 5 processes killed');
|
|
328
305
|
});
|
|
329
306
|
|
|
330
307
|
it('should handle kill all when no processes running', async () => {
|
|
331
|
-
const mockResponse:
|
|
308
|
+
const mockResponse: ProcessCleanupResult = {
|
|
332
309
|
success: true,
|
|
333
|
-
|
|
334
|
-
message: 'No processes to kill',
|
|
310
|
+
cleanedCount: 0,
|
|
335
311
|
timestamp: '2023-01-01T00:00:00Z'
|
|
336
312
|
};
|
|
337
313
|
|
|
@@ -339,11 +315,9 @@ describe('ProcessClient', () => {
|
|
|
339
315
|
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
340
316
|
);
|
|
341
317
|
|
|
342
|
-
const result = await client.killAllProcesses(
|
|
318
|
+
const result = await client.killAllProcesses();
|
|
343
319
|
|
|
344
320
|
expect(result.success).toBe(true);
|
|
345
|
-
expect(result.killedCount).toBe(0);
|
|
346
|
-
expect(result.message).toContain('No processes to kill');
|
|
347
321
|
});
|
|
348
322
|
|
|
349
323
|
it('should handle kill failures', async () => {
|
|
@@ -356,15 +330,15 @@ describe('ProcessClient', () => {
|
|
|
356
330
|
new Response(JSON.stringify(errorResponse), { status: 500 })
|
|
357
331
|
);
|
|
358
332
|
|
|
359
|
-
await expect(
|
|
360
|
-
|
|
361
|
-
)
|
|
333
|
+
await expect(client.killProcess('protected-proc')).rejects.toThrow(
|
|
334
|
+
ProcessError
|
|
335
|
+
);
|
|
362
336
|
});
|
|
363
337
|
});
|
|
364
338
|
|
|
365
339
|
describe('process log management', () => {
|
|
366
340
|
it('should retrieve process logs', async () => {
|
|
367
|
-
const mockResponse:
|
|
341
|
+
const mockResponse: ProcessLogsResult = {
|
|
368
342
|
success: true,
|
|
369
343
|
processId: 'proc-server',
|
|
370
344
|
stdout: `Server starting...
|
|
@@ -382,7 +356,7 @@ describe('ProcessClient', () => {
|
|
|
382
356
|
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
383
357
|
);
|
|
384
358
|
|
|
385
|
-
const result = await client.getProcessLogs('proc-server'
|
|
359
|
+
const result = await client.getProcessLogs('proc-server');
|
|
386
360
|
|
|
387
361
|
expect(result.success).toBe(true);
|
|
388
362
|
expect(result.processId).toBe('proc-server');
|
|
@@ -402,16 +376,16 @@ describe('ProcessClient', () => {
|
|
|
402
376
|
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
403
377
|
);
|
|
404
378
|
|
|
405
|
-
await expect(
|
|
406
|
-
|
|
407
|
-
)
|
|
379
|
+
await expect(client.getProcessLogs('missing-proc')).rejects.toThrow(
|
|
380
|
+
ProcessNotFoundError
|
|
381
|
+
);
|
|
408
382
|
});
|
|
409
383
|
|
|
410
384
|
it('should retrieve logs for processes with large output', async () => {
|
|
411
385
|
const largeStdout = 'Log entry with details\n'.repeat(10000);
|
|
412
386
|
const largeStderr = 'Error trace line\n'.repeat(1000);
|
|
413
387
|
|
|
414
|
-
const mockResponse:
|
|
388
|
+
const mockResponse: ProcessLogsResult = {
|
|
415
389
|
success: true,
|
|
416
390
|
processId: 'proc-batch',
|
|
417
391
|
stdout: largeStdout,
|
|
@@ -423,7 +397,7 @@ describe('ProcessClient', () => {
|
|
|
423
397
|
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
424
398
|
);
|
|
425
399
|
|
|
426
|
-
const result = await client.getProcessLogs('proc-batch'
|
|
400
|
+
const result = await client.getProcessLogs('proc-batch');
|
|
427
401
|
|
|
428
402
|
expect(result.success).toBe(true);
|
|
429
403
|
expect(result.stdout.length).toBeGreaterThan(200000);
|
|
@@ -433,7 +407,7 @@ describe('ProcessClient', () => {
|
|
|
433
407
|
});
|
|
434
408
|
|
|
435
409
|
it('should handle empty process logs', async () => {
|
|
436
|
-
const mockResponse:
|
|
410
|
+
const mockResponse: ProcessLogsResult = {
|
|
437
411
|
success: true,
|
|
438
412
|
processId: 'proc-silent',
|
|
439
413
|
stdout: '',
|
|
@@ -445,7 +419,7 @@ describe('ProcessClient', () => {
|
|
|
445
419
|
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
446
420
|
);
|
|
447
421
|
|
|
448
|
-
const result = await client.getProcessLogs('proc-silent'
|
|
422
|
+
const result = await client.getProcessLogs('proc-silent');
|
|
449
423
|
|
|
450
424
|
expect(result.success).toBe(true);
|
|
451
425
|
expect(result.stdout).toBe('');
|
|
@@ -480,10 +454,7 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0
|
|
|
480
454
|
})
|
|
481
455
|
);
|
|
482
456
|
|
|
483
|
-
const stream = await client.streamProcessLogs(
|
|
484
|
-
'proc-realtime',
|
|
485
|
-
'session-stream'
|
|
486
|
-
);
|
|
457
|
+
const stream = await client.streamProcessLogs('proc-realtime');
|
|
487
458
|
|
|
488
459
|
expect(stream).toBeInstanceOf(ReadableStream);
|
|
489
460
|
|
|
@@ -517,9 +488,9 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0
|
|
|
517
488
|
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
518
489
|
);
|
|
519
490
|
|
|
520
|
-
await expect(
|
|
521
|
-
|
|
522
|
-
)
|
|
491
|
+
await expect(client.streamProcessLogs('stream-missing')).rejects.toThrow(
|
|
492
|
+
ProcessNotFoundError
|
|
493
|
+
);
|
|
523
494
|
});
|
|
524
495
|
|
|
525
496
|
it('should handle streaming setup failures', async () => {
|
|
@@ -532,9 +503,9 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0
|
|
|
532
503
|
new Response(JSON.stringify(errorResponse), { status: 500 })
|
|
533
504
|
);
|
|
534
505
|
|
|
535
|
-
await expect(
|
|
536
|
-
|
|
537
|
-
)
|
|
506
|
+
await expect(client.streamProcessLogs('proc-no-logs')).rejects.toThrow(
|
|
507
|
+
ProcessError
|
|
508
|
+
);
|
|
538
509
|
});
|
|
539
510
|
|
|
540
511
|
it('should handle missing stream body', async () => {
|
|
@@ -546,22 +517,18 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0
|
|
|
546
517
|
);
|
|
547
518
|
|
|
548
519
|
await expect(
|
|
549
|
-
client.streamProcessLogs('proc-empty-stream'
|
|
520
|
+
client.streamProcessLogs('proc-empty-stream')
|
|
550
521
|
).rejects.toThrow('No response body for streaming');
|
|
551
522
|
});
|
|
552
523
|
});
|
|
553
524
|
|
|
554
525
|
describe('session integration', () => {
|
|
555
526
|
it('should include session in process operations', async () => {
|
|
556
|
-
const mockResponse:
|
|
527
|
+
const mockResponse: ProcessStartResult = {
|
|
557
528
|
success: true,
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
status: 'running',
|
|
562
|
-
pid: 11111,
|
|
563
|
-
startTime: '2023-01-01T00:00:00Z'
|
|
564
|
-
},
|
|
529
|
+
processId: 'proc-session-test',
|
|
530
|
+
command: 'echo session-test',
|
|
531
|
+
pid: 11111,
|
|
565
532
|
timestamp: '2023-01-01T00:00:00Z'
|
|
566
533
|
};
|
|
567
534
|
|
|
@@ -608,7 +575,6 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0
|
|
|
608
575
|
JSON.stringify({
|
|
609
576
|
success: true,
|
|
610
577
|
processes: [],
|
|
611
|
-
count: 0,
|
|
612
578
|
timestamp: new Date().toISOString()
|
|
613
579
|
})
|
|
614
580
|
)
|
|
@@ -632,8 +598,8 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0
|
|
|
632
598
|
const operations = await Promise.all([
|
|
633
599
|
client.startProcess('npm run dev', 'session-concurrent'),
|
|
634
600
|
client.startProcess('python api.py', 'session-concurrent'),
|
|
635
|
-
client.listProcesses(
|
|
636
|
-
client.getProcessLogs('existing-proc'
|
|
601
|
+
client.listProcesses(),
|
|
602
|
+
client.getProcessLogs('existing-proc'),
|
|
637
603
|
client.startProcess('node worker.js', 'session-concurrent')
|
|
638
604
|
]);
|
|
639
605
|
|
|
@@ -650,7 +616,7 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0
|
|
|
650
616
|
it('should handle network failures gracefully', async () => {
|
|
651
617
|
mockFetch.mockRejectedValue(new Error('Network connection failed'));
|
|
652
618
|
|
|
653
|
-
await expect(client.listProcesses(
|
|
619
|
+
await expect(client.listProcesses()).rejects.toThrow(
|
|
654
620
|
'Network connection failed'
|
|
655
621
|
);
|
|
656
622
|
});
|
package/tests/sandbox.test.ts
CHANGED
|
@@ -42,6 +42,10 @@ vi.mock('@cloudflare/containers', () => {
|
|
|
42
42
|
// Mock implementation for HTTP path
|
|
43
43
|
return new Response('Mock Container HTTP fetch');
|
|
44
44
|
}
|
|
45
|
+
async getState() {
|
|
46
|
+
// Mock implementation - return healthy state
|
|
47
|
+
return { status: 'healthy' };
|
|
48
|
+
}
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
return {
|
|
@@ -72,6 +76,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
72
76
|
.mockImplementation(
|
|
73
77
|
<T>(callback: () => Promise<T>): Promise<T> => callback()
|
|
74
78
|
),
|
|
79
|
+
waitUntil: vi.fn(),
|
|
75
80
|
id: {
|
|
76
81
|
toString: () => 'test-sandbox-id',
|
|
77
82
|
equals: vi.fn(),
|
|
@@ -82,7 +87,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
82
87
|
mockEnv = {};
|
|
83
88
|
|
|
84
89
|
// Create Sandbox instance - SandboxClient is created internally
|
|
85
|
-
const stub = new Sandbox(mockCtx
|
|
90
|
+
const stub = new Sandbox(mockCtx as DurableObjectState<{}>, mockEnv);
|
|
86
91
|
|
|
87
92
|
// Wait for blockConcurrencyWhile to complete
|
|
88
93
|
await vi.waitFor(() => {
|
|
@@ -736,4 +741,126 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
736
741
|
expect(result.sessionId).toBe('custom-session');
|
|
737
742
|
});
|
|
738
743
|
});
|
|
744
|
+
|
|
745
|
+
describe('constructPreviewUrl validation', () => {
|
|
746
|
+
it('should throw clear error for ID with uppercase letters without normalizeId', async () => {
|
|
747
|
+
await sandbox.setSandboxName('MyProject-123', false);
|
|
748
|
+
|
|
749
|
+
vi.spyOn(sandbox.client.ports, 'exposePort').mockResolvedValue({
|
|
750
|
+
success: true,
|
|
751
|
+
port: 8080,
|
|
752
|
+
url: '',
|
|
753
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
await expect(
|
|
757
|
+
sandbox.exposePort(8080, { hostname: 'example.com' })
|
|
758
|
+
).rejects.toThrow(/Preview URLs require lowercase sandbox IDs/);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it('should construct valid URL for lowercase ID', async () => {
|
|
762
|
+
await sandbox.setSandboxName('my-project', false);
|
|
763
|
+
|
|
764
|
+
vi.spyOn(sandbox.client.ports, 'exposePort').mockResolvedValue({
|
|
765
|
+
success: true,
|
|
766
|
+
port: 8080,
|
|
767
|
+
url: '',
|
|
768
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
const result = await sandbox.exposePort(8080, {
|
|
772
|
+
hostname: 'example.com'
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
expect(result.url).toMatch(
|
|
776
|
+
/^https:\/\/8080-my-project-[a-z0-9_-]{16}\.example\.com\/?$/
|
|
777
|
+
);
|
|
778
|
+
expect(result.port).toBe(8080);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('should construct valid URL with normalized ID', async () => {
|
|
782
|
+
await sandbox.setSandboxName('myproject-123', true);
|
|
783
|
+
|
|
784
|
+
vi.spyOn(sandbox.client.ports, 'exposePort').mockResolvedValue({
|
|
785
|
+
success: true,
|
|
786
|
+
port: 4000,
|
|
787
|
+
url: '',
|
|
788
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
const result = await sandbox.exposePort(4000, { hostname: 'my-app.dev' });
|
|
792
|
+
|
|
793
|
+
expect(result.url).toMatch(
|
|
794
|
+
/^https:\/\/4000-myproject-123-[a-z0-9_-]{16}\.my-app\.dev\/?$/
|
|
795
|
+
);
|
|
796
|
+
expect(result.port).toBe(4000);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('should construct valid localhost URL', async () => {
|
|
800
|
+
await sandbox.setSandboxName('test-sandbox', false);
|
|
801
|
+
|
|
802
|
+
vi.spyOn(sandbox.client.ports, 'exposePort').mockResolvedValue({
|
|
803
|
+
success: true,
|
|
804
|
+
port: 8080,
|
|
805
|
+
url: '',
|
|
806
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
const result = await sandbox.exposePort(8080, {
|
|
810
|
+
hostname: 'localhost:3000'
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
expect(result.url).toMatch(
|
|
814
|
+
/^http:\/\/8080-test-sandbox-[a-z0-9_-]{16}\.localhost:3000\/?$/
|
|
815
|
+
);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('should include helpful guidance in error message', async () => {
|
|
819
|
+
await sandbox.setSandboxName('MyProject-ABC', false);
|
|
820
|
+
|
|
821
|
+
vi.spyOn(sandbox.client.ports, 'exposePort').mockResolvedValue({
|
|
822
|
+
success: true,
|
|
823
|
+
port: 8080,
|
|
824
|
+
url: '',
|
|
825
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
await expect(
|
|
829
|
+
sandbox.exposePort(8080, { hostname: 'example.com' })
|
|
830
|
+
).rejects.toThrow(
|
|
831
|
+
/getSandbox\(ns, "MyProject-ABC", \{ normalizeId: true \}\)/
|
|
832
|
+
);
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
describe('timeout configuration validation', () => {
|
|
837
|
+
it('should reject invalid timeout values', async () => {
|
|
838
|
+
// NaN, Infinity, and out-of-range values should all be rejected
|
|
839
|
+
await expect(
|
|
840
|
+
sandbox.setContainerTimeouts({ instanceGetTimeoutMS: NaN })
|
|
841
|
+
).rejects.toThrow();
|
|
842
|
+
|
|
843
|
+
await expect(
|
|
844
|
+
sandbox.setContainerTimeouts({ portReadyTimeoutMS: Infinity })
|
|
845
|
+
).rejects.toThrow();
|
|
846
|
+
|
|
847
|
+
await expect(
|
|
848
|
+
sandbox.setContainerTimeouts({ instanceGetTimeoutMS: -1 })
|
|
849
|
+
).rejects.toThrow();
|
|
850
|
+
|
|
851
|
+
await expect(
|
|
852
|
+
sandbox.setContainerTimeouts({ waitIntervalMS: 999_999 })
|
|
853
|
+
).rejects.toThrow();
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('should accept valid timeout values', async () => {
|
|
857
|
+
await expect(
|
|
858
|
+
sandbox.setContainerTimeouts({
|
|
859
|
+
instanceGetTimeoutMS: 30_000,
|
|
860
|
+
portReadyTimeoutMS: 90_000,
|
|
861
|
+
waitIntervalMS: 1000
|
|
862
|
+
})
|
|
863
|
+
).resolves.toBeUndefined();
|
|
864
|
+
});
|
|
865
|
+
});
|
|
739
866
|
});
|