@cloudflare/sandbox 0.4.11 → 0.4.14
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 +13 -47
- package/CHANGELOG.md +44 -16
- package/Dockerfile +15 -9
- package/README.md +0 -1
- package/dist/index.d.ts +1889 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3144 -65
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/clients/base-client.ts +39 -24
- package/src/clients/command-client.ts +8 -8
- package/src/clients/file-client.ts +31 -26
- package/src/clients/git-client.ts +3 -4
- package/src/clients/index.ts +12 -16
- 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 +10 -6
- 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 +22 -15
- package/src/interpreter.ts +50 -41
- package/src/request-handler.ts +24 -21
- package/src/sandbox.ts +370 -148
- package/src/security.ts +21 -6
- package/src/sse-parser.ts +4 -3
- package/src/version.ts +1 -1
- package/tests/base-client.test.ts +116 -80
- package/tests/command-client.test.ts +149 -112
- package/tests/file-client.test.ts +309 -197
- package/tests/file-stream.test.ts +24 -20
- package/tests/get-sandbox.test.ts +45 -6
- package/tests/git-client.test.ts +188 -101
- package/tests/port-client.test.ts +100 -108
- package/tests/process-client.test.ts +204 -179
- package/tests/request-handler.test.ts +117 -65
- package/tests/sandbox.test.ts +220 -68
- package/tests/sse-parser.test.ts +17 -16
- package/tests/utility-client.test.ts +79 -72
- package/tsdown.config.ts +12 -0
- package/vitest.config.ts +6 -6
- 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-FE4PJSRB.js +0 -7
- package/dist/chunk-FE4PJSRB.js.map +0 -1
- package/dist/chunk-JXZMAU2C.js +0 -559
- package/dist/chunk-JXZMAU2C.js.map +0 -1
- package/dist/chunk-SVWLTRHD.js +0 -2456
- package/dist/chunk-SVWLTRHD.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 -13
- package/dist/request-handler.js.map +0 -1
- package/dist/sandbox-DWQVgVTY.d.ts +0 -603
- package/dist/sandbox.d.ts +0 -4
- package/dist/sandbox.js +0 -13
- 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/dist/version.d.ts +0 -8
- package/dist/version.js +0 -7
- package/dist/version.js.map +0 -1
package/tests/sandbox.test.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
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 { Container } from '@cloudflare/containers';
|
|
4
|
+
import { connect, Sandbox } from '../src/sandbox';
|
|
5
5
|
|
|
6
6
|
// Mock dependencies before imports
|
|
7
7
|
vi.mock('./interpreter', () => ({
|
|
8
|
-
CodeInterpreter: vi.fn().mockImplementation(() => ({}))
|
|
8
|
+
CodeInterpreter: vi.fn().mockImplementation(() => ({}))
|
|
9
9
|
}));
|
|
10
10
|
|
|
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
|
+
|
|
12
19
|
const MockContainer = class Container {
|
|
13
20
|
ctx: any;
|
|
14
21
|
env: any;
|
|
@@ -18,6 +25,17 @@ vi.mock('@cloudflare/containers', () => {
|
|
|
18
25
|
}
|
|
19
26
|
async fetch(request: Request): Promise<Response> {
|
|
20
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
|
+
}
|
|
21
39
|
return new Response('Mock Container fetch');
|
|
22
40
|
}
|
|
23
41
|
async containerFetch(request: Request, port: number): Promise<Response> {
|
|
@@ -29,12 +47,13 @@ vi.mock('@cloudflare/containers', () => {
|
|
|
29
47
|
return {
|
|
30
48
|
Container: MockContainer,
|
|
31
49
|
getContainer: vi.fn(),
|
|
50
|
+
switchPort: mockSwitchPort
|
|
32
51
|
};
|
|
33
52
|
});
|
|
34
53
|
|
|
35
54
|
describe('Sandbox - Automatic Session Management', () => {
|
|
36
55
|
let sandbox: Sandbox;
|
|
37
|
-
let mockCtx: Partial<DurableObjectState
|
|
56
|
+
let mockCtx: Partial<DurableObjectState<{}>>;
|
|
38
57
|
let mockEnv: any;
|
|
39
58
|
|
|
40
59
|
beforeEach(async () => {
|
|
@@ -46,31 +65,39 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
46
65
|
get: vi.fn().mockResolvedValue(null),
|
|
47
66
|
put: vi.fn().mockResolvedValue(undefined),
|
|
48
67
|
delete: vi.fn().mockResolvedValue(undefined),
|
|
49
|
-
list: vi.fn().mockResolvedValue(new Map())
|
|
68
|
+
list: vi.fn().mockResolvedValue(new Map())
|
|
50
69
|
} as any,
|
|
51
|
-
blockConcurrencyWhile: vi
|
|
70
|
+
blockConcurrencyWhile: vi
|
|
71
|
+
.fn()
|
|
72
|
+
.mockImplementation(
|
|
73
|
+
<T>(callback: () => Promise<T>): Promise<T> => callback()
|
|
74
|
+
),
|
|
52
75
|
id: {
|
|
53
76
|
toString: () => 'test-sandbox-id',
|
|
54
77
|
equals: vi.fn(),
|
|
55
|
-
name: 'test-sandbox'
|
|
56
|
-
} as any
|
|
78
|
+
name: 'test-sandbox'
|
|
79
|
+
} as any
|
|
57
80
|
};
|
|
58
81
|
|
|
59
82
|
mockEnv = {};
|
|
60
83
|
|
|
61
84
|
// Create Sandbox instance - SandboxClient is created internally
|
|
62
|
-
|
|
85
|
+
const stub = new Sandbox(mockCtx, mockEnv);
|
|
63
86
|
|
|
64
87
|
// Wait for blockConcurrencyWhile to complete
|
|
65
88
|
await vi.waitFor(() => {
|
|
66
89
|
expect(mockCtx.blockConcurrencyWhile).toHaveBeenCalled();
|
|
67
90
|
});
|
|
68
91
|
|
|
92
|
+
sandbox = Object.assign(stub, {
|
|
93
|
+
wsConnect: connect(stub)
|
|
94
|
+
});
|
|
95
|
+
|
|
69
96
|
// Now spy on the client methods that we need for testing
|
|
70
97
|
vi.spyOn(sandbox.client.utils, 'createSession').mockResolvedValue({
|
|
71
98
|
success: true,
|
|
72
99
|
id: 'sandbox-default',
|
|
73
|
-
message: 'Created'
|
|
100
|
+
message: 'Created'
|
|
74
101
|
} as any);
|
|
75
102
|
|
|
76
103
|
vi.spyOn(sandbox.client.commands, 'execute').mockResolvedValue({
|
|
@@ -79,13 +106,13 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
79
106
|
stderr: '',
|
|
80
107
|
exitCode: 0,
|
|
81
108
|
command: '',
|
|
82
|
-
timestamp: new Date().toISOString()
|
|
109
|
+
timestamp: new Date().toISOString()
|
|
83
110
|
} as any);
|
|
84
111
|
|
|
85
112
|
vi.spyOn(sandbox.client.files, 'writeFile').mockResolvedValue({
|
|
86
113
|
success: true,
|
|
87
114
|
path: '/test.txt',
|
|
88
|
-
timestamp: new Date().toISOString()
|
|
115
|
+
timestamp: new Date().toISOString()
|
|
89
116
|
} as any);
|
|
90
117
|
});
|
|
91
118
|
|
|
@@ -101,7 +128,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
101
128
|
stderr: '',
|
|
102
129
|
exitCode: 0,
|
|
103
130
|
command: 'echo test',
|
|
104
|
-
timestamp: new Date().toISOString()
|
|
131
|
+
timestamp: new Date().toISOString()
|
|
105
132
|
} as any);
|
|
106
133
|
|
|
107
134
|
await sandbox.exec('echo test');
|
|
@@ -110,7 +137,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
110
137
|
expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({
|
|
111
138
|
id: expect.stringMatching(/^sandbox-/),
|
|
112
139
|
env: {},
|
|
113
|
-
cwd: '/workspace'
|
|
140
|
+
cwd: '/workspace'
|
|
114
141
|
});
|
|
115
142
|
|
|
116
143
|
expect(sandbox.client.commands.execute).toHaveBeenCalledWith(
|
|
@@ -126,9 +153,12 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
126
153
|
|
|
127
154
|
expect(sandbox.client.utils.createSession).toHaveBeenCalledTimes(1);
|
|
128
155
|
|
|
129
|
-
const firstSessionId = vi.mocked(sandbox.client.commands.execute).mock
|
|
130
|
-
|
|
131
|
-
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];
|
|
132
162
|
|
|
133
163
|
expect(firstSessionId).toBe(fileSessionId);
|
|
134
164
|
expect(firstSessionId).toBe(secondSessionId);
|
|
@@ -140,19 +170,21 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
140
170
|
processId: 'proc-1',
|
|
141
171
|
pid: 1234,
|
|
142
172
|
command: 'sleep 10',
|
|
143
|
-
timestamp: new Date().toISOString()
|
|
173
|
+
timestamp: new Date().toISOString()
|
|
144
174
|
} as any);
|
|
145
175
|
|
|
146
176
|
vi.spyOn(sandbox.client.processes, 'listProcesses').mockResolvedValue({
|
|
147
177
|
success: true,
|
|
148
|
-
processes: [
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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()
|
|
156
188
|
} as any);
|
|
157
189
|
|
|
158
190
|
const process = await sandbox.startProcess('sleep 10');
|
|
@@ -161,11 +193,14 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
161
193
|
expect(sandbox.client.utils.createSession).toHaveBeenCalledTimes(1);
|
|
162
194
|
|
|
163
195
|
// startProcess uses sessionId (to start process in that session)
|
|
164
|
-
const startSessionId = vi.mocked(sandbox.client.processes.startProcess)
|
|
196
|
+
const startSessionId = vi.mocked(sandbox.client.processes.startProcess)
|
|
197
|
+
.mock.calls[0][1];
|
|
165
198
|
expect(startSessionId).toMatch(/^sandbox-/);
|
|
166
199
|
|
|
167
200
|
// listProcesses is sandbox-scoped - no sessionId parameter
|
|
168
|
-
const listProcessesCall = vi.mocked(
|
|
201
|
+
const listProcessesCall = vi.mocked(
|
|
202
|
+
sandbox.client.processes.listProcesses
|
|
203
|
+
).mock.calls[0];
|
|
169
204
|
expect(listProcessesCall).toEqual([]);
|
|
170
205
|
|
|
171
206
|
// Verify the started process appears in the list
|
|
@@ -181,10 +216,12 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
181
216
|
stderr: '',
|
|
182
217
|
branch: 'main',
|
|
183
218
|
targetDir: '/workspace/repo',
|
|
184
|
-
timestamp: new Date().toISOString()
|
|
219
|
+
timestamp: new Date().toISOString()
|
|
185
220
|
} as any);
|
|
186
221
|
|
|
187
|
-
await sandbox.gitCheckout('https://github.com/test/repo.git', {
|
|
222
|
+
await sandbox.gitCheckout('https://github.com/test/repo.git', {
|
|
223
|
+
branch: 'main'
|
|
224
|
+
});
|
|
188
225
|
|
|
189
226
|
expect(sandbox.client.utils.createSession).toHaveBeenCalledTimes(1);
|
|
190
227
|
expect(sandbox.client.git.checkout).toHaveBeenCalledWith(
|
|
@@ -202,7 +239,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
202
239
|
expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({
|
|
203
240
|
id: 'sandbox-my-sandbox',
|
|
204
241
|
env: {},
|
|
205
|
-
cwd: '/workspace'
|
|
242
|
+
cwd: '/workspace'
|
|
206
243
|
});
|
|
207
244
|
});
|
|
208
245
|
});
|
|
@@ -212,19 +249,19 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
212
249
|
vi.mocked(sandbox.client.utils.createSession).mockResolvedValueOnce({
|
|
213
250
|
success: true,
|
|
214
251
|
id: 'custom-session-123',
|
|
215
|
-
message: 'Created'
|
|
252
|
+
message: 'Created'
|
|
216
253
|
} as any);
|
|
217
254
|
|
|
218
255
|
const session = await sandbox.createSession({
|
|
219
256
|
id: 'custom-session-123',
|
|
220
257
|
env: { NODE_ENV: 'test' },
|
|
221
|
-
cwd: '/test'
|
|
258
|
+
cwd: '/test'
|
|
222
259
|
});
|
|
223
260
|
|
|
224
261
|
expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({
|
|
225
262
|
id: 'custom-session-123',
|
|
226
263
|
env: { NODE_ENV: 'test' },
|
|
227
|
-
cwd: '/test'
|
|
264
|
+
cwd: '/test'
|
|
228
265
|
});
|
|
229
266
|
|
|
230
267
|
expect(session.id).toBe('custom-session-123');
|
|
@@ -238,7 +275,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
238
275
|
vi.mocked(sandbox.client.utils.createSession).mockResolvedValueOnce({
|
|
239
276
|
success: true,
|
|
240
277
|
id: 'isolated-session',
|
|
241
|
-
message: 'Created'
|
|
278
|
+
message: 'Created'
|
|
242
279
|
} as any);
|
|
243
280
|
|
|
244
281
|
const session = await sandbox.createSession({ id: 'isolated-session' });
|
|
@@ -253,8 +290,16 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
253
290
|
|
|
254
291
|
it('should isolate multiple explicit sessions', async () => {
|
|
255
292
|
vi.mocked(sandbox.client.utils.createSession)
|
|
256
|
-
.mockResolvedValueOnce({
|
|
257
|
-
|
|
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);
|
|
258
303
|
|
|
259
304
|
const session1 = await sandbox.createSession({ id: 'session-1' });
|
|
260
305
|
const session2 = await sandbox.createSession({ id: 'session-2' });
|
|
@@ -262,8 +307,10 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
262
307
|
await session1.exec('echo build');
|
|
263
308
|
await session2.exec('echo test');
|
|
264
309
|
|
|
265
|
-
const session1Id = vi.mocked(sandbox.client.commands.execute).mock
|
|
266
|
-
|
|
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];
|
|
267
314
|
|
|
268
315
|
expect(session1Id).toBe('session-1');
|
|
269
316
|
expect(session2Id).toBe('session-2');
|
|
@@ -272,19 +319,32 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
272
319
|
|
|
273
320
|
it('should not interfere with default session', async () => {
|
|
274
321
|
vi.mocked(sandbox.client.utils.createSession)
|
|
275
|
-
.mockResolvedValueOnce({
|
|
276
|
-
|
|
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);
|
|
277
332
|
|
|
278
333
|
await sandbox.exec('echo default');
|
|
279
334
|
|
|
280
|
-
const explicitSession = await sandbox.createSession({
|
|
335
|
+
const explicitSession = await sandbox.createSession({
|
|
336
|
+
id: 'explicit-session'
|
|
337
|
+
});
|
|
281
338
|
await explicitSession.exec('echo explicit');
|
|
282
339
|
|
|
283
340
|
await sandbox.exec('echo default-again');
|
|
284
341
|
|
|
285
|
-
const defaultSessionId1 = vi.mocked(sandbox.client.commands.execute).mock
|
|
286
|
-
|
|
287
|
-
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];
|
|
288
348
|
|
|
289
349
|
expect(defaultSessionId1).toBe('sandbox-default');
|
|
290
350
|
expect(explicitSessionId).toBe('explicit-session');
|
|
@@ -297,7 +357,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
297
357
|
vi.mocked(sandbox.client.utils.createSession).mockResolvedValueOnce({
|
|
298
358
|
success: true,
|
|
299
359
|
id: 'session-generated-123',
|
|
300
|
-
message: 'Created'
|
|
360
|
+
message: 'Created'
|
|
301
361
|
} as any);
|
|
302
362
|
|
|
303
363
|
await sandbox.createSession();
|
|
@@ -305,7 +365,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
305
365
|
expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({
|
|
306
366
|
id: expect.stringMatching(/^session-/),
|
|
307
367
|
env: undefined,
|
|
308
|
-
cwd: undefined
|
|
368
|
+
cwd: undefined
|
|
309
369
|
});
|
|
310
370
|
});
|
|
311
371
|
});
|
|
@@ -317,7 +377,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
317
377
|
vi.mocked(sandbox.client.utils.createSession).mockResolvedValueOnce({
|
|
318
378
|
success: true,
|
|
319
379
|
id: 'test-session',
|
|
320
|
-
message: 'Created'
|
|
380
|
+
message: 'Created'
|
|
321
381
|
} as any);
|
|
322
382
|
|
|
323
383
|
session = await sandbox.createSession({ id: 'test-session' });
|
|
@@ -325,7 +385,10 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
325
385
|
|
|
326
386
|
it('should execute command with session context', async () => {
|
|
327
387
|
await session.exec('pwd');
|
|
328
|
-
expect(sandbox.client.commands.execute).toHaveBeenCalledWith(
|
|
388
|
+
expect(sandbox.client.commands.execute).toHaveBeenCalledWith(
|
|
389
|
+
'pwd',
|
|
390
|
+
'test-session'
|
|
391
|
+
);
|
|
329
392
|
});
|
|
330
393
|
|
|
331
394
|
it('should start process with session context', async () => {
|
|
@@ -336,8 +399,8 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
336
399
|
pid: 1234,
|
|
337
400
|
command: 'sleep 10',
|
|
338
401
|
status: 'running',
|
|
339
|
-
startTime: new Date().toISOString()
|
|
340
|
-
}
|
|
402
|
+
startTime: new Date().toISOString()
|
|
403
|
+
}
|
|
341
404
|
} as any);
|
|
342
405
|
|
|
343
406
|
await session.startProcess('sleep 10');
|
|
@@ -353,7 +416,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
353
416
|
vi.spyOn(sandbox.client.files, 'writeFile').mockResolvedValue({
|
|
354
417
|
success: true,
|
|
355
418
|
path: '/test.txt',
|
|
356
|
-
timestamp: new Date().toISOString()
|
|
419
|
+
timestamp: new Date().toISOString()
|
|
357
420
|
} as any);
|
|
358
421
|
|
|
359
422
|
await session.writeFile('/test.txt', 'content');
|
|
@@ -373,7 +436,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
373
436
|
stderr: '',
|
|
374
437
|
branch: 'main',
|
|
375
438
|
targetDir: '/workspace/repo',
|
|
376
|
-
timestamp: new Date().toISOString()
|
|
439
|
+
timestamp: new Date().toISOString()
|
|
377
440
|
} as any);
|
|
378
441
|
|
|
379
442
|
await session.gitCheckout('https://github.com/test/repo.git');
|
|
@@ -392,7 +455,9 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
392
455
|
new Error('Session creation failed')
|
|
393
456
|
);
|
|
394
457
|
|
|
395
|
-
await expect(sandbox.exec('echo test')).rejects.toThrow(
|
|
458
|
+
await expect(sandbox.exec('echo test')).rejects.toThrow(
|
|
459
|
+
'Session creation failed'
|
|
460
|
+
);
|
|
396
461
|
});
|
|
397
462
|
|
|
398
463
|
it('should initialize with empty environment when not set', async () => {
|
|
@@ -401,7 +466,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
401
466
|
expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({
|
|
402
467
|
id: expect.any(String),
|
|
403
468
|
env: {},
|
|
404
|
-
cwd: '/workspace'
|
|
469
|
+
cwd: '/workspace'
|
|
405
470
|
});
|
|
406
471
|
});
|
|
407
472
|
|
|
@@ -413,7 +478,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
413
478
|
expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({
|
|
414
479
|
id: expect.any(String),
|
|
415
480
|
env: { NODE_ENV: 'production', DEBUG: 'true' },
|
|
416
|
-
cwd: '/workspace'
|
|
481
|
+
cwd: '/workspace'
|
|
417
482
|
});
|
|
418
483
|
});
|
|
419
484
|
});
|
|
@@ -425,7 +490,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
425
490
|
success: true,
|
|
426
491
|
port: 8080,
|
|
427
492
|
name: 'test-service',
|
|
428
|
-
exposedAt: new Date().toISOString()
|
|
493
|
+
exposedAt: new Date().toISOString()
|
|
429
494
|
} as any);
|
|
430
495
|
});
|
|
431
496
|
|
|
@@ -459,7 +524,10 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
459
524
|
];
|
|
460
525
|
|
|
461
526
|
for (const { hostname } of testCases) {
|
|
462
|
-
const result = await sandbox.exposePort(8080, {
|
|
527
|
+
const result = await sandbox.exposePort(8080, {
|
|
528
|
+
name: 'test',
|
|
529
|
+
hostname
|
|
530
|
+
});
|
|
463
531
|
expect(result.url).toContain(hostname);
|
|
464
532
|
expect(result.port).toBe(8080);
|
|
465
533
|
}
|
|
@@ -483,7 +551,8 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
483
551
|
await sandbox.setSandboxName('test-sandbox');
|
|
484
552
|
|
|
485
553
|
// Spy on Container.prototype.fetch to verify WebSocket routing
|
|
486
|
-
superFetchSpy = vi
|
|
554
|
+
superFetchSpy = vi
|
|
555
|
+
.spyOn(Container.prototype, 'fetch')
|
|
487
556
|
.mockResolvedValue(new Response('WebSocket response'));
|
|
488
557
|
});
|
|
489
558
|
|
|
@@ -494,9 +563,9 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
494
563
|
it('should detect WebSocket upgrade header and route to super.fetch', async () => {
|
|
495
564
|
const request = new Request('https://example.com/ws', {
|
|
496
565
|
headers: {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
566
|
+
Upgrade: 'websocket',
|
|
567
|
+
Connection: 'Upgrade'
|
|
568
|
+
}
|
|
500
569
|
});
|
|
501
570
|
|
|
502
571
|
const response = await sandbox.fetch(request);
|
|
@@ -518,7 +587,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
518
587
|
const postRequest = new Request('https://example.com/api/data', {
|
|
519
588
|
method: 'POST',
|
|
520
589
|
body: JSON.stringify({ data: 'test' }),
|
|
521
|
-
headers: { 'Content-Type': 'application/json' }
|
|
590
|
+
headers: { 'Content-Type': 'application/json' }
|
|
522
591
|
});
|
|
523
592
|
await sandbox.fetch(postRequest);
|
|
524
593
|
expect(superFetchSpy).not.toHaveBeenCalled();
|
|
@@ -527,7 +596,7 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
527
596
|
|
|
528
597
|
// SSE request (should not be detected as WebSocket)
|
|
529
598
|
const sseRequest = new Request('https://example.com/events', {
|
|
530
|
-
headers: {
|
|
599
|
+
headers: { Accept: 'text/event-stream' }
|
|
531
600
|
});
|
|
532
601
|
await sandbox.fetch(sseRequest);
|
|
533
602
|
expect(superFetchSpy).not.toHaveBeenCalled();
|
|
@@ -536,10 +605,11 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
536
605
|
it('should preserve WebSocket request unchanged when calling super.fetch()', async () => {
|
|
537
606
|
const request = new Request('https://example.com/ws', {
|
|
538
607
|
headers: {
|
|
539
|
-
|
|
608
|
+
Upgrade: 'websocket',
|
|
609
|
+
Connection: 'Upgrade',
|
|
540
610
|
'Sec-WebSocket-Key': 'test-key-123',
|
|
541
|
-
'Sec-WebSocket-Version': '13'
|
|
542
|
-
}
|
|
611
|
+
'Sec-WebSocket-Version': '13'
|
|
612
|
+
}
|
|
543
613
|
});
|
|
544
614
|
|
|
545
615
|
await sandbox.fetch(request);
|
|
@@ -547,8 +617,90 @@ describe('Sandbox - Automatic Session Management', () => {
|
|
|
547
617
|
expect(superFetchSpy).toHaveBeenCalledTimes(1);
|
|
548
618
|
const passedRequest = superFetchSpy.mock.calls[0][0] as Request;
|
|
549
619
|
expect(passedRequest.headers.get('Upgrade')).toBe('websocket');
|
|
550
|
-
expect(passedRequest.headers.get('
|
|
620
|
+
expect(passedRequest.headers.get('Connection')).toBe('Upgrade');
|
|
621
|
+
expect(passedRequest.headers.get('Sec-WebSocket-Key')).toBe(
|
|
622
|
+
'test-key-123'
|
|
623
|
+
);
|
|
551
624
|
expect(passedRequest.headers.get('Sec-WebSocket-Version')).toBe('13');
|
|
552
625
|
});
|
|
553
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
|
+
});
|
|
554
706
|
});
|
package/tests/sse-parser.test.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
asyncIterableToSSEStream,
|
|
4
|
+
parseSSEStream,
|
|
5
|
+
responseToAsyncIterable
|
|
6
|
+
} from '../src/sse-parser';
|
|
3
7
|
|
|
4
8
|
function createMockSSEStream(events: string[]): ReadableStream<Uint8Array> {
|
|
5
9
|
return new ReadableStream({
|
|
@@ -25,7 +29,6 @@ describe('SSE Parser', () => {
|
|
|
25
29
|
});
|
|
26
30
|
|
|
27
31
|
describe('parseSSEStream', () => {
|
|
28
|
-
|
|
29
32
|
it('should parse valid SSE events', async () => {
|
|
30
33
|
const stream = createMockSSEStream([
|
|
31
34
|
'data: {"type":"start","command":"echo test"}\n\n',
|
|
@@ -134,9 +137,7 @@ describe('SSE Parser', () => {
|
|
|
134
137
|
});
|
|
135
138
|
|
|
136
139
|
it('should handle remaining buffer data after stream ends', async () => {
|
|
137
|
-
const stream = createMockSSEStream([
|
|
138
|
-
'data: {"type":"complete"}'
|
|
139
|
-
]);
|
|
140
|
+
const stream = createMockSSEStream(['data: {"type":"complete"}']);
|
|
140
141
|
|
|
141
142
|
const events: any[] = [];
|
|
142
143
|
for await (const event of parseSSEStream(stream)) {
|
|
@@ -153,7 +154,8 @@ describe('SSE Parser', () => {
|
|
|
153
154
|
controller.abort();
|
|
154
155
|
|
|
155
156
|
await expect(async () => {
|
|
156
|
-
for await (const event of parseSSEStream(stream, controller.signal)) {
|
|
157
|
+
for await (const event of parseSSEStream(stream, controller.signal)) {
|
|
158
|
+
}
|
|
157
159
|
}).rejects.toThrow('Operation was aborted');
|
|
158
160
|
});
|
|
159
161
|
|
|
@@ -236,10 +238,10 @@ describe('SSE Parser', () => {
|
|
|
236
238
|
const stream = asyncIterableToSSEStream(mockEvents());
|
|
237
239
|
const reader = stream.getReader();
|
|
238
240
|
const decoder = new TextDecoder();
|
|
239
|
-
|
|
241
|
+
|
|
240
242
|
const chunks: string[] = [];
|
|
241
243
|
let done = false;
|
|
242
|
-
|
|
244
|
+
|
|
243
245
|
while (!done) {
|
|
244
246
|
const { value, done: readerDone } = await reader.read();
|
|
245
247
|
done = readerDone;
|
|
@@ -251,9 +253,9 @@ describe('SSE Parser', () => {
|
|
|
251
253
|
const fullOutput = chunks.join('');
|
|
252
254
|
expect(fullOutput).toBe(
|
|
253
255
|
'data: {"type":"start","command":"test"}\n\n' +
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
'data: {"type":"stdout","data":"output"}\n\n' +
|
|
257
|
+
'data: {"type":"complete","exitCode":0}\n\n' +
|
|
258
|
+
'data: [DONE]\n\n'
|
|
257
259
|
);
|
|
258
260
|
});
|
|
259
261
|
|
|
@@ -262,10 +264,9 @@ describe('SSE Parser', () => {
|
|
|
262
264
|
yield { name: 'test', value: 123 };
|
|
263
265
|
}
|
|
264
266
|
|
|
265
|
-
const stream = asyncIterableToSSEStream(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
);
|
|
267
|
+
const stream = asyncIterableToSSEStream(mockEvents(), {
|
|
268
|
+
serialize: (event) => `custom:${event.name}=${event.value}`
|
|
269
|
+
});
|
|
269
270
|
|
|
270
271
|
const reader = stream.getReader();
|
|
271
272
|
const decoder = new TextDecoder();
|
|
@@ -287,4 +288,4 @@ describe('SSE Parser', () => {
|
|
|
287
288
|
await expect(reader.read()).rejects.toThrow('Async iterable error');
|
|
288
289
|
});
|
|
289
290
|
});
|
|
290
|
-
});
|
|
291
|
+
});
|