@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.
- package/CHANGELOG.md +176 -15
- package/Dockerfile +88 -71
- package/LICENSE +176 -0
- package/README.md +10 -5
- package/dist/index.d.ts +1953 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3278 -53
- package/dist/index.js.map +1 -1
- package/package.json +11 -9
- package/src/clients/base-client.ts +39 -24
- package/src/clients/command-client.ts +8 -8
- package/src/clients/file-client.ts +51 -20
- package/src/clients/git-client.ts +9 -3
- package/src/clients/index.ts +16 -15
- package/src/clients/interpreter-client.ts +51 -47
- package/src/clients/port-client.ts +10 -10
- package/src/clients/process-client.ts +11 -8
- package/src/clients/sandbox-client.ts +2 -4
- package/src/clients/types.ts +6 -2
- package/src/clients/utility-client.ts +67 -5
- package/src/errors/adapter.ts +90 -32
- package/src/errors/classes.ts +189 -64
- package/src/errors/index.ts +9 -5
- package/src/file-stream.ts +11 -6
- package/src/index.ts +28 -17
- package/src/interpreter.ts +50 -41
- package/src/request-handler.ts +34 -21
- package/src/sandbox.ts +516 -145
- package/src/security.ts +21 -6
- package/src/sse-parser.ts +4 -3
- package/src/version.ts +6 -0
- package/startup.sh +1 -1
- package/tests/base-client.test.ts +116 -80
- package/tests/command-client.test.ts +149 -112
- package/tests/file-client.test.ts +373 -185
- package/tests/file-stream.test.ts +24 -20
- package/tests/get-sandbox.test.ts +149 -0
- package/tests/git-client.test.ts +260 -101
- package/tests/port-client.test.ts +100 -108
- package/tests/process-client.test.ts +204 -179
- package/tests/request-handler.test.ts +292 -0
- package/tests/sandbox.test.ts +336 -62
- package/tests/sse-parser.test.ts +17 -16
- package/tests/utility-client.test.ts +129 -56
- package/tests/version.test.ts +16 -0
- package/tsdown.config.ts +12 -0
- package/vitest.config.ts +6 -6
- package/dist/chunk-BCJ7SF3Q.js +0 -117
- package/dist/chunk-BCJ7SF3Q.js.map +0 -1
- package/dist/chunk-BFVUNTP4.js +0 -104
- package/dist/chunk-BFVUNTP4.js.map +0 -1
- package/dist/chunk-EKSWCBCA.js +0 -86
- package/dist/chunk-EKSWCBCA.js.map +0 -1
- package/dist/chunk-U2M5GSMU.js +0 -2220
- package/dist/chunk-U2M5GSMU.js.map +0 -1
- package/dist/chunk-Z532A7QC.js +0 -78
- package/dist/chunk-Z532A7QC.js.map +0 -1
- package/dist/file-stream.d.ts +0 -43
- package/dist/file-stream.js +0 -9
- package/dist/file-stream.js.map +0 -1
- package/dist/interpreter.d.ts +0 -33
- package/dist/interpreter.js +0 -8
- package/dist/interpreter.js.map +0 -1
- package/dist/request-handler.d.ts +0 -18
- package/dist/request-handler.js +0 -12
- package/dist/request-handler.js.map +0 -1
- package/dist/sandbox-Cyuj5F-M.d.ts +0 -579
- package/dist/sandbox.d.ts +0 -4
- package/dist/sandbox.js +0 -12
- package/dist/sandbox.js.map +0 -1
- package/dist/security.d.ts +0 -31
- package/dist/security.js +0 -13
- package/dist/security.js.map +0 -1
- package/dist/sse-parser.d.ts +0 -28
- package/dist/sse-parser.js +0 -11
- package/dist/sse-parser.js.map +0 -1
package/tests/sandbox.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
const
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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)
|
|
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(
|
|
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', {
|
|
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({
|
|
244
|
-
|
|
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
|
|
253
|
-
|
|
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({
|
|
263
|
-
|
|
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({
|
|
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
|
|
273
|
-
|
|
274
|
-
const
|
|
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(
|
|
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(
|
|
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, {
|
|
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
|
});
|