@cloudflare/sandbox 0.4.12 → 0.4.15
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 +46 -16
- package/Dockerfile +78 -31
- package/README.md +9 -2
- 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 +339 -149
- 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 +10 -10
- 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 +219 -67
- 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-JXZMAU2C.js +0 -559
- package/dist/chunk-JXZMAU2C.js.map +0 -1
- package/dist/chunk-UJ3TV4M6.js +0 -7
- package/dist/chunk-UJ3TV4M6.js.map +0 -1
- package/dist/chunk-YE265ASX.js +0 -2484
- package/dist/chunk-YE265ASX.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-CLZWpfGc.d.ts +0 -613
- 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
|
@@ -11,11 +11,11 @@ import type {
|
|
|
11
11
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
12
|
import { FileClient } from '../src/clients/file-client';
|
|
13
13
|
import {
|
|
14
|
-
FileExistsError,
|
|
15
|
-
FileNotFoundError,
|
|
14
|
+
FileExistsError,
|
|
15
|
+
FileNotFoundError,
|
|
16
16
|
FileSystemError,
|
|
17
|
-
PermissionDeniedError,
|
|
18
|
-
SandboxError
|
|
17
|
+
PermissionDeniedError,
|
|
18
|
+
SandboxError
|
|
19
19
|
} from '../src/errors';
|
|
20
20
|
|
|
21
21
|
describe('FileClient', () => {
|
|
@@ -24,13 +24,13 @@ describe('FileClient', () => {
|
|
|
24
24
|
|
|
25
25
|
beforeEach(() => {
|
|
26
26
|
vi.clearAllMocks();
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
mockFetch = vi.fn();
|
|
29
29
|
global.fetch = mockFetch as unknown as typeof fetch;
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
client = new FileClient({
|
|
32
32
|
baseUrl: 'http://test.com',
|
|
33
|
-
port: 3000
|
|
33
|
+
port: 3000
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -45,13 +45,12 @@ describe('FileClient', () => {
|
|
|
45
45
|
exitCode: 0,
|
|
46
46
|
path: '/app/new-directory',
|
|
47
47
|
recursive: false,
|
|
48
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
48
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
mockFetch.mockResolvedValue(
|
|
52
|
-
JSON.stringify(mockResponse),
|
|
53
|
-
|
|
54
|
-
));
|
|
51
|
+
mockFetch.mockResolvedValue(
|
|
52
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
53
|
+
);
|
|
55
54
|
|
|
56
55
|
const result = await client.mkdir('/app/new-directory', 'session-mkdir');
|
|
57
56
|
|
|
@@ -67,15 +66,18 @@ describe('FileClient', () => {
|
|
|
67
66
|
exitCode: 0,
|
|
68
67
|
path: '/app/deep/nested/directory',
|
|
69
68
|
recursive: true,
|
|
70
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
69
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
71
70
|
};
|
|
72
71
|
|
|
73
|
-
mockFetch.mockResolvedValue(
|
|
74
|
-
JSON.stringify(mockResponse),
|
|
75
|
-
|
|
76
|
-
));
|
|
72
|
+
mockFetch.mockResolvedValue(
|
|
73
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
74
|
+
);
|
|
77
75
|
|
|
78
|
-
const result = await client.mkdir(
|
|
76
|
+
const result = await client.mkdir(
|
|
77
|
+
'/app/deep/nested/directory',
|
|
78
|
+
'session-mkdir',
|
|
79
|
+
{ recursive: true }
|
|
80
|
+
);
|
|
79
81
|
|
|
80
82
|
expect(result.success).toBe(true);
|
|
81
83
|
expect(result.recursive).toBe(true);
|
|
@@ -89,13 +91,13 @@ describe('FileClient', () => {
|
|
|
89
91
|
path: '/root/secure'
|
|
90
92
|
};
|
|
91
93
|
|
|
92
|
-
mockFetch.mockResolvedValue(
|
|
93
|
-
JSON.stringify(errorResponse),
|
|
94
|
-
|
|
95
|
-
));
|
|
94
|
+
mockFetch.mockResolvedValue(
|
|
95
|
+
new Response(JSON.stringify(errorResponse), { status: 403 })
|
|
96
|
+
);
|
|
96
97
|
|
|
97
|
-
await expect(
|
|
98
|
-
.
|
|
98
|
+
await expect(
|
|
99
|
+
client.mkdir('/root/secure', 'session-mkdir')
|
|
100
|
+
).rejects.toThrow(PermissionDeniedError);
|
|
99
101
|
});
|
|
100
102
|
|
|
101
103
|
it('should handle directory already exists errors', async () => {
|
|
@@ -105,13 +107,13 @@ describe('FileClient', () => {
|
|
|
105
107
|
path: '/app/existing'
|
|
106
108
|
};
|
|
107
109
|
|
|
108
|
-
mockFetch.mockResolvedValue(
|
|
109
|
-
JSON.stringify(errorResponse),
|
|
110
|
-
|
|
111
|
-
));
|
|
110
|
+
mockFetch.mockResolvedValue(
|
|
111
|
+
new Response(JSON.stringify(errorResponse), { status: 409 })
|
|
112
|
+
);
|
|
112
113
|
|
|
113
|
-
await expect(
|
|
114
|
-
.
|
|
114
|
+
await expect(
|
|
115
|
+
client.mkdir('/app/existing', 'session-mkdir')
|
|
116
|
+
).rejects.toThrow(FileExistsError);
|
|
115
117
|
});
|
|
116
118
|
});
|
|
117
119
|
|
|
@@ -121,16 +123,19 @@ describe('FileClient', () => {
|
|
|
121
123
|
success: true,
|
|
122
124
|
exitCode: 0,
|
|
123
125
|
path: '/app/config.json',
|
|
124
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
126
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
125
127
|
};
|
|
126
128
|
|
|
127
|
-
mockFetch.mockResolvedValue(
|
|
128
|
-
JSON.stringify(mockResponse),
|
|
129
|
-
|
|
130
|
-
));
|
|
129
|
+
mockFetch.mockResolvedValue(
|
|
130
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
131
|
+
);
|
|
131
132
|
|
|
132
133
|
const content = '{"setting": "value", "enabled": true}';
|
|
133
|
-
const result = await client.writeFile(
|
|
134
|
+
const result = await client.writeFile(
|
|
135
|
+
'/app/config.json',
|
|
136
|
+
content,
|
|
137
|
+
'session-write'
|
|
138
|
+
);
|
|
134
139
|
|
|
135
140
|
expect(result.success).toBe(true);
|
|
136
141
|
expect(result.path).toBe('/app/config.json');
|
|
@@ -142,16 +147,21 @@ describe('FileClient', () => {
|
|
|
142
147
|
success: true,
|
|
143
148
|
exitCode: 0,
|
|
144
149
|
path: '/app/image.png',
|
|
145
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
150
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
146
151
|
};
|
|
147
152
|
|
|
148
|
-
mockFetch.mockResolvedValue(
|
|
149
|
-
JSON.stringify(mockResponse),
|
|
150
|
-
|
|
151
|
-
));
|
|
153
|
+
mockFetch.mockResolvedValue(
|
|
154
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
155
|
+
);
|
|
152
156
|
|
|
153
|
-
const binaryData =
|
|
154
|
-
|
|
157
|
+
const binaryData =
|
|
158
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jYlkKQAAAABJRU5ErkJggg==';
|
|
159
|
+
const result = await client.writeFile(
|
|
160
|
+
'/app/image.png',
|
|
161
|
+
binaryData,
|
|
162
|
+
'session-write',
|
|
163
|
+
{ encoding: 'base64' }
|
|
164
|
+
);
|
|
155
165
|
|
|
156
166
|
expect(result.success).toBe(true);
|
|
157
167
|
expect(result.path).toBe('/app/image.png');
|
|
@@ -164,13 +174,13 @@ describe('FileClient', () => {
|
|
|
164
174
|
path: '/system/readonly.txt'
|
|
165
175
|
};
|
|
166
176
|
|
|
167
|
-
mockFetch.mockResolvedValue(
|
|
168
|
-
JSON.stringify(errorResponse),
|
|
169
|
-
|
|
170
|
-
));
|
|
177
|
+
mockFetch.mockResolvedValue(
|
|
178
|
+
new Response(JSON.stringify(errorResponse), { status: 403 })
|
|
179
|
+
);
|
|
171
180
|
|
|
172
|
-
await expect(
|
|
173
|
-
.
|
|
181
|
+
await expect(
|
|
182
|
+
client.writeFile('/system/readonly.txt', 'content', 'session-err')
|
|
183
|
+
).rejects.toThrow(PermissionDeniedError);
|
|
174
184
|
});
|
|
175
185
|
|
|
176
186
|
it('should handle disk space errors', async () => {
|
|
@@ -180,13 +190,17 @@ describe('FileClient', () => {
|
|
|
180
190
|
path: '/app/largefile.dat'
|
|
181
191
|
};
|
|
182
192
|
|
|
183
|
-
mockFetch.mockResolvedValue(
|
|
184
|
-
JSON.stringify(errorResponse),
|
|
185
|
-
|
|
186
|
-
));
|
|
193
|
+
mockFetch.mockResolvedValue(
|
|
194
|
+
new Response(JSON.stringify(errorResponse), { status: 507 })
|
|
195
|
+
);
|
|
187
196
|
|
|
188
|
-
await expect(
|
|
189
|
-
.
|
|
197
|
+
await expect(
|
|
198
|
+
client.writeFile(
|
|
199
|
+
'/app/largefile.dat',
|
|
200
|
+
'x'.repeat(1000000),
|
|
201
|
+
'session-err'
|
|
202
|
+
)
|
|
203
|
+
).rejects.toThrow(FileSystemError);
|
|
190
204
|
});
|
|
191
205
|
});
|
|
192
206
|
|
|
@@ -208,13 +222,12 @@ database:
|
|
|
208
222
|
encoding: 'utf-8',
|
|
209
223
|
isBinary: false,
|
|
210
224
|
mimeType: 'text/yaml',
|
|
211
|
-
size: 100
|
|
225
|
+
size: 100
|
|
212
226
|
};
|
|
213
227
|
|
|
214
|
-
mockFetch.mockResolvedValue(
|
|
215
|
-
JSON.stringify(mockResponse),
|
|
216
|
-
|
|
217
|
-
));
|
|
228
|
+
mockFetch.mockResolvedValue(
|
|
229
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
230
|
+
);
|
|
218
231
|
|
|
219
232
|
const result = await client.readFile('/app/config.yaml', 'session-read');
|
|
220
233
|
|
|
@@ -230,7 +243,8 @@ database:
|
|
|
230
243
|
});
|
|
231
244
|
|
|
232
245
|
it('should read binary files with base64 encoding and metadata', async () => {
|
|
233
|
-
const binaryContent =
|
|
246
|
+
const binaryContent =
|
|
247
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jYlkKQAAAABJRU5ErkJggg==';
|
|
234
248
|
const mockResponse: ReadFileResult = {
|
|
235
249
|
success: true,
|
|
236
250
|
exitCode: 0,
|
|
@@ -240,15 +254,16 @@ database:
|
|
|
240
254
|
encoding: 'base64',
|
|
241
255
|
isBinary: true,
|
|
242
256
|
mimeType: 'image/png',
|
|
243
|
-
size: 95
|
|
257
|
+
size: 95
|
|
244
258
|
};
|
|
245
259
|
|
|
246
|
-
mockFetch.mockResolvedValue(
|
|
247
|
-
JSON.stringify(mockResponse),
|
|
248
|
-
|
|
249
|
-
));
|
|
260
|
+
mockFetch.mockResolvedValue(
|
|
261
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
262
|
+
);
|
|
250
263
|
|
|
251
|
-
const result = await client.readFile('/app/logo.png', 'session-read', {
|
|
264
|
+
const result = await client.readFile('/app/logo.png', 'session-read', {
|
|
265
|
+
encoding: 'base64'
|
|
266
|
+
});
|
|
252
267
|
|
|
253
268
|
expect(result.success).toBe(true);
|
|
254
269
|
expect(result.content).toBe(binaryContent);
|
|
@@ -266,13 +281,13 @@ database:
|
|
|
266
281
|
path: '/app/missing.txt'
|
|
267
282
|
};
|
|
268
283
|
|
|
269
|
-
mockFetch.mockResolvedValue(
|
|
270
|
-
JSON.stringify(errorResponse),
|
|
271
|
-
|
|
272
|
-
));
|
|
284
|
+
mockFetch.mockResolvedValue(
|
|
285
|
+
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
286
|
+
);
|
|
273
287
|
|
|
274
|
-
await expect(
|
|
275
|
-
.
|
|
288
|
+
await expect(
|
|
289
|
+
client.readFile('/app/missing.txt', 'session-read')
|
|
290
|
+
).rejects.toThrow(FileNotFoundError);
|
|
276
291
|
});
|
|
277
292
|
|
|
278
293
|
it('should handle directory read attempts', async () => {
|
|
@@ -282,13 +297,13 @@ database:
|
|
|
282
297
|
path: '/app/logs'
|
|
283
298
|
};
|
|
284
299
|
|
|
285
|
-
mockFetch.mockResolvedValue(
|
|
286
|
-
JSON.stringify(errorResponse),
|
|
287
|
-
|
|
288
|
-
));
|
|
300
|
+
mockFetch.mockResolvedValue(
|
|
301
|
+
new Response(JSON.stringify(errorResponse), { status: 400 })
|
|
302
|
+
);
|
|
289
303
|
|
|
290
|
-
await expect(
|
|
291
|
-
.
|
|
304
|
+
await expect(
|
|
305
|
+
client.readFile('/app/logs', 'session-read')
|
|
306
|
+
).rejects.toThrow(FileSystemError);
|
|
292
307
|
});
|
|
293
308
|
});
|
|
294
309
|
|
|
@@ -296,19 +311,36 @@ database:
|
|
|
296
311
|
it('should stream file successfully', async () => {
|
|
297
312
|
const mockStream = new ReadableStream({
|
|
298
313
|
start(controller) {
|
|
299
|
-
controller.enqueue(
|
|
300
|
-
|
|
301
|
-
|
|
314
|
+
controller.enqueue(
|
|
315
|
+
new TextEncoder().encode(
|
|
316
|
+
'data: {"type":"metadata","mimeType":"text/plain","size":100,"isBinary":false,"encoding":"utf-8"}\n\n'
|
|
317
|
+
)
|
|
318
|
+
);
|
|
319
|
+
controller.enqueue(
|
|
320
|
+
new TextEncoder().encode(
|
|
321
|
+
'data: {"type":"chunk","data":"Hello"}\n\n'
|
|
322
|
+
)
|
|
323
|
+
);
|
|
324
|
+
controller.enqueue(
|
|
325
|
+
new TextEncoder().encode(
|
|
326
|
+
'data: {"type":"complete","bytesRead":5}\n\n'
|
|
327
|
+
)
|
|
328
|
+
);
|
|
302
329
|
controller.close();
|
|
303
330
|
}
|
|
304
331
|
});
|
|
305
332
|
|
|
306
|
-
mockFetch.mockResolvedValue(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
333
|
+
mockFetch.mockResolvedValue(
|
|
334
|
+
new Response(mockStream, {
|
|
335
|
+
status: 200,
|
|
336
|
+
headers: { 'Content-Type': 'text/event-stream' }
|
|
337
|
+
})
|
|
338
|
+
);
|
|
310
339
|
|
|
311
|
-
const result = await client.readFileStream(
|
|
340
|
+
const result = await client.readFileStream(
|
|
341
|
+
'/app/test.txt',
|
|
342
|
+
'session-stream'
|
|
343
|
+
);
|
|
312
344
|
|
|
313
345
|
expect(result).toBeInstanceOf(ReadableStream);
|
|
314
346
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
@@ -317,7 +349,7 @@ database:
|
|
|
317
349
|
method: 'POST',
|
|
318
350
|
body: JSON.stringify({
|
|
319
351
|
path: '/app/test.txt',
|
|
320
|
-
sessionId: 'session-stream'
|
|
352
|
+
sessionId: 'session-stream'
|
|
321
353
|
})
|
|
322
354
|
})
|
|
323
355
|
);
|
|
@@ -326,19 +358,36 @@ database:
|
|
|
326
358
|
it('should handle binary file streams', async () => {
|
|
327
359
|
const mockStream = new ReadableStream({
|
|
328
360
|
start(controller) {
|
|
329
|
-
controller.enqueue(
|
|
330
|
-
|
|
331
|
-
|
|
361
|
+
controller.enqueue(
|
|
362
|
+
new TextEncoder().encode(
|
|
363
|
+
'data: {"type":"metadata","mimeType":"image/png","size":1024,"isBinary":true,"encoding":"base64"}\n\n'
|
|
364
|
+
)
|
|
365
|
+
);
|
|
366
|
+
controller.enqueue(
|
|
367
|
+
new TextEncoder().encode(
|
|
368
|
+
'data: {"type":"chunk","data":"iVBORw0K"}\n\n'
|
|
369
|
+
)
|
|
370
|
+
);
|
|
371
|
+
controller.enqueue(
|
|
372
|
+
new TextEncoder().encode(
|
|
373
|
+
'data: {"type":"complete","bytesRead":1024}\n\n'
|
|
374
|
+
)
|
|
375
|
+
);
|
|
332
376
|
controller.close();
|
|
333
377
|
}
|
|
334
378
|
});
|
|
335
379
|
|
|
336
|
-
mockFetch.mockResolvedValue(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
380
|
+
mockFetch.mockResolvedValue(
|
|
381
|
+
new Response(mockStream, {
|
|
382
|
+
status: 200,
|
|
383
|
+
headers: { 'Content-Type': 'text/event-stream' }
|
|
384
|
+
})
|
|
385
|
+
);
|
|
340
386
|
|
|
341
|
-
const result = await client.readFileStream(
|
|
387
|
+
const result = await client.readFileStream(
|
|
388
|
+
'/app/image.png',
|
|
389
|
+
'session-stream'
|
|
390
|
+
);
|
|
342
391
|
|
|
343
392
|
expect(result).toBeInstanceOf(ReadableStream);
|
|
344
393
|
});
|
|
@@ -350,20 +399,21 @@ database:
|
|
|
350
399
|
path: '/app/missing.txt'
|
|
351
400
|
};
|
|
352
401
|
|
|
353
|
-
mockFetch.mockResolvedValue(
|
|
354
|
-
JSON.stringify(errorResponse),
|
|
355
|
-
|
|
356
|
-
));
|
|
402
|
+
mockFetch.mockResolvedValue(
|
|
403
|
+
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
404
|
+
);
|
|
357
405
|
|
|
358
|
-
await expect(
|
|
359
|
-
.
|
|
406
|
+
await expect(
|
|
407
|
+
client.readFileStream('/app/missing.txt', 'session-stream')
|
|
408
|
+
).rejects.toThrow(FileNotFoundError);
|
|
360
409
|
});
|
|
361
410
|
|
|
362
411
|
it('should handle network errors during streaming', async () => {
|
|
363
412
|
mockFetch.mockRejectedValue(new Error('Network timeout'));
|
|
364
413
|
|
|
365
|
-
await expect(
|
|
366
|
-
.
|
|
414
|
+
await expect(
|
|
415
|
+
client.readFileStream('/app/file.txt', 'session-stream')
|
|
416
|
+
).rejects.toThrow('Network timeout');
|
|
367
417
|
});
|
|
368
418
|
});
|
|
369
419
|
|
|
@@ -373,13 +423,12 @@ database:
|
|
|
373
423
|
success: true,
|
|
374
424
|
exitCode: 0,
|
|
375
425
|
path: '/app/temp.txt',
|
|
376
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
426
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
377
427
|
};
|
|
378
428
|
|
|
379
|
-
mockFetch.mockResolvedValue(
|
|
380
|
-
JSON.stringify(mockResponse),
|
|
381
|
-
|
|
382
|
-
));
|
|
429
|
+
mockFetch.mockResolvedValue(
|
|
430
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
431
|
+
);
|
|
383
432
|
|
|
384
433
|
const result = await client.deleteFile('/app/temp.txt', 'session-delete');
|
|
385
434
|
|
|
@@ -395,13 +444,13 @@ database:
|
|
|
395
444
|
path: '/app/nonexistent.txt'
|
|
396
445
|
};
|
|
397
446
|
|
|
398
|
-
mockFetch.mockResolvedValue(
|
|
399
|
-
JSON.stringify(errorResponse),
|
|
400
|
-
|
|
401
|
-
));
|
|
447
|
+
mockFetch.mockResolvedValue(
|
|
448
|
+
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
449
|
+
);
|
|
402
450
|
|
|
403
|
-
await expect(
|
|
404
|
-
.
|
|
451
|
+
await expect(
|
|
452
|
+
client.deleteFile('/app/nonexistent.txt', 'session-delete')
|
|
453
|
+
).rejects.toThrow(FileNotFoundError);
|
|
405
454
|
});
|
|
406
455
|
|
|
407
456
|
it('should handle delete permission errors', async () => {
|
|
@@ -411,13 +460,13 @@ database:
|
|
|
411
460
|
path: '/system/important.conf'
|
|
412
461
|
};
|
|
413
462
|
|
|
414
|
-
mockFetch.mockResolvedValue(
|
|
415
|
-
JSON.stringify(errorResponse),
|
|
416
|
-
|
|
417
|
-
));
|
|
463
|
+
mockFetch.mockResolvedValue(
|
|
464
|
+
new Response(JSON.stringify(errorResponse), { status: 403 })
|
|
465
|
+
);
|
|
418
466
|
|
|
419
|
-
await expect(
|
|
420
|
-
.
|
|
467
|
+
await expect(
|
|
468
|
+
client.deleteFile('/system/important.conf', 'session-delete')
|
|
469
|
+
).rejects.toThrow(PermissionDeniedError);
|
|
421
470
|
});
|
|
422
471
|
});
|
|
423
472
|
|
|
@@ -428,15 +477,18 @@ database:
|
|
|
428
477
|
exitCode: 0,
|
|
429
478
|
path: '/app/old-name.txt',
|
|
430
479
|
newPath: '/app/new-name.txt',
|
|
431
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
480
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
432
481
|
};
|
|
433
482
|
|
|
434
|
-
mockFetch.mockResolvedValue(
|
|
435
|
-
JSON.stringify(mockResponse),
|
|
436
|
-
|
|
437
|
-
));
|
|
483
|
+
mockFetch.mockResolvedValue(
|
|
484
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
485
|
+
);
|
|
438
486
|
|
|
439
|
-
const result = await client.renameFile(
|
|
487
|
+
const result = await client.renameFile(
|
|
488
|
+
'/app/old-name.txt',
|
|
489
|
+
'/app/new-name.txt',
|
|
490
|
+
'session-rename'
|
|
491
|
+
);
|
|
440
492
|
|
|
441
493
|
expect(result.success).toBe(true);
|
|
442
494
|
expect(result.path).toBe('/app/old-name.txt');
|
|
@@ -451,13 +503,17 @@ database:
|
|
|
451
503
|
path: '/app/existing.txt'
|
|
452
504
|
};
|
|
453
505
|
|
|
454
|
-
mockFetch.mockResolvedValue(
|
|
455
|
-
JSON.stringify(errorResponse),
|
|
456
|
-
|
|
457
|
-
));
|
|
506
|
+
mockFetch.mockResolvedValue(
|
|
507
|
+
new Response(JSON.stringify(errorResponse), { status: 409 })
|
|
508
|
+
);
|
|
458
509
|
|
|
459
|
-
await expect(
|
|
460
|
-
.
|
|
510
|
+
await expect(
|
|
511
|
+
client.renameFile(
|
|
512
|
+
'/app/source.txt',
|
|
513
|
+
'/app/existing.txt',
|
|
514
|
+
'session-rename'
|
|
515
|
+
)
|
|
516
|
+
).rejects.toThrow(FileExistsError);
|
|
461
517
|
});
|
|
462
518
|
});
|
|
463
519
|
|
|
@@ -468,15 +524,18 @@ database:
|
|
|
468
524
|
exitCode: 0,
|
|
469
525
|
path: '/src/document.pdf',
|
|
470
526
|
newPath: '/dest/document.pdf',
|
|
471
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
527
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
472
528
|
};
|
|
473
529
|
|
|
474
|
-
mockFetch.mockResolvedValue(
|
|
475
|
-
JSON.stringify(mockResponse),
|
|
476
|
-
|
|
477
|
-
));
|
|
530
|
+
mockFetch.mockResolvedValue(
|
|
531
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
532
|
+
);
|
|
478
533
|
|
|
479
|
-
const result = await client.moveFile(
|
|
534
|
+
const result = await client.moveFile(
|
|
535
|
+
'/src/document.pdf',
|
|
536
|
+
'/dest/document.pdf',
|
|
537
|
+
'session-move'
|
|
538
|
+
);
|
|
480
539
|
|
|
481
540
|
expect(result.success).toBe(true);
|
|
482
541
|
expect(result.path).toBe('/src/document.pdf');
|
|
@@ -491,13 +550,17 @@ database:
|
|
|
491
550
|
path: '/nonexistent/'
|
|
492
551
|
};
|
|
493
552
|
|
|
494
|
-
mockFetch.mockResolvedValue(
|
|
495
|
-
JSON.stringify(errorResponse),
|
|
496
|
-
|
|
497
|
-
));
|
|
553
|
+
mockFetch.mockResolvedValue(
|
|
554
|
+
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
555
|
+
);
|
|
498
556
|
|
|
499
|
-
await expect(
|
|
500
|
-
.
|
|
557
|
+
await expect(
|
|
558
|
+
client.moveFile(
|
|
559
|
+
'/app/file.txt',
|
|
560
|
+
'/nonexistent/file.txt',
|
|
561
|
+
'session-move'
|
|
562
|
+
)
|
|
563
|
+
).rejects.toThrow(FileSystemError);
|
|
501
564
|
});
|
|
502
565
|
});
|
|
503
566
|
|
|
@@ -511,7 +574,7 @@ database:
|
|
|
511
574
|
modifiedAt: '2023-01-01T00:00:00Z',
|
|
512
575
|
mode: 'rw-r--r--',
|
|
513
576
|
permissions: { readable: true, writable: true, executable: false },
|
|
514
|
-
...overrides
|
|
577
|
+
...overrides
|
|
515
578
|
});
|
|
516
579
|
|
|
517
580
|
it('should list files with correct structure', async () => {
|
|
@@ -520,13 +583,15 @@ database:
|
|
|
520
583
|
path: '/workspace',
|
|
521
584
|
files: [
|
|
522
585
|
createMockFile({ name: 'file.txt' }),
|
|
523
|
-
createMockFile({ name: 'dir', type: 'directory', mode: 'rwxr-xr-x' })
|
|
586
|
+
createMockFile({ name: 'dir', type: 'directory', mode: 'rwxr-xr-x' })
|
|
524
587
|
],
|
|
525
588
|
count: 2,
|
|
526
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
589
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
527
590
|
};
|
|
528
591
|
|
|
529
|
-
mockFetch.mockResolvedValue(
|
|
592
|
+
mockFetch.mockResolvedValue(
|
|
593
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
594
|
+
);
|
|
530
595
|
|
|
531
596
|
const result = await client.listFiles('/workspace', 'session-list');
|
|
532
597
|
|
|
@@ -543,12 +608,17 @@ database:
|
|
|
543
608
|
path: '/workspace',
|
|
544
609
|
files: [createMockFile({ name: '.hidden', relativePath: '.hidden' })],
|
|
545
610
|
count: 1,
|
|
546
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
611
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
547
612
|
};
|
|
548
613
|
|
|
549
|
-
mockFetch.mockResolvedValue(
|
|
614
|
+
mockFetch.mockResolvedValue(
|
|
615
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
616
|
+
);
|
|
550
617
|
|
|
551
|
-
await client.listFiles('/workspace', 'session-list', {
|
|
618
|
+
await client.listFiles('/workspace', 'session-list', {
|
|
619
|
+
recursive: true,
|
|
620
|
+
includeHidden: true
|
|
621
|
+
});
|
|
552
622
|
|
|
553
623
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
554
624
|
expect.stringContaining('/api/list-files'),
|
|
@@ -556,17 +626,25 @@ database:
|
|
|
556
626
|
body: JSON.stringify({
|
|
557
627
|
path: '/workspace',
|
|
558
628
|
sessionId: 'session-list',
|
|
559
|
-
options: { recursive: true, includeHidden: true }
|
|
629
|
+
options: { recursive: true, includeHidden: true }
|
|
560
630
|
})
|
|
561
631
|
})
|
|
562
632
|
);
|
|
563
633
|
});
|
|
564
634
|
|
|
565
635
|
it('should handle empty directories', async () => {
|
|
566
|
-
mockFetch.mockResolvedValue(
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
636
|
+
mockFetch.mockResolvedValue(
|
|
637
|
+
new Response(
|
|
638
|
+
JSON.stringify({
|
|
639
|
+
success: true,
|
|
640
|
+
path: '/empty',
|
|
641
|
+
files: [],
|
|
642
|
+
count: 0,
|
|
643
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
644
|
+
}),
|
|
645
|
+
{ status: 200 }
|
|
646
|
+
)
|
|
647
|
+
);
|
|
570
648
|
|
|
571
649
|
const result = await client.listFiles('/empty', 'session-list');
|
|
572
650
|
|
|
@@ -575,13 +653,19 @@ database:
|
|
|
575
653
|
});
|
|
576
654
|
|
|
577
655
|
it('should handle error responses', async () => {
|
|
578
|
-
mockFetch.mockResolvedValue(
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
656
|
+
mockFetch.mockResolvedValue(
|
|
657
|
+
new Response(
|
|
658
|
+
JSON.stringify({
|
|
659
|
+
error: 'Directory not found',
|
|
660
|
+
code: 'FILE_NOT_FOUND'
|
|
661
|
+
}),
|
|
662
|
+
{ status: 404 }
|
|
663
|
+
)
|
|
664
|
+
);
|
|
582
665
|
|
|
583
|
-
await expect(
|
|
584
|
-
.
|
|
666
|
+
await expect(
|
|
667
|
+
client.listFiles('/nonexistent', 'session-list')
|
|
668
|
+
).rejects.toThrow(FileNotFoundError);
|
|
585
669
|
});
|
|
586
670
|
});
|
|
587
671
|
|
|
@@ -591,12 +675,17 @@ database:
|
|
|
591
675
|
success: true,
|
|
592
676
|
path: '/workspace/test.txt',
|
|
593
677
|
exists: true,
|
|
594
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
678
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
595
679
|
};
|
|
596
680
|
|
|
597
|
-
mockFetch.mockResolvedValue(
|
|
681
|
+
mockFetch.mockResolvedValue(
|
|
682
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
683
|
+
);
|
|
598
684
|
|
|
599
|
-
const result = await client.exists(
|
|
685
|
+
const result = await client.exists(
|
|
686
|
+
'/workspace/test.txt',
|
|
687
|
+
'session-exists'
|
|
688
|
+
);
|
|
600
689
|
|
|
601
690
|
expect(result.success).toBe(true);
|
|
602
691
|
expect(result.exists).toBe(true);
|
|
@@ -608,12 +697,17 @@ database:
|
|
|
608
697
|
success: true,
|
|
609
698
|
path: '/workspace/nonexistent.txt',
|
|
610
699
|
exists: false,
|
|
611
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
700
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
612
701
|
};
|
|
613
702
|
|
|
614
|
-
mockFetch.mockResolvedValue(
|
|
703
|
+
mockFetch.mockResolvedValue(
|
|
704
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
705
|
+
);
|
|
615
706
|
|
|
616
|
-
const result = await client.exists(
|
|
707
|
+
const result = await client.exists(
|
|
708
|
+
'/workspace/nonexistent.txt',
|
|
709
|
+
'session-exists'
|
|
710
|
+
);
|
|
617
711
|
|
|
618
712
|
expect(result.success).toBe(true);
|
|
619
713
|
expect(result.exists).toBe(false);
|
|
@@ -624,12 +718,17 @@ database:
|
|
|
624
718
|
success: true,
|
|
625
719
|
path: '/workspace/some-dir',
|
|
626
720
|
exists: true,
|
|
627
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
721
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
628
722
|
};
|
|
629
723
|
|
|
630
|
-
mockFetch.mockResolvedValue(
|
|
724
|
+
mockFetch.mockResolvedValue(
|
|
725
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
726
|
+
);
|
|
631
727
|
|
|
632
|
-
const result = await client.exists(
|
|
728
|
+
const result = await client.exists(
|
|
729
|
+
'/workspace/some-dir',
|
|
730
|
+
'session-exists'
|
|
731
|
+
);
|
|
633
732
|
|
|
634
733
|
expect(result.success).toBe(true);
|
|
635
734
|
expect(result.exists).toBe(true);
|
|
@@ -640,10 +739,12 @@ database:
|
|
|
640
739
|
success: true,
|
|
641
740
|
path: '/test/path',
|
|
642
741
|
exists: true,
|
|
643
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
742
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
644
743
|
};
|
|
645
744
|
|
|
646
|
-
mockFetch.mockResolvedValue(
|
|
745
|
+
mockFetch.mockResolvedValue(
|
|
746
|
+
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
747
|
+
);
|
|
647
748
|
|
|
648
749
|
await client.exists('/test/path', 'session-test');
|
|
649
750
|
|
|
@@ -653,7 +754,7 @@ database:
|
|
|
653
754
|
method: 'POST',
|
|
654
755
|
body: JSON.stringify({
|
|
655
756
|
path: '/test/path',
|
|
656
|
-
sessionId: 'session-test'
|
|
757
|
+
sessionId: 'session-test'
|
|
657
758
|
})
|
|
658
759
|
})
|
|
659
760
|
);
|
|
@@ -664,40 +765,51 @@ database:
|
|
|
664
765
|
it('should handle network failures gracefully', async () => {
|
|
665
766
|
mockFetch.mockRejectedValue(new Error('Network connection failed'));
|
|
666
767
|
|
|
667
|
-
await expect(
|
|
668
|
-
.
|
|
768
|
+
await expect(
|
|
769
|
+
client.readFile('/app/file.txt', 'session-read')
|
|
770
|
+
).rejects.toThrow('Network connection failed');
|
|
669
771
|
});
|
|
670
772
|
|
|
671
773
|
it('should handle malformed server responses', async () => {
|
|
672
|
-
mockFetch.mockResolvedValue(
|
|
673
|
-
'invalid json {',
|
|
674
|
-
|
|
675
|
-
|
|
774
|
+
mockFetch.mockResolvedValue(
|
|
775
|
+
new Response('invalid json {', {
|
|
776
|
+
status: 200,
|
|
777
|
+
headers: { 'Content-Type': 'application/json' }
|
|
778
|
+
})
|
|
779
|
+
);
|
|
676
780
|
|
|
677
|
-
await expect(
|
|
678
|
-
.
|
|
781
|
+
await expect(
|
|
782
|
+
client.writeFile('/app/file.txt', 'content', 'session-err')
|
|
783
|
+
).rejects.toThrow(SandboxError);
|
|
679
784
|
});
|
|
680
785
|
|
|
681
786
|
it('should handle server errors with proper mapping', async () => {
|
|
682
787
|
const serverErrorScenarios = [
|
|
683
788
|
{ status: 400, code: 'FILESYSTEM_ERROR', error: FileSystemError },
|
|
684
|
-
{
|
|
789
|
+
{
|
|
790
|
+
status: 403,
|
|
791
|
+
code: 'PERMISSION_DENIED',
|
|
792
|
+
error: PermissionDeniedError
|
|
793
|
+
},
|
|
685
794
|
{ status: 404, code: 'FILE_NOT_FOUND', error: FileNotFoundError },
|
|
686
795
|
{ status: 409, code: 'FILE_EXISTS', error: FileExistsError },
|
|
687
|
-
{ status: 500, code: 'INTERNAL_ERROR', error: SandboxError }
|
|
796
|
+
{ status: 500, code: 'INTERNAL_ERROR', error: SandboxError }
|
|
688
797
|
];
|
|
689
798
|
|
|
690
799
|
for (const scenario of serverErrorScenarios) {
|
|
691
|
-
mockFetch.mockResolvedValueOnce(
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
800
|
+
mockFetch.mockResolvedValueOnce(
|
|
801
|
+
new Response(
|
|
802
|
+
JSON.stringify({
|
|
803
|
+
error: 'Test error',
|
|
804
|
+
code: scenario.code
|
|
805
|
+
}),
|
|
806
|
+
{ status: scenario.status }
|
|
807
|
+
)
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
await expect(
|
|
811
|
+
client.readFile('/app/test.txt', 'session-read')
|
|
812
|
+
).rejects.toThrow(scenario.error);
|
|
701
813
|
}
|
|
702
814
|
});
|
|
703
815
|
});
|
|
@@ -711,9 +823,9 @@ database:
|
|
|
711
823
|
it('should initialize with full options', () => {
|
|
712
824
|
const fullOptionsClient = new FileClient({
|
|
713
825
|
baseUrl: 'http://custom.com',
|
|
714
|
-
port: 8080
|
|
826
|
+
port: 8080
|
|
715
827
|
});
|
|
716
828
|
expect(fullOptionsClient).toBeDefined();
|
|
717
829
|
});
|
|
718
830
|
});
|
|
719
|
-
});
|
|
831
|
+
});
|