@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.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +17 -9
  2. package/CHANGELOG.md +64 -0
  3. package/Dockerfile +5 -1
  4. package/LICENSE +176 -0
  5. package/README.md +1 -1
  6. package/dist/dist-gVyG2H2h.js +612 -0
  7. package/dist/dist-gVyG2H2h.js.map +1 -0
  8. package/dist/index.d.ts +94 -1834
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +489 -678
  11. package/dist/index.js.map +1 -1
  12. package/dist/openai/index.d.ts +67 -0
  13. package/dist/openai/index.d.ts.map +1 -0
  14. package/dist/openai/index.js +362 -0
  15. package/dist/openai/index.js.map +1 -0
  16. package/dist/sandbox-B3vJ541e.d.ts +1729 -0
  17. package/dist/sandbox-B3vJ541e.d.ts.map +1 -0
  18. package/package.json +16 -2
  19. package/src/clients/base-client.ts +107 -46
  20. package/src/index.ts +19 -2
  21. package/src/openai/index.ts +465 -0
  22. package/src/request-handler.ts +2 -1
  23. package/src/sandbox.ts +684 -62
  24. package/src/storage-mount/credential-detection.ts +41 -0
  25. package/src/storage-mount/errors.ts +51 -0
  26. package/src/storage-mount/index.ts +17 -0
  27. package/src/storage-mount/provider-detection.ts +93 -0
  28. package/src/storage-mount/types.ts +17 -0
  29. package/src/version.ts +1 -1
  30. package/tests/base-client.test.ts +218 -0
  31. package/tests/get-sandbox.test.ts +24 -1
  32. package/tests/git-client.test.ts +7 -39
  33. package/tests/openai-shell-editor.test.ts +434 -0
  34. package/tests/port-client.test.ts +25 -35
  35. package/tests/process-client.test.ts +73 -107
  36. package/tests/sandbox.test.ts +128 -1
  37. package/tests/storage-mount/credential-detection.test.ts +119 -0
  38. package/tests/storage-mount/provider-detection.test.ts +77 -0
  39. package/tsconfig.json +2 -2
  40. 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
- GetProcessLogsResponse,
4
- GetProcessResponse,
5
- KillAllProcessesResponse,
6
- KillProcessResponse,
7
- ListProcessesResponse,
8
- StartProcessResponse
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: StartProcessResponse = {
40
+ const mockResponse: ProcessStartResult = {
41
41
  success: true,
42
- process: {
43
- id: 'proc-web-server',
44
- command: 'npm run dev',
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.process.command).toBe('npm run dev');
60
- expect(result.process.status).toBe('running');
61
- expect(result.process.pid).toBe(12345);
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: StartProcessResponse = {
61
+ const mockResponse: ProcessStartResult = {
67
62
  success: true,
68
- process: {
69
- id: 'my-api-server',
70
- command: 'python app.py',
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.process.id).toBe('my-api-server');
88
- expect(result.process.command).toBe('python app.py');
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: StartProcessResponse = {
83
+ const mockResponse: ProcessStartResult = {
94
84
  success: true,
95
- process: {
96
- id: 'proc-database',
97
- command: 'docker run postgres',
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.process.status).toBe('running');
125
- expect(result.process.command).toBe('docker run postgres');
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: ListProcessesResponse = {
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('session-list');
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: GetProcessResponse = {
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', 'session-get');
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
- client.getProcess('nonexistent-proc', 'session-err')
254
- ).rejects.toThrow(ProcessNotFoundError);
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: ListProcessesResponse = {
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('session-list');
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: KillProcessResponse = {
261
+ const mockResponse: ProcessKillResult = {
280
262
  success: true,
281
- message: 'Process proc-web killed successfully',
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', 'session-kill');
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
- client.killProcess('already-dead-proc', 'session-err')
308
- ).rejects.toThrow(ProcessNotFoundError);
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: KillAllProcessesResponse = {
292
+ const mockResponse: ProcessCleanupResult = {
313
293
  success: true,
314
- killedCount: 5,
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('session-killall');
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: KillAllProcessesResponse = {
308
+ const mockResponse: ProcessCleanupResult = {
332
309
  success: true,
333
- killedCount: 0,
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('session-killall');
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
- client.killProcess('protected-proc', 'session-err')
361
- ).rejects.toThrow(ProcessError);
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: GetProcessLogsResponse = {
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', 'session-logs');
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
- client.getProcessLogs('missing-proc', 'session-err')
407
- ).rejects.toThrow(ProcessNotFoundError);
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: GetProcessLogsResponse = {
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', 'session-logs');
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: GetProcessLogsResponse = {
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', 'session-logs');
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
- client.streamProcessLogs('stream-missing', 'session-err')
522
- ).rejects.toThrow(ProcessNotFoundError);
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
- client.streamProcessLogs('proc-no-logs', 'session-err')
537
- ).rejects.toThrow(ProcessError);
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', 'session-err')
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: StartProcessResponse = {
527
+ const mockResponse: ProcessStartResult = {
557
528
  success: true,
558
- process: {
559
- id: 'proc-session-test',
560
- command: 'echo session-test',
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('session-concurrent'),
636
- client.getProcessLogs('existing-proc', 'session-concurrent'),
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('session-err')).rejects.toThrow(
619
+ await expect(client.listProcesses()).rejects.toThrow(
654
620
  'Network connection failed'
655
621
  );
656
622
  });
@@ -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, mockEnv);
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
  });