@cloudflare/sandbox 0.5.4 → 0.6.0
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/Dockerfile +54 -59
- package/README.md +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/package.json +13 -8
- package/.turbo/turbo-build.log +0 -23
- package/CHANGELOG.md +0 -441
- package/src/clients/base-client.ts +0 -356
- package/src/clients/command-client.ts +0 -133
- package/src/clients/file-client.ts +0 -300
- package/src/clients/git-client.ts +0 -98
- package/src/clients/index.ts +0 -64
- package/src/clients/interpreter-client.ts +0 -333
- package/src/clients/port-client.ts +0 -105
- package/src/clients/process-client.ts +0 -198
- package/src/clients/sandbox-client.ts +0 -39
- package/src/clients/types.ts +0 -88
- package/src/clients/utility-client.ts +0 -156
- package/src/errors/adapter.ts +0 -238
- package/src/errors/classes.ts +0 -594
- package/src/errors/index.ts +0 -109
- package/src/file-stream.ts +0 -169
- package/src/index.ts +0 -121
- package/src/interpreter.ts +0 -168
- package/src/openai/index.ts +0 -465
- package/src/request-handler.ts +0 -184
- package/src/sandbox.ts +0 -1937
- package/src/security.ts +0 -119
- package/src/sse-parser.ts +0 -144
- package/src/storage-mount/credential-detection.ts +0 -41
- package/src/storage-mount/errors.ts +0 -51
- package/src/storage-mount/index.ts +0 -17
- package/src/storage-mount/provider-detection.ts +0 -93
- package/src/storage-mount/types.ts +0 -17
- package/src/version.ts +0 -6
- package/tests/base-client.test.ts +0 -582
- package/tests/command-client.test.ts +0 -444
- package/tests/file-client.test.ts +0 -831
- package/tests/file-stream.test.ts +0 -310
- package/tests/get-sandbox.test.ts +0 -172
- package/tests/git-client.test.ts +0 -455
- package/tests/openai-shell-editor.test.ts +0 -434
- package/tests/port-client.test.ts +0 -283
- package/tests/process-client.test.ts +0 -649
- package/tests/request-handler.test.ts +0 -292
- package/tests/sandbox.test.ts +0 -890
- package/tests/sse-parser.test.ts +0 -291
- package/tests/storage-mount/credential-detection.test.ts +0 -119
- package/tests/storage-mount/provider-detection.test.ts +0 -77
- package/tests/utility-client.test.ts +0 -339
- package/tests/version.test.ts +0 -16
- package/tests/wrangler.jsonc +0 -35
- package/tsconfig.json +0 -11
- package/tsdown.config.ts +0 -13
- package/vitest.config.ts +0 -31
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
import type { ApplyPatchOperation } from '@openai/agents';
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { Editor, Shell } from '../src/openai/index';
|
|
4
|
-
import type { Sandbox } from '../src/sandbox';
|
|
5
|
-
|
|
6
|
-
interface MockSandbox {
|
|
7
|
-
exec?: ReturnType<typeof vi.fn>;
|
|
8
|
-
mkdir?: ReturnType<typeof vi.fn>;
|
|
9
|
-
writeFile?: ReturnType<typeof vi.fn>;
|
|
10
|
-
readFile?: ReturnType<typeof vi.fn>;
|
|
11
|
-
deleteFile?: ReturnType<typeof vi.fn>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const { loggerSpies, createLoggerMock, applyDiffMock } = vi.hoisted(() => {
|
|
15
|
-
const logger = {
|
|
16
|
-
debug: vi.fn(),
|
|
17
|
-
info: vi.fn(),
|
|
18
|
-
warn: vi.fn(),
|
|
19
|
-
error: vi.fn(),
|
|
20
|
-
child: vi.fn().mockReturnThis()
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
loggerSpies: logger,
|
|
25
|
-
createLoggerMock: vi.fn(() => logger),
|
|
26
|
-
applyDiffMock: vi.fn<(...args: any[]) => string>()
|
|
27
|
-
};
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
vi.mock('@repo/shared', () => ({
|
|
31
|
-
createLogger: createLoggerMock
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
vi.mock('@openai/agents', () => ({
|
|
35
|
-
applyDiff: applyDiffMock
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
describe('Shell', () => {
|
|
39
|
-
beforeEach(() => {
|
|
40
|
-
vi.clearAllMocks();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('runs commands and collects results', async () => {
|
|
44
|
-
const execMock = vi.fn().mockResolvedValue({
|
|
45
|
-
stdout: 'hello\n',
|
|
46
|
-
stderr: '',
|
|
47
|
-
exitCode: 0
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const mockSandbox: MockSandbox = { exec: execMock };
|
|
51
|
-
const shell = new Shell(mockSandbox as unknown as Sandbox);
|
|
52
|
-
|
|
53
|
-
const result = await shell.run({
|
|
54
|
-
commands: ['echo hello'],
|
|
55
|
-
timeoutMs: 500
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
expect(execMock).toHaveBeenCalledWith('echo hello', {
|
|
59
|
-
timeout: 500,
|
|
60
|
-
cwd: '/workspace'
|
|
61
|
-
});
|
|
62
|
-
expect(result.output).toHaveLength(1);
|
|
63
|
-
expect(result.output[0]).toMatchObject({
|
|
64
|
-
command: 'echo hello',
|
|
65
|
-
stdout: 'hello\n',
|
|
66
|
-
stderr: '',
|
|
67
|
-
outcome: { type: 'exit', exitCode: 0 }
|
|
68
|
-
});
|
|
69
|
-
expect(shell.results).toHaveLength(1);
|
|
70
|
-
expect(loggerSpies.info).toHaveBeenCalledWith(
|
|
71
|
-
'Command completed successfully',
|
|
72
|
-
{ command: 'echo hello' }
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('halts subsequent commands after a timeout error', async () => {
|
|
77
|
-
const timeoutError = new Error('Command timed out');
|
|
78
|
-
const execMock = vi.fn().mockRejectedValue(timeoutError);
|
|
79
|
-
|
|
80
|
-
const mockSandbox: MockSandbox = { exec: execMock };
|
|
81
|
-
const shell = new Shell(mockSandbox as unknown as Sandbox);
|
|
82
|
-
const action = {
|
|
83
|
-
commands: ['sleep 1', 'echo never'],
|
|
84
|
-
timeoutMs: 25
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const result = await shell.run(action);
|
|
88
|
-
|
|
89
|
-
expect(execMock).toHaveBeenCalledTimes(1);
|
|
90
|
-
expect(result.output[0].outcome).toEqual({ type: 'timeout' });
|
|
91
|
-
expect(shell.results[0].exitCode).toBeNull();
|
|
92
|
-
expect(loggerSpies.warn).toHaveBeenCalledWith(
|
|
93
|
-
'Breaking command loop due to timeout'
|
|
94
|
-
);
|
|
95
|
-
expect(loggerSpies.error).toHaveBeenCalledWith(
|
|
96
|
-
'Command timed out',
|
|
97
|
-
undefined,
|
|
98
|
-
expect.objectContaining({
|
|
99
|
-
command: 'sleep 1',
|
|
100
|
-
timeout: 25
|
|
101
|
-
})
|
|
102
|
-
);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('Editor', () => {
|
|
107
|
-
beforeEach(() => {
|
|
108
|
-
vi.clearAllMocks();
|
|
109
|
-
applyDiffMock.mockReset();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('creates files using applyDiff output', async () => {
|
|
113
|
-
applyDiffMock.mockReturnValueOnce('file contents');
|
|
114
|
-
|
|
115
|
-
const mockSandbox: MockSandbox = {
|
|
116
|
-
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
117
|
-
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
121
|
-
const operation = {
|
|
122
|
-
type: 'create_file',
|
|
123
|
-
path: 'src/app.ts',
|
|
124
|
-
diff: '--- diff ---'
|
|
125
|
-
} as Extract<ApplyPatchOperation, { type: 'create_file' }>;
|
|
126
|
-
|
|
127
|
-
await editor.createFile(operation);
|
|
128
|
-
|
|
129
|
-
expect(applyDiffMock).toHaveBeenCalledWith('', operation.diff, 'create');
|
|
130
|
-
expect(mockSandbox.mkdir).toHaveBeenCalledWith('/workspace/src', {
|
|
131
|
-
recursive: true
|
|
132
|
-
});
|
|
133
|
-
expect(mockSandbox.writeFile).toHaveBeenCalledWith(
|
|
134
|
-
'/workspace/src/app.ts',
|
|
135
|
-
'file contents',
|
|
136
|
-
{ encoding: 'utf-8' }
|
|
137
|
-
);
|
|
138
|
-
expect(editor.results[0]).toMatchObject({
|
|
139
|
-
operation: 'create',
|
|
140
|
-
path: 'src/app.ts',
|
|
141
|
-
status: 'completed'
|
|
142
|
-
});
|
|
143
|
-
expect(loggerSpies.info).toHaveBeenCalledWith(
|
|
144
|
-
'File created successfully',
|
|
145
|
-
expect.objectContaining({ path: 'src/app.ts' })
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('applies diffs when updating existing files', async () => {
|
|
150
|
-
applyDiffMock.mockReturnValueOnce('patched content');
|
|
151
|
-
|
|
152
|
-
const mockSandbox: MockSandbox = {
|
|
153
|
-
readFile: vi.fn().mockResolvedValue({ content: 'original content' }),
|
|
154
|
-
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
158
|
-
const operation = {
|
|
159
|
-
type: 'update_file',
|
|
160
|
-
path: 'README.md',
|
|
161
|
-
diff: 'patch diff'
|
|
162
|
-
} as Extract<ApplyPatchOperation, { type: 'update_file' }>;
|
|
163
|
-
|
|
164
|
-
await editor.updateFile(operation);
|
|
165
|
-
|
|
166
|
-
expect(mockSandbox.readFile).toHaveBeenCalledWith('/workspace/README.md', {
|
|
167
|
-
encoding: 'utf-8'
|
|
168
|
-
});
|
|
169
|
-
expect(applyDiffMock).toHaveBeenCalledWith(
|
|
170
|
-
'original content',
|
|
171
|
-
operation.diff
|
|
172
|
-
);
|
|
173
|
-
expect(mockSandbox.writeFile).toHaveBeenCalledWith(
|
|
174
|
-
'/workspace/README.md',
|
|
175
|
-
'patched content',
|
|
176
|
-
{ encoding: 'utf-8' }
|
|
177
|
-
);
|
|
178
|
-
expect(editor.results[0]).toMatchObject({
|
|
179
|
-
operation: 'update',
|
|
180
|
-
path: 'README.md',
|
|
181
|
-
status: 'completed'
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('throws descriptive error when attempting to update a missing file', async () => {
|
|
186
|
-
const missingError = Object.assign(new Error('not found'), { status: 404 });
|
|
187
|
-
const mockSandbox: MockSandbox = {
|
|
188
|
-
readFile: vi.fn().mockRejectedValue(missingError)
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
192
|
-
const operation = {
|
|
193
|
-
type: 'update_file',
|
|
194
|
-
path: 'missing.txt',
|
|
195
|
-
diff: 'patch diff'
|
|
196
|
-
} as Extract<ApplyPatchOperation, { type: 'update_file' }>;
|
|
197
|
-
|
|
198
|
-
await expect(editor.updateFile(operation)).rejects.toThrow(
|
|
199
|
-
'Cannot update missing file: missing.txt'
|
|
200
|
-
);
|
|
201
|
-
expect(loggerSpies.error).toHaveBeenCalledWith(
|
|
202
|
-
'Cannot update missing file',
|
|
203
|
-
undefined,
|
|
204
|
-
{ path: 'missing.txt' }
|
|
205
|
-
);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe('Path traversal security', () => {
|
|
209
|
-
it('should reject path traversal attempts with ../', async () => {
|
|
210
|
-
const mockSandbox: MockSandbox = {
|
|
211
|
-
mkdir: vi.fn(),
|
|
212
|
-
writeFile: vi.fn()
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
216
|
-
const operation = {
|
|
217
|
-
type: 'create_file',
|
|
218
|
-
path: '../etc/passwd',
|
|
219
|
-
diff: 'malicious content'
|
|
220
|
-
} as Extract<ApplyPatchOperation, { type: 'create_file' }>;
|
|
221
|
-
|
|
222
|
-
await expect(editor.createFile(operation)).rejects.toThrow(
|
|
223
|
-
'Operation outside workspace: ../etc/passwd'
|
|
224
|
-
);
|
|
225
|
-
expect(mockSandbox.writeFile).not.toHaveBeenCalled();
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('should reject path traversal attempts with ../../', async () => {
|
|
229
|
-
const mockSandbox: MockSandbox = {
|
|
230
|
-
mkdir: vi.fn(),
|
|
231
|
-
writeFile: vi.fn()
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
235
|
-
const operation = {
|
|
236
|
-
type: 'create_file',
|
|
237
|
-
path: '../../etc/passwd',
|
|
238
|
-
diff: 'malicious content'
|
|
239
|
-
} as Extract<ApplyPatchOperation, { type: 'create_file' }>;
|
|
240
|
-
|
|
241
|
-
await expect(editor.createFile(operation)).rejects.toThrow(
|
|
242
|
-
'Operation outside workspace: ../../etc/passwd'
|
|
243
|
-
);
|
|
244
|
-
expect(mockSandbox.writeFile).not.toHaveBeenCalled();
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('should reject path traversal attempts with mixed paths like src/../../etc/passwd', async () => {
|
|
248
|
-
const mockSandbox: MockSandbox = {
|
|
249
|
-
mkdir: vi.fn(),
|
|
250
|
-
writeFile: vi.fn()
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
254
|
-
const operation = {
|
|
255
|
-
type: 'create_file',
|
|
256
|
-
path: 'src/../../etc/passwd',
|
|
257
|
-
diff: 'malicious content'
|
|
258
|
-
} as Extract<ApplyPatchOperation, { type: 'create_file' }>;
|
|
259
|
-
|
|
260
|
-
await expect(editor.createFile(operation)).rejects.toThrow(
|
|
261
|
-
'Operation outside workspace: src/../../etc/passwd'
|
|
262
|
-
);
|
|
263
|
-
expect(mockSandbox.writeFile).not.toHaveBeenCalled();
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('should reject path traversal attempts with leading slash /../../etc/passwd', async () => {
|
|
267
|
-
const mockSandbox: MockSandbox = {
|
|
268
|
-
mkdir: vi.fn(),
|
|
269
|
-
writeFile: vi.fn()
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
273
|
-
const operation = {
|
|
274
|
-
type: 'create_file',
|
|
275
|
-
path: '/../../etc/passwd',
|
|
276
|
-
diff: 'malicious content'
|
|
277
|
-
} as Extract<ApplyPatchOperation, { type: 'create_file' }>;
|
|
278
|
-
|
|
279
|
-
await expect(editor.createFile(operation)).rejects.toThrow(
|
|
280
|
-
'Operation outside workspace: /../../etc/passwd'
|
|
281
|
-
);
|
|
282
|
-
expect(mockSandbox.writeFile).not.toHaveBeenCalled();
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('should reject path traversal attempts with leading dot-slash ./../../etc/passwd', async () => {
|
|
286
|
-
const mockSandbox: MockSandbox = {
|
|
287
|
-
mkdir: vi.fn(),
|
|
288
|
-
writeFile: vi.fn()
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
292
|
-
const operation = {
|
|
293
|
-
type: 'create_file',
|
|
294
|
-
path: './../../etc/passwd',
|
|
295
|
-
diff: 'malicious content'
|
|
296
|
-
} as Extract<ApplyPatchOperation, { type: 'create_file' }>;
|
|
297
|
-
|
|
298
|
-
await expect(editor.createFile(operation)).rejects.toThrow(
|
|
299
|
-
'Operation outside workspace: ./../../etc/passwd'
|
|
300
|
-
);
|
|
301
|
-
expect(mockSandbox.writeFile).not.toHaveBeenCalled();
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('should reject path traversal in updateFile operations', async () => {
|
|
305
|
-
const mockSandbox: MockSandbox = {
|
|
306
|
-
readFile: vi.fn()
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
310
|
-
const operation = {
|
|
311
|
-
type: 'update_file',
|
|
312
|
-
path: '../../etc/passwd',
|
|
313
|
-
diff: 'patch diff'
|
|
314
|
-
} as Extract<ApplyPatchOperation, { type: 'update_file' }>;
|
|
315
|
-
|
|
316
|
-
await expect(editor.updateFile(operation)).rejects.toThrow(
|
|
317
|
-
'Operation outside workspace: ../../etc/passwd'
|
|
318
|
-
);
|
|
319
|
-
expect(mockSandbox.readFile).not.toHaveBeenCalled();
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it('should reject path traversal in deleteFile operations', async () => {
|
|
323
|
-
const mockSandbox: MockSandbox = {
|
|
324
|
-
deleteFile: vi.fn()
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
328
|
-
const operation = {
|
|
329
|
-
type: 'delete_file',
|
|
330
|
-
path: '../../etc/passwd'
|
|
331
|
-
} as Extract<ApplyPatchOperation, { type: 'delete_file' }>;
|
|
332
|
-
|
|
333
|
-
await expect(editor.deleteFile(operation)).rejects.toThrow(
|
|
334
|
-
'Operation outside workspace: ../../etc/passwd'
|
|
335
|
-
);
|
|
336
|
-
expect(mockSandbox.deleteFile).not.toHaveBeenCalled();
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it('should allow valid paths that use .. but stay within workspace', async () => {
|
|
340
|
-
applyDiffMock.mockReturnValueOnce('file contents');
|
|
341
|
-
|
|
342
|
-
const mockSandbox: MockSandbox = {
|
|
343
|
-
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
344
|
-
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
348
|
-
const operation = {
|
|
349
|
-
type: 'create_file',
|
|
350
|
-
path: 'src/subdir/../../file.txt',
|
|
351
|
-
diff: '--- diff ---'
|
|
352
|
-
} as Extract<ApplyPatchOperation, { type: 'create_file' }>;
|
|
353
|
-
|
|
354
|
-
await editor.createFile(operation);
|
|
355
|
-
|
|
356
|
-
// Should resolve to /workspace/file.txt
|
|
357
|
-
expect(mockSandbox.writeFile).toHaveBeenCalledWith(
|
|
358
|
-
'/workspace/file.txt',
|
|
359
|
-
'file contents',
|
|
360
|
-
{ encoding: 'utf-8' }
|
|
361
|
-
);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it('should handle paths with multiple consecutive slashes correctly', async () => {
|
|
365
|
-
applyDiffMock.mockReturnValueOnce('file contents');
|
|
366
|
-
|
|
367
|
-
const mockSandbox: MockSandbox = {
|
|
368
|
-
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
369
|
-
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
373
|
-
const operation = {
|
|
374
|
-
type: 'create_file',
|
|
375
|
-
path: 'src//subdir///file.txt',
|
|
376
|
-
diff: '--- diff ---'
|
|
377
|
-
} as Extract<ApplyPatchOperation, { type: 'create_file' }>;
|
|
378
|
-
|
|
379
|
-
await editor.createFile(operation);
|
|
380
|
-
|
|
381
|
-
expect(mockSandbox.writeFile).toHaveBeenCalledWith(
|
|
382
|
-
'/workspace/src/subdir/file.txt',
|
|
383
|
-
'file contents',
|
|
384
|
-
{ encoding: 'utf-8' }
|
|
385
|
-
);
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it('should reject deep path traversal attempts', async () => {
|
|
389
|
-
const mockSandbox: MockSandbox = {
|
|
390
|
-
mkdir: vi.fn(),
|
|
391
|
-
writeFile: vi.fn()
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
const editor = new Editor(mockSandbox as unknown as Sandbox);
|
|
395
|
-
const operation = {
|
|
396
|
-
type: 'create_file',
|
|
397
|
-
path: 'a/b/c/../../../../etc/passwd',
|
|
398
|
-
diff: 'malicious content'
|
|
399
|
-
} as Extract<ApplyPatchOperation, { type: 'create_file' }>;
|
|
400
|
-
|
|
401
|
-
await expect(editor.createFile(operation)).rejects.toThrow(
|
|
402
|
-
'Operation outside workspace: a/b/c/../../../../etc/passwd'
|
|
403
|
-
);
|
|
404
|
-
expect(mockSandbox.writeFile).not.toHaveBeenCalled();
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('should handle absolute paths within workspace', async () => {
|
|
408
|
-
applyDiffMock.mockReturnValueOnce('content');
|
|
409
|
-
|
|
410
|
-
const mockSandbox: MockSandbox = {
|
|
411
|
-
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
412
|
-
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
const editor = new Editor(
|
|
416
|
-
mockSandbox as unknown as Sandbox,
|
|
417
|
-
'/workspace'
|
|
418
|
-
);
|
|
419
|
-
const operation = {
|
|
420
|
-
type: 'create_file',
|
|
421
|
-
path: '/workspace/file.txt',
|
|
422
|
-
diff: 'content'
|
|
423
|
-
} as Extract<ApplyPatchOperation, { type: 'create_file' }>;
|
|
424
|
-
|
|
425
|
-
await editor.createFile(operation);
|
|
426
|
-
|
|
427
|
-
expect(mockSandbox.writeFile).toHaveBeenCalledWith(
|
|
428
|
-
'/workspace/file.txt', // Not /workspace/workspace/file.txt
|
|
429
|
-
'content',
|
|
430
|
-
{ encoding: 'utf-8' }
|
|
431
|
-
);
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
});
|
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
PortCloseResult,
|
|
3
|
-
PortExposeResult,
|
|
4
|
-
PortListResult
|
|
5
|
-
} from '@repo/shared';
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
-
import { PortClient } from '../src/clients/port-client';
|
|
8
|
-
import {
|
|
9
|
-
InvalidPortError,
|
|
10
|
-
PortAlreadyExposedError,
|
|
11
|
-
PortError,
|
|
12
|
-
PortInUseError,
|
|
13
|
-
PortNotExposedError,
|
|
14
|
-
SandboxError,
|
|
15
|
-
ServiceNotRespondingError
|
|
16
|
-
} from '../src/errors';
|
|
17
|
-
|
|
18
|
-
describe('PortClient', () => {
|
|
19
|
-
let client: PortClient;
|
|
20
|
-
let mockFetch: ReturnType<typeof vi.fn>;
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
vi.clearAllMocks();
|
|
24
|
-
|
|
25
|
-
mockFetch = vi.fn();
|
|
26
|
-
global.fetch = mockFetch as unknown as typeof fetch;
|
|
27
|
-
|
|
28
|
-
client = new PortClient({
|
|
29
|
-
baseUrl: 'http://test.com',
|
|
30
|
-
port: 3000
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
vi.restoreAllMocks();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('service exposure', () => {
|
|
39
|
-
it('should expose web services successfully', async () => {
|
|
40
|
-
const mockResponse: PortExposeResult = {
|
|
41
|
-
success: true,
|
|
42
|
-
port: 3001,
|
|
43
|
-
url: 'https://preview-abc123.workers.dev',
|
|
44
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
mockFetch.mockResolvedValue(
|
|
48
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const result = await client.exposePort(3001, 'session-123', 'web-server');
|
|
52
|
-
|
|
53
|
-
expect(result.success).toBe(true);
|
|
54
|
-
expect(result.port).toBe(3001);
|
|
55
|
-
expect(result.url).toBe('https://preview-abc123.workers.dev');
|
|
56
|
-
expect(result.url.startsWith('https://')).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should expose API services on different ports', async () => {
|
|
60
|
-
const mockResponse: PortExposeResult = {
|
|
61
|
-
success: true,
|
|
62
|
-
port: 8080,
|
|
63
|
-
url: 'https://api-def456.workers.dev',
|
|
64
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
mockFetch.mockResolvedValue(
|
|
68
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const result = await client.exposePort(8080, 'session-456', 'api-server');
|
|
72
|
-
|
|
73
|
-
expect(result.success).toBe(true);
|
|
74
|
-
expect(result.port).toBe(8080);
|
|
75
|
-
expect(result.url).toContain('api-');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should expose services without explicit names', async () => {
|
|
79
|
-
const mockResponse: PortExposeResult = {
|
|
80
|
-
success: true,
|
|
81
|
-
port: 5000,
|
|
82
|
-
url: 'https://service-ghi789.workers.dev',
|
|
83
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
mockFetch.mockResolvedValue(
|
|
87
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const result = await client.exposePort(5000, 'session-789');
|
|
91
|
-
|
|
92
|
-
expect(result.success).toBe(true);
|
|
93
|
-
expect(result.port).toBe(5000);
|
|
94
|
-
expect(result.url).toBeDefined();
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe('service management', () => {
|
|
99
|
-
it('should list all exposed services', async () => {
|
|
100
|
-
const mockResponse: PortListResult = {
|
|
101
|
-
success: true,
|
|
102
|
-
ports: [
|
|
103
|
-
{
|
|
104
|
-
port: 3000,
|
|
105
|
-
url: 'https://frontend-abc123.workers.dev',
|
|
106
|
-
status: 'active'
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
port: 4000,
|
|
110
|
-
url: 'https://api-def456.workers.dev',
|
|
111
|
-
status: 'active'
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
port: 5432,
|
|
115
|
-
url: 'https://db-ghi789.workers.dev',
|
|
116
|
-
status: 'active'
|
|
117
|
-
}
|
|
118
|
-
],
|
|
119
|
-
timestamp: '2023-01-01T00:10:00Z'
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
mockFetch.mockResolvedValue(
|
|
123
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const result = await client.getExposedPorts('session-list');
|
|
127
|
-
|
|
128
|
-
expect(result.success).toBe(true);
|
|
129
|
-
expect(result.ports).toHaveLength(3);
|
|
130
|
-
|
|
131
|
-
result.ports.forEach((service) => {
|
|
132
|
-
expect(service.url).toContain('.workers.dev');
|
|
133
|
-
expect(service.port).toBeGreaterThan(0);
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('should handle empty exposed ports list', async () => {
|
|
138
|
-
const mockResponse: PortListResult = {
|
|
139
|
-
success: true,
|
|
140
|
-
ports: [],
|
|
141
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
mockFetch.mockResolvedValue(
|
|
145
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
const result = await client.getExposedPorts('session-empty');
|
|
149
|
-
|
|
150
|
-
expect(result.success).toBe(true);
|
|
151
|
-
expect(result.ports).toHaveLength(0);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('should unexpose services cleanly', async () => {
|
|
155
|
-
const mockResponse: PortCloseResult = {
|
|
156
|
-
success: true,
|
|
157
|
-
port: 3001,
|
|
158
|
-
timestamp: '2023-01-01T00:15:00Z'
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
mockFetch.mockResolvedValue(
|
|
162
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
const result = await client.unexposePort(3001, 'session-unexpose');
|
|
166
|
-
|
|
167
|
-
expect(result.success).toBe(true);
|
|
168
|
-
expect(result.port).toBe(3001);
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe('port validation and error handling', () => {
|
|
173
|
-
it('should handle port already exposed errors', async () => {
|
|
174
|
-
const errorResponse = {
|
|
175
|
-
error: 'Port already exposed: 3000',
|
|
176
|
-
code: 'PORT_ALREADY_EXPOSED'
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
mockFetch.mockResolvedValue(
|
|
180
|
-
new Response(JSON.stringify(errorResponse), { status: 409 })
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
await expect(client.exposePort(3000, 'session-err')).rejects.toThrow(
|
|
184
|
-
PortAlreadyExposedError
|
|
185
|
-
);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should handle invalid port numbers', async () => {
|
|
189
|
-
const errorResponse = {
|
|
190
|
-
error: 'Invalid port number: 0',
|
|
191
|
-
code: 'INVALID_PORT_NUMBER'
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
mockFetch.mockResolvedValue(
|
|
195
|
-
new Response(JSON.stringify(errorResponse), { status: 400 })
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
await expect(client.exposePort(0, 'session-err')).rejects.toThrow(
|
|
199
|
-
InvalidPortError
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('should handle port in use errors', async () => {
|
|
204
|
-
const errorResponse = {
|
|
205
|
-
error: 'Port in use: 3000 is already bound by another process',
|
|
206
|
-
code: 'PORT_IN_USE'
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
mockFetch.mockResolvedValue(
|
|
210
|
-
new Response(JSON.stringify(errorResponse), { status: 409 })
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
await expect(client.exposePort(3000, 'session-err')).rejects.toThrow(
|
|
214
|
-
PortInUseError
|
|
215
|
-
);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('should handle service not responding errors', async () => {
|
|
219
|
-
const errorResponse = {
|
|
220
|
-
error: 'Service not responding on port 8080',
|
|
221
|
-
code: 'SERVICE_NOT_RESPONDING'
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
mockFetch.mockResolvedValue(
|
|
225
|
-
new Response(JSON.stringify(errorResponse), { status: 503 })
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
await expect(client.exposePort(8080, 'session-err')).rejects.toThrow(
|
|
229
|
-
ServiceNotRespondingError
|
|
230
|
-
);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('should handle unexpose non-existent port', async () => {
|
|
234
|
-
const errorResponse = {
|
|
235
|
-
error: 'Port not exposed: 9999',
|
|
236
|
-
code: 'PORT_NOT_EXPOSED'
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
mockFetch.mockResolvedValue(
|
|
240
|
-
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
await expect(client.unexposePort(9999, 'session-err')).rejects.toThrow(
|
|
244
|
-
PortNotExposedError
|
|
245
|
-
);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('should handle port operation failures', async () => {
|
|
249
|
-
const errorResponse = {
|
|
250
|
-
error: 'Port operation failed: unable to setup proxy',
|
|
251
|
-
code: 'PORT_OPERATION_ERROR'
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
mockFetch.mockResolvedValue(
|
|
255
|
-
new Response(JSON.stringify(errorResponse), { status: 500 })
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
await expect(client.exposePort(3000, 'session-err')).rejects.toThrow(
|
|
259
|
-
PortError
|
|
260
|
-
);
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
describe('edge cases and resilience', () => {
|
|
265
|
-
it('should handle network failures gracefully', async () => {
|
|
266
|
-
mockFetch.mockRejectedValue(new Error('Network connection failed'));
|
|
267
|
-
|
|
268
|
-
await expect(client.exposePort(3000, 'session-net')).rejects.toThrow(
|
|
269
|
-
'Network connection failed'
|
|
270
|
-
);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('should handle malformed server responses', async () => {
|
|
274
|
-
mockFetch.mockResolvedValue(
|
|
275
|
-
new Response('invalid json {', { status: 200 })
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
await expect(client.exposePort(3000, 'session-malform')).rejects.toThrow(
|
|
279
|
-
SandboxError
|
|
280
|
-
);
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
});
|