@cloudflare/sandbox 0.0.0-0b4cc05 → 0.0.0-102fc4f

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 (76) hide show
  1. package/CHANGELOG.md +176 -15
  2. package/Dockerfile +88 -71
  3. package/LICENSE +176 -0
  4. package/README.md +10 -5
  5. package/dist/index.d.ts +1953 -9
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3278 -53
  8. package/dist/index.js.map +1 -1
  9. package/package.json +11 -9
  10. package/src/clients/base-client.ts +39 -24
  11. package/src/clients/command-client.ts +8 -8
  12. package/src/clients/file-client.ts +51 -20
  13. package/src/clients/git-client.ts +9 -3
  14. package/src/clients/index.ts +16 -15
  15. package/src/clients/interpreter-client.ts +51 -47
  16. package/src/clients/port-client.ts +10 -10
  17. package/src/clients/process-client.ts +11 -8
  18. package/src/clients/sandbox-client.ts +2 -4
  19. package/src/clients/types.ts +6 -2
  20. package/src/clients/utility-client.ts +67 -5
  21. package/src/errors/adapter.ts +90 -32
  22. package/src/errors/classes.ts +189 -64
  23. package/src/errors/index.ts +9 -5
  24. package/src/file-stream.ts +11 -6
  25. package/src/index.ts +28 -17
  26. package/src/interpreter.ts +50 -41
  27. package/src/request-handler.ts +34 -21
  28. package/src/sandbox.ts +516 -145
  29. package/src/security.ts +21 -6
  30. package/src/sse-parser.ts +4 -3
  31. package/src/version.ts +6 -0
  32. package/startup.sh +1 -1
  33. package/tests/base-client.test.ts +116 -80
  34. package/tests/command-client.test.ts +149 -112
  35. package/tests/file-client.test.ts +373 -185
  36. package/tests/file-stream.test.ts +24 -20
  37. package/tests/get-sandbox.test.ts +149 -0
  38. package/tests/git-client.test.ts +260 -101
  39. package/tests/port-client.test.ts +100 -108
  40. package/tests/process-client.test.ts +204 -179
  41. package/tests/request-handler.test.ts +292 -0
  42. package/tests/sandbox.test.ts +336 -62
  43. package/tests/sse-parser.test.ts +17 -16
  44. package/tests/utility-client.test.ts +129 -56
  45. package/tests/version.test.ts +16 -0
  46. package/tsdown.config.ts +12 -0
  47. package/vitest.config.ts +6 -6
  48. package/dist/chunk-BCJ7SF3Q.js +0 -117
  49. package/dist/chunk-BCJ7SF3Q.js.map +0 -1
  50. package/dist/chunk-BFVUNTP4.js +0 -104
  51. package/dist/chunk-BFVUNTP4.js.map +0 -1
  52. package/dist/chunk-EKSWCBCA.js +0 -86
  53. package/dist/chunk-EKSWCBCA.js.map +0 -1
  54. package/dist/chunk-U2M5GSMU.js +0 -2220
  55. package/dist/chunk-U2M5GSMU.js.map +0 -1
  56. package/dist/chunk-Z532A7QC.js +0 -78
  57. package/dist/chunk-Z532A7QC.js.map +0 -1
  58. package/dist/file-stream.d.ts +0 -43
  59. package/dist/file-stream.js +0 -9
  60. package/dist/file-stream.js.map +0 -1
  61. package/dist/interpreter.d.ts +0 -33
  62. package/dist/interpreter.js +0 -8
  63. package/dist/interpreter.js.map +0 -1
  64. package/dist/request-handler.d.ts +0 -18
  65. package/dist/request-handler.js +0 -12
  66. package/dist/request-handler.js.map +0 -1
  67. package/dist/sandbox-Cyuj5F-M.d.ts +0 -579
  68. package/dist/sandbox.d.ts +0 -4
  69. package/dist/sandbox.js +0 -12
  70. package/dist/sandbox.js.map +0 -1
  71. package/dist/security.d.ts +0 -31
  72. package/dist/security.js +0 -13
  73. package/dist/security.js.map +0 -1
  74. package/dist/sse-parser.d.ts +0 -28
  75. package/dist/sse-parser.js +0 -11
  76. package/dist/sse-parser.js.map +0 -1
@@ -1,27 +1,59 @@
1
+ import { Container } from '@cloudflare/containers';
1
2
  import type { DurableObjectState } from '@cloudflare/workers-types';
2
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { Sandbox } from '../src/sandbox';
4
+ import { connect, Sandbox } from '../src/sandbox';
4
5
 
5
6
  // Mock dependencies before imports
6
7
  vi.mock('./interpreter', () => ({
7
- CodeInterpreter: vi.fn().mockImplementation(() => ({})),
8
+ CodeInterpreter: vi.fn().mockImplementation(() => ({}))
8
9
  }));
9
10
 
10
- vi.mock('@cloudflare/containers', () => ({
11
- Container: class Container {
11
+ vi.mock('@cloudflare/containers', () => {
12
+ const mockSwitchPort = vi.fn((request: Request, port: number) => {
13
+ // Create a new request with the port in the URL path
14
+ const url = new URL(request.url);
15
+ url.pathname = `/proxy/${port}${url.pathname}`;
16
+ return new Request(url, request);
17
+ });
18
+
19
+ const MockContainer = class Container {
12
20
  ctx: any;
13
21
  env: any;
14
22
  constructor(ctx: any, env: any) {
15
23
  this.ctx = ctx;
16
24
  this.env = env;
17
25
  }
18
- },
19
- getContainer: vi.fn(),
20
- }));
26
+ async fetch(request: Request): Promise<Response> {
27
+ // Mock implementation - will be spied on in tests
28
+ const upgradeHeader = request.headers.get('Upgrade');
29
+ if (upgradeHeader?.toLowerCase() === 'websocket') {
30
+ return new Response('WebSocket Upgraded', {
31
+ status: 200,
32
+ headers: {
33
+ 'X-WebSocket-Upgraded': 'true',
34
+ Upgrade: 'websocket',
35
+ Connection: 'Upgrade'
36
+ }
37
+ });
38
+ }
39
+ return new Response('Mock Container fetch');
40
+ }
41
+ async containerFetch(request: Request, port: number): Promise<Response> {
42
+ // Mock implementation for HTTP path
43
+ return new Response('Mock Container HTTP fetch');
44
+ }
45
+ };
46
+
47
+ return {
48
+ Container: MockContainer,
49
+ getContainer: vi.fn(),
50
+ switchPort: mockSwitchPort
51
+ };
52
+ });
21
53
 
22
54
  describe('Sandbox - Automatic Session Management', () => {
23
55
  let sandbox: Sandbox;
24
- let mockCtx: Partial<DurableObjectState>;
56
+ let mockCtx: Partial<DurableObjectState<{}>>;
25
57
  let mockEnv: any;
26
58
 
27
59
  beforeEach(async () => {
@@ -33,31 +65,39 @@ describe('Sandbox - Automatic Session Management', () => {
33
65
  get: vi.fn().mockResolvedValue(null),
34
66
  put: vi.fn().mockResolvedValue(undefined),
35
67
  delete: vi.fn().mockResolvedValue(undefined),
36
- list: vi.fn().mockResolvedValue(new Map()),
68
+ list: vi.fn().mockResolvedValue(new Map())
37
69
  } as any,
38
- blockConcurrencyWhile: vi.fn((fn: () => Promise<void>) => fn()),
70
+ blockConcurrencyWhile: vi
71
+ .fn()
72
+ .mockImplementation(
73
+ <T>(callback: () => Promise<T>): Promise<T> => callback()
74
+ ),
39
75
  id: {
40
76
  toString: () => 'test-sandbox-id',
41
77
  equals: vi.fn(),
42
- name: 'test-sandbox',
43
- } as any,
78
+ name: 'test-sandbox'
79
+ } as any
44
80
  };
45
81
 
46
82
  mockEnv = {};
47
83
 
48
84
  // Create Sandbox instance - SandboxClient is created internally
49
- sandbox = new Sandbox(mockCtx as DurableObjectState, mockEnv);
85
+ const stub = new Sandbox(mockCtx, mockEnv);
50
86
 
51
87
  // Wait for blockConcurrencyWhile to complete
52
88
  await vi.waitFor(() => {
53
89
  expect(mockCtx.blockConcurrencyWhile).toHaveBeenCalled();
54
90
  });
55
91
 
92
+ sandbox = Object.assign(stub, {
93
+ wsConnect: connect(stub)
94
+ });
95
+
56
96
  // Now spy on the client methods that we need for testing
57
97
  vi.spyOn(sandbox.client.utils, 'createSession').mockResolvedValue({
58
98
  success: true,
59
99
  id: 'sandbox-default',
60
- message: 'Created',
100
+ message: 'Created'
61
101
  } as any);
62
102
 
63
103
  vi.spyOn(sandbox.client.commands, 'execute').mockResolvedValue({
@@ -66,13 +106,13 @@ describe('Sandbox - Automatic Session Management', () => {
66
106
  stderr: '',
67
107
  exitCode: 0,
68
108
  command: '',
69
- timestamp: new Date().toISOString(),
109
+ timestamp: new Date().toISOString()
70
110
  } as any);
71
111
 
72
112
  vi.spyOn(sandbox.client.files, 'writeFile').mockResolvedValue({
73
113
  success: true,
74
114
  path: '/test.txt',
75
- timestamp: new Date().toISOString(),
115
+ timestamp: new Date().toISOString()
76
116
  } as any);
77
117
  });
78
118
 
@@ -88,7 +128,7 @@ describe('Sandbox - Automatic Session Management', () => {
88
128
  stderr: '',
89
129
  exitCode: 0,
90
130
  command: 'echo test',
91
- timestamp: new Date().toISOString(),
131
+ timestamp: new Date().toISOString()
92
132
  } as any);
93
133
 
94
134
  await sandbox.exec('echo test');
@@ -97,7 +137,7 @@ describe('Sandbox - Automatic Session Management', () => {
97
137
  expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({
98
138
  id: expect.stringMatching(/^sandbox-/),
99
139
  env: {},
100
- cwd: '/workspace',
140
+ cwd: '/workspace'
101
141
  });
102
142
 
103
143
  expect(sandbox.client.commands.execute).toHaveBeenCalledWith(
@@ -113,9 +153,12 @@ describe('Sandbox - Automatic Session Management', () => {
113
153
 
114
154
  expect(sandbox.client.utils.createSession).toHaveBeenCalledTimes(1);
115
155
 
116
- const firstSessionId = vi.mocked(sandbox.client.commands.execute).mock.calls[0][1];
117
- const fileSessionId = vi.mocked(sandbox.client.files.writeFile).mock.calls[0][2];
118
- const secondSessionId = vi.mocked(sandbox.client.commands.execute).mock.calls[1][1];
156
+ const firstSessionId = vi.mocked(sandbox.client.commands.execute).mock
157
+ .calls[0][1];
158
+ const fileSessionId = vi.mocked(sandbox.client.files.writeFile).mock
159
+ .calls[0][2];
160
+ const secondSessionId = vi.mocked(sandbox.client.commands.execute).mock
161
+ .calls[1][1];
119
162
 
120
163
  expect(firstSessionId).toBe(fileSessionId);
121
164
  expect(firstSessionId).toBe(secondSessionId);
@@ -127,19 +170,21 @@ describe('Sandbox - Automatic Session Management', () => {
127
170
  processId: 'proc-1',
128
171
  pid: 1234,
129
172
  command: 'sleep 10',
130
- timestamp: new Date().toISOString(),
173
+ timestamp: new Date().toISOString()
131
174
  } as any);
132
175
 
133
176
  vi.spyOn(sandbox.client.processes, 'listProcesses').mockResolvedValue({
134
177
  success: true,
135
- processes: [{
136
- id: 'proc-1',
137
- pid: 1234,
138
- command: 'sleep 10',
139
- status: 'running',
140
- startTime: new Date().toISOString(),
141
- }],
142
- timestamp: new Date().toISOString(),
178
+ processes: [
179
+ {
180
+ id: 'proc-1',
181
+ pid: 1234,
182
+ command: 'sleep 10',
183
+ status: 'running',
184
+ startTime: new Date().toISOString()
185
+ }
186
+ ],
187
+ timestamp: new Date().toISOString()
143
188
  } as any);
144
189
 
145
190
  const process = await sandbox.startProcess('sleep 10');
@@ -148,11 +193,14 @@ describe('Sandbox - Automatic Session Management', () => {
148
193
  expect(sandbox.client.utils.createSession).toHaveBeenCalledTimes(1);
149
194
 
150
195
  // startProcess uses sessionId (to start process in that session)
151
- const startSessionId = vi.mocked(sandbox.client.processes.startProcess).mock.calls[0][1];
196
+ const startSessionId = vi.mocked(sandbox.client.processes.startProcess)
197
+ .mock.calls[0][1];
152
198
  expect(startSessionId).toMatch(/^sandbox-/);
153
199
 
154
200
  // listProcesses is sandbox-scoped - no sessionId parameter
155
- const listProcessesCall = vi.mocked(sandbox.client.processes.listProcesses).mock.calls[0];
201
+ const listProcessesCall = vi.mocked(
202
+ sandbox.client.processes.listProcesses
203
+ ).mock.calls[0];
156
204
  expect(listProcessesCall).toEqual([]);
157
205
 
158
206
  // Verify the started process appears in the list
@@ -168,10 +216,12 @@ describe('Sandbox - Automatic Session Management', () => {
168
216
  stderr: '',
169
217
  branch: 'main',
170
218
  targetDir: '/workspace/repo',
171
- timestamp: new Date().toISOString(),
219
+ timestamp: new Date().toISOString()
172
220
  } as any);
173
221
 
174
- await sandbox.gitCheckout('https://github.com/test/repo.git', { branch: 'main' });
222
+ await sandbox.gitCheckout('https://github.com/test/repo.git', {
223
+ branch: 'main'
224
+ });
175
225
 
176
226
  expect(sandbox.client.utils.createSession).toHaveBeenCalledTimes(1);
177
227
  expect(sandbox.client.git.checkout).toHaveBeenCalledWith(
@@ -189,7 +239,7 @@ describe('Sandbox - Automatic Session Management', () => {
189
239
  expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({
190
240
  id: 'sandbox-my-sandbox',
191
241
  env: {},
192
- cwd: '/workspace',
242
+ cwd: '/workspace'
193
243
  });
194
244
  });
195
245
  });
@@ -199,19 +249,19 @@ describe('Sandbox - Automatic Session Management', () => {
199
249
  vi.mocked(sandbox.client.utils.createSession).mockResolvedValueOnce({
200
250
  success: true,
201
251
  id: 'custom-session-123',
202
- message: 'Created',
252
+ message: 'Created'
203
253
  } as any);
204
254
 
205
255
  const session = await sandbox.createSession({
206
256
  id: 'custom-session-123',
207
257
  env: { NODE_ENV: 'test' },
208
- cwd: '/test',
258
+ cwd: '/test'
209
259
  });
210
260
 
211
261
  expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({
212
262
  id: 'custom-session-123',
213
263
  env: { NODE_ENV: 'test' },
214
- cwd: '/test',
264
+ cwd: '/test'
215
265
  });
216
266
 
217
267
  expect(session.id).toBe('custom-session-123');
@@ -225,7 +275,7 @@ describe('Sandbox - Automatic Session Management', () => {
225
275
  vi.mocked(sandbox.client.utils.createSession).mockResolvedValueOnce({
226
276
  success: true,
227
277
  id: 'isolated-session',
228
- message: 'Created',
278
+ message: 'Created'
229
279
  } as any);
230
280
 
231
281
  const session = await sandbox.createSession({ id: 'isolated-session' });
@@ -240,8 +290,16 @@ describe('Sandbox - Automatic Session Management', () => {
240
290
 
241
291
  it('should isolate multiple explicit sessions', async () => {
242
292
  vi.mocked(sandbox.client.utils.createSession)
243
- .mockResolvedValueOnce({ success: true, id: 'session-1', message: 'Created' } as any)
244
- .mockResolvedValueOnce({ success: true, id: 'session-2', message: 'Created' } as any);
293
+ .mockResolvedValueOnce({
294
+ success: true,
295
+ id: 'session-1',
296
+ message: 'Created'
297
+ } as any)
298
+ .mockResolvedValueOnce({
299
+ success: true,
300
+ id: 'session-2',
301
+ message: 'Created'
302
+ } as any);
245
303
 
246
304
  const session1 = await sandbox.createSession({ id: 'session-1' });
247
305
  const session2 = await sandbox.createSession({ id: 'session-2' });
@@ -249,8 +307,10 @@ describe('Sandbox - Automatic Session Management', () => {
249
307
  await session1.exec('echo build');
250
308
  await session2.exec('echo test');
251
309
 
252
- const session1Id = vi.mocked(sandbox.client.commands.execute).mock.calls[0][1];
253
- const session2Id = vi.mocked(sandbox.client.commands.execute).mock.calls[1][1];
310
+ const session1Id = vi.mocked(sandbox.client.commands.execute).mock
311
+ .calls[0][1];
312
+ const session2Id = vi.mocked(sandbox.client.commands.execute).mock
313
+ .calls[1][1];
254
314
 
255
315
  expect(session1Id).toBe('session-1');
256
316
  expect(session2Id).toBe('session-2');
@@ -259,19 +319,32 @@ describe('Sandbox - Automatic Session Management', () => {
259
319
 
260
320
  it('should not interfere with default session', async () => {
261
321
  vi.mocked(sandbox.client.utils.createSession)
262
- .mockResolvedValueOnce({ success: true, id: 'sandbox-default', message: 'Created' } as any)
263
- .mockResolvedValueOnce({ success: true, id: 'explicit-session', message: 'Created' } as any);
322
+ .mockResolvedValueOnce({
323
+ success: true,
324
+ id: 'sandbox-default',
325
+ message: 'Created'
326
+ } as any)
327
+ .mockResolvedValueOnce({
328
+ success: true,
329
+ id: 'explicit-session',
330
+ message: 'Created'
331
+ } as any);
264
332
 
265
333
  await sandbox.exec('echo default');
266
334
 
267
- const explicitSession = await sandbox.createSession({ id: 'explicit-session' });
335
+ const explicitSession = await sandbox.createSession({
336
+ id: 'explicit-session'
337
+ });
268
338
  await explicitSession.exec('echo explicit');
269
339
 
270
340
  await sandbox.exec('echo default-again');
271
341
 
272
- const defaultSessionId1 = vi.mocked(sandbox.client.commands.execute).mock.calls[0][1];
273
- const explicitSessionId = vi.mocked(sandbox.client.commands.execute).mock.calls[1][1];
274
- const defaultSessionId2 = vi.mocked(sandbox.client.commands.execute).mock.calls[2][1];
342
+ const defaultSessionId1 = vi.mocked(sandbox.client.commands.execute).mock
343
+ .calls[0][1];
344
+ const explicitSessionId = vi.mocked(sandbox.client.commands.execute).mock
345
+ .calls[1][1];
346
+ const defaultSessionId2 = vi.mocked(sandbox.client.commands.execute).mock
347
+ .calls[2][1];
275
348
 
276
349
  expect(defaultSessionId1).toBe('sandbox-default');
277
350
  expect(explicitSessionId).toBe('explicit-session');
@@ -284,7 +357,7 @@ describe('Sandbox - Automatic Session Management', () => {
284
357
  vi.mocked(sandbox.client.utils.createSession).mockResolvedValueOnce({
285
358
  success: true,
286
359
  id: 'session-generated-123',
287
- message: 'Created',
360
+ message: 'Created'
288
361
  } as any);
289
362
 
290
363
  await sandbox.createSession();
@@ -292,7 +365,7 @@ describe('Sandbox - Automatic Session Management', () => {
292
365
  expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({
293
366
  id: expect.stringMatching(/^session-/),
294
367
  env: undefined,
295
- cwd: undefined,
368
+ cwd: undefined
296
369
  });
297
370
  });
298
371
  });
@@ -304,7 +377,7 @@ describe('Sandbox - Automatic Session Management', () => {
304
377
  vi.mocked(sandbox.client.utils.createSession).mockResolvedValueOnce({
305
378
  success: true,
306
379
  id: 'test-session',
307
- message: 'Created',
380
+ message: 'Created'
308
381
  } as any);
309
382
 
310
383
  session = await sandbox.createSession({ id: 'test-session' });
@@ -312,7 +385,10 @@ describe('Sandbox - Automatic Session Management', () => {
312
385
 
313
386
  it('should execute command with session context', async () => {
314
387
  await session.exec('pwd');
315
- expect(sandbox.client.commands.execute).toHaveBeenCalledWith('pwd', 'test-session');
388
+ expect(sandbox.client.commands.execute).toHaveBeenCalledWith(
389
+ 'pwd',
390
+ 'test-session'
391
+ );
316
392
  });
317
393
 
318
394
  it('should start process with session context', async () => {
@@ -323,8 +399,8 @@ describe('Sandbox - Automatic Session Management', () => {
323
399
  pid: 1234,
324
400
  command: 'sleep 10',
325
401
  status: 'running',
326
- startTime: new Date().toISOString(),
327
- },
402
+ startTime: new Date().toISOString()
403
+ }
328
404
  } as any);
329
405
 
330
406
  await session.startProcess('sleep 10');
@@ -340,7 +416,7 @@ describe('Sandbox - Automatic Session Management', () => {
340
416
  vi.spyOn(sandbox.client.files, 'writeFile').mockResolvedValue({
341
417
  success: true,
342
418
  path: '/test.txt',
343
- timestamp: new Date().toISOString(),
419
+ timestamp: new Date().toISOString()
344
420
  } as any);
345
421
 
346
422
  await session.writeFile('/test.txt', 'content');
@@ -360,7 +436,7 @@ describe('Sandbox - Automatic Session Management', () => {
360
436
  stderr: '',
361
437
  branch: 'main',
362
438
  targetDir: '/workspace/repo',
363
- timestamp: new Date().toISOString(),
439
+ timestamp: new Date().toISOString()
364
440
  } as any);
365
441
 
366
442
  await session.gitCheckout('https://github.com/test/repo.git');
@@ -379,7 +455,9 @@ describe('Sandbox - Automatic Session Management', () => {
379
455
  new Error('Session creation failed')
380
456
  );
381
457
 
382
- await expect(sandbox.exec('echo test')).rejects.toThrow('Session creation failed');
458
+ await expect(sandbox.exec('echo test')).rejects.toThrow(
459
+ 'Session creation failed'
460
+ );
383
461
  });
384
462
 
385
463
  it('should initialize with empty environment when not set', async () => {
@@ -388,7 +466,7 @@ describe('Sandbox - Automatic Session Management', () => {
388
466
  expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({
389
467
  id: expect.any(String),
390
468
  env: {},
391
- cwd: '/workspace',
469
+ cwd: '/workspace'
392
470
  });
393
471
  });
394
472
 
@@ -400,7 +478,7 @@ describe('Sandbox - Automatic Session Management', () => {
400
478
  expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({
401
479
  id: expect.any(String),
402
480
  env: { NODE_ENV: 'production', DEBUG: 'true' },
403
- cwd: '/workspace',
481
+ cwd: '/workspace'
404
482
  });
405
483
  });
406
484
  });
@@ -412,7 +490,7 @@ describe('Sandbox - Automatic Session Management', () => {
412
490
  success: true,
413
491
  port: 8080,
414
492
  name: 'test-service',
415
- exposedAt: new Date().toISOString(),
493
+ exposedAt: new Date().toISOString()
416
494
  } as any);
417
495
  });
418
496
 
@@ -446,7 +524,10 @@ describe('Sandbox - Automatic Session Management', () => {
446
524
  ];
447
525
 
448
526
  for (const { hostname } of testCases) {
449
- const result = await sandbox.exposePort(8080, { name: 'test', hostname });
527
+ const result = await sandbox.exposePort(8080, {
528
+ name: 'test',
529
+ hostname
530
+ });
450
531
  expect(result.url).toContain(hostname);
451
532
  expect(result.port).toBe(8080);
452
533
  }
@@ -462,4 +543,197 @@ describe('Sandbox - Automatic Session Management', () => {
462
543
  expect(sandbox.client.ports.exposePort).toHaveBeenCalled();
463
544
  });
464
545
  });
546
+
547
+ describe('fetch() override - WebSocket detection', () => {
548
+ let superFetchSpy: any;
549
+
550
+ beforeEach(async () => {
551
+ await sandbox.setSandboxName('test-sandbox');
552
+
553
+ // Spy on Container.prototype.fetch to verify WebSocket routing
554
+ superFetchSpy = vi
555
+ .spyOn(Container.prototype, 'fetch')
556
+ .mockResolvedValue(new Response('WebSocket response'));
557
+ });
558
+
559
+ afterEach(() => {
560
+ superFetchSpy?.mockRestore();
561
+ });
562
+
563
+ it('should detect WebSocket upgrade header and route to super.fetch', async () => {
564
+ const request = new Request('https://example.com/ws', {
565
+ headers: {
566
+ Upgrade: 'websocket',
567
+ Connection: 'Upgrade'
568
+ }
569
+ });
570
+
571
+ const response = await sandbox.fetch(request);
572
+
573
+ // Should route through super.fetch() for WebSocket
574
+ expect(superFetchSpy).toHaveBeenCalledTimes(1);
575
+ expect(await response.text()).toBe('WebSocket response');
576
+ });
577
+
578
+ it('should route non-WebSocket requests through containerFetch', async () => {
579
+ // GET request
580
+ const getRequest = new Request('https://example.com/api/data');
581
+ await sandbox.fetch(getRequest);
582
+ expect(superFetchSpy).not.toHaveBeenCalled();
583
+
584
+ vi.clearAllMocks();
585
+
586
+ // POST request
587
+ const postRequest = new Request('https://example.com/api/data', {
588
+ method: 'POST',
589
+ body: JSON.stringify({ data: 'test' }),
590
+ headers: { 'Content-Type': 'application/json' }
591
+ });
592
+ await sandbox.fetch(postRequest);
593
+ expect(superFetchSpy).not.toHaveBeenCalled();
594
+
595
+ vi.clearAllMocks();
596
+
597
+ // SSE request (should not be detected as WebSocket)
598
+ const sseRequest = new Request('https://example.com/events', {
599
+ headers: { Accept: 'text/event-stream' }
600
+ });
601
+ await sandbox.fetch(sseRequest);
602
+ expect(superFetchSpy).not.toHaveBeenCalled();
603
+ });
604
+
605
+ it('should preserve WebSocket request unchanged when calling super.fetch()', async () => {
606
+ const request = new Request('https://example.com/ws', {
607
+ headers: {
608
+ Upgrade: 'websocket',
609
+ Connection: 'Upgrade',
610
+ 'Sec-WebSocket-Key': 'test-key-123',
611
+ 'Sec-WebSocket-Version': '13'
612
+ }
613
+ });
614
+
615
+ await sandbox.fetch(request);
616
+
617
+ expect(superFetchSpy).toHaveBeenCalledTimes(1);
618
+ const passedRequest = superFetchSpy.mock.calls[0][0] as Request;
619
+ expect(passedRequest.headers.get('Upgrade')).toBe('websocket');
620
+ expect(passedRequest.headers.get('Connection')).toBe('Upgrade');
621
+ expect(passedRequest.headers.get('Sec-WebSocket-Key')).toBe(
622
+ 'test-key-123'
623
+ );
624
+ expect(passedRequest.headers.get('Sec-WebSocket-Version')).toBe('13');
625
+ });
626
+ });
627
+
628
+ describe('wsConnect() method', () => {
629
+ it('should route WebSocket request through switchPort to sandbox.fetch', async () => {
630
+ const { switchPort } = await import('@cloudflare/containers');
631
+ const switchPortMock = vi.mocked(switchPort);
632
+
633
+ const request = new Request('http://localhost/ws/echo', {
634
+ headers: {
635
+ Upgrade: 'websocket',
636
+ Connection: 'Upgrade'
637
+ }
638
+ });
639
+
640
+ const fetchSpy = vi.spyOn(sandbox, 'fetch');
641
+ const response = await sandbox.wsConnect(request, 8080);
642
+
643
+ // Verify switchPort was called with correct port
644
+ expect(switchPortMock).toHaveBeenCalledWith(request, 8080);
645
+
646
+ // Verify fetch was called with the switched request
647
+ expect(fetchSpy).toHaveBeenCalledOnce();
648
+
649
+ // Verify response indicates WebSocket upgrade
650
+ expect(response.status).toBe(200);
651
+ expect(response.headers.get('X-WebSocket-Upgraded')).toBe('true');
652
+ });
653
+
654
+ it('should reject invalid ports with SecurityError', async () => {
655
+ const request = new Request('http://localhost/ws/test', {
656
+ headers: { Upgrade: 'websocket', Connection: 'Upgrade' }
657
+ });
658
+
659
+ // Invalid port values
660
+ await expect(sandbox.wsConnect(request, -1)).rejects.toThrow(
661
+ 'Invalid or restricted port'
662
+ );
663
+ await expect(sandbox.wsConnect(request, 0)).rejects.toThrow(
664
+ 'Invalid or restricted port'
665
+ );
666
+ await expect(sandbox.wsConnect(request, 70000)).rejects.toThrow(
667
+ 'Invalid or restricted port'
668
+ );
669
+
670
+ // Privileged ports
671
+ await expect(sandbox.wsConnect(request, 80)).rejects.toThrow(
672
+ 'Invalid or restricted port'
673
+ );
674
+ await expect(sandbox.wsConnect(request, 443)).rejects.toThrow(
675
+ 'Invalid or restricted port'
676
+ );
677
+ });
678
+
679
+ it('should preserve request properties through routing', async () => {
680
+ const request = new Request(
681
+ 'http://localhost/ws/test?token=abc&room=lobby',
682
+ {
683
+ headers: {
684
+ Upgrade: 'websocket',
685
+ Connection: 'Upgrade',
686
+ 'X-Custom-Header': 'custom-value'
687
+ }
688
+ }
689
+ );
690
+
691
+ const fetchSpy = vi.spyOn(sandbox, 'fetch');
692
+ await sandbox.wsConnect(request, 8080);
693
+
694
+ const calledRequest = fetchSpy.mock.calls[0][0];
695
+
696
+ // Verify headers are preserved
697
+ expect(calledRequest.headers.get('Upgrade')).toBe('websocket');
698
+ expect(calledRequest.headers.get('X-Custom-Header')).toBe('custom-value');
699
+
700
+ // Verify query parameters are preserved
701
+ const url = new URL(calledRequest.url);
702
+ expect(url.searchParams.get('token')).toBe('abc');
703
+ expect(url.searchParams.get('room')).toBe('lobby');
704
+ });
705
+ });
706
+
707
+ describe('deleteSession', () => {
708
+ it('should prevent deletion of default session', async () => {
709
+ // Trigger creation of default session
710
+ await sandbox.exec('echo "test"');
711
+
712
+ // Verify default session exists
713
+ expect((sandbox as any).defaultSession).toBeTruthy();
714
+ const defaultSessionId = (sandbox as any).defaultSession;
715
+
716
+ // Attempt to delete default session should throw
717
+ await expect(sandbox.deleteSession(defaultSessionId)).rejects.toThrow(
718
+ `Cannot delete default session '${defaultSessionId}'. Use sandbox.destroy() to terminate the sandbox.`
719
+ );
720
+ });
721
+
722
+ it('should allow deletion of non-default sessions', async () => {
723
+ // Mock the deleteSession API response
724
+ vi.spyOn(sandbox.client.utils, 'deleteSession').mockResolvedValue({
725
+ success: true,
726
+ sessionId: 'custom-session',
727
+ timestamp: new Date().toISOString()
728
+ });
729
+
730
+ // Create a custom session
731
+ await sandbox.createSession({ id: 'custom-session' });
732
+
733
+ // Should successfully delete non-default session
734
+ const result = await sandbox.deleteSession('custom-session');
735
+ expect(result.success).toBe(true);
736
+ expect(result.sessionId).toBe('custom-session');
737
+ });
738
+ });
465
739
  });