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