@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +158 -15
  2. package/Dockerfile +88 -71
  3. package/LICENSE +176 -0
  4. package/README.md +10 -5
  5. package/dist/index.d.ts +1953 -9
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3278 -53
  8. package/dist/index.js.map +1 -1
  9. package/package.json +11 -9
  10. package/src/clients/base-client.ts +39 -24
  11. package/src/clients/command-client.ts +8 -8
  12. package/src/clients/file-client.ts +51 -20
  13. package/src/clients/git-client.ts +9 -3
  14. package/src/clients/index.ts +16 -15
  15. package/src/clients/interpreter-client.ts +51 -47
  16. package/src/clients/port-client.ts +10 -10
  17. package/src/clients/process-client.ts +11 -8
  18. package/src/clients/sandbox-client.ts +2 -4
  19. package/src/clients/types.ts +6 -2
  20. package/src/clients/utility-client.ts +67 -5
  21. package/src/errors/adapter.ts +90 -32
  22. package/src/errors/classes.ts +189 -64
  23. package/src/errors/index.ts +9 -5
  24. package/src/file-stream.ts +11 -6
  25. package/src/index.ts +28 -17
  26. package/src/interpreter.ts +50 -41
  27. package/src/request-handler.ts +34 -21
  28. package/src/sandbox.ts +502 -151
  29. package/src/security.ts +21 -6
  30. package/src/sse-parser.ts +4 -3
  31. package/src/version.ts +6 -0
  32. package/startup.sh +1 -1
  33. package/tests/base-client.test.ts +116 -80
  34. package/tests/command-client.test.ts +149 -112
  35. package/tests/file-client.test.ts +373 -185
  36. package/tests/file-stream.test.ts +24 -20
  37. package/tests/get-sandbox.test.ts +149 -0
  38. package/tests/git-client.test.ts +260 -101
  39. package/tests/port-client.test.ts +100 -108
  40. package/tests/process-client.test.ts +204 -179
  41. package/tests/request-handler.test.ts +292 -0
  42. package/tests/sandbox.test.ts +336 -62
  43. package/tests/sse-parser.test.ts +17 -16
  44. package/tests/utility-client.test.ts +129 -56
  45. package/tests/version.test.ts +16 -0
  46. package/tsdown.config.ts +12 -0
  47. package/vitest.config.ts +6 -6
  48. package/dist/chunk-BCJ7SF3Q.js +0 -117
  49. package/dist/chunk-BCJ7SF3Q.js.map +0 -1
  50. package/dist/chunk-BFVUNTP4.js +0 -104
  51. package/dist/chunk-BFVUNTP4.js.map +0 -1
  52. package/dist/chunk-EKSWCBCA.js +0 -86
  53. package/dist/chunk-EKSWCBCA.js.map +0 -1
  54. package/dist/chunk-HGF554LH.js +0 -2236
  55. package/dist/chunk-HGF554LH.js.map +0 -1
  56. package/dist/chunk-Z532A7QC.js +0 -78
  57. package/dist/chunk-Z532A7QC.js.map +0 -1
  58. package/dist/file-stream.d.ts +0 -43
  59. package/dist/file-stream.js +0 -9
  60. package/dist/file-stream.js.map +0 -1
  61. package/dist/interpreter.d.ts +0 -33
  62. package/dist/interpreter.js +0 -8
  63. package/dist/interpreter.js.map +0 -1
  64. package/dist/request-handler.d.ts +0 -18
  65. package/dist/request-handler.js +0 -12
  66. package/dist/request-handler.js.map +0 -1
  67. package/dist/sandbox-D9K2ypln.d.ts +0 -583
  68. package/dist/sandbox.d.ts +0 -4
  69. package/dist/sandbox.js +0 -12
  70. package/dist/sandbox.js.map +0 -1
  71. package/dist/security.d.ts +0 -31
  72. package/dist/security.js +0 -13
  73. package/dist/security.js.map +0 -1
  74. package/dist/sse-parser.d.ts +0 -28
  75. package/dist/sse-parser.js +0 -11
  76. 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(new Response(
51
- JSON.stringify(mockResponse),
52
- { status: 200 }
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(new Response(
73
- JSON.stringify(mockResponse),
74
- { status: 200 }
75
- ));
72
+ mockFetch.mockResolvedValue(
73
+ new Response(JSON.stringify(mockResponse), { status: 200 })
74
+ );
76
75
 
77
- const result = await client.mkdir('/app/deep/nested/directory', 'session-mkdir', { recursive: true });
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(new Response(
92
- JSON.stringify(errorResponse),
93
- { status: 403 }
94
- ));
94
+ mockFetch.mockResolvedValue(
95
+ new Response(JSON.stringify(errorResponse), { status: 403 })
96
+ );
95
97
 
96
- await expect(client.mkdir('/root/secure', 'session-mkdir'))
97
- .rejects.toThrow(PermissionDeniedError);
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(new Response(
108
- JSON.stringify(errorResponse),
109
- { status: 409 }
110
- ));
110
+ mockFetch.mockResolvedValue(
111
+ new Response(JSON.stringify(errorResponse), { status: 409 })
112
+ );
111
113
 
112
- await expect(client.mkdir('/app/existing', 'session-mkdir'))
113
- .rejects.toThrow(FileExistsError);
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(new Response(
127
- JSON.stringify(mockResponse),
128
- { status: 200 }
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('/app/config.json', content, 'session-write');
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(new Response(
148
- JSON.stringify(mockResponse),
149
- { status: 200 }
150
- ));
153
+ mockFetch.mockResolvedValue(
154
+ new Response(JSON.stringify(mockResponse), { status: 200 })
155
+ );
151
156
 
152
- const binaryData = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jYlkKQAAAABJRU5ErkJggg==';
153
- const result = await client.writeFile('/app/image.png', binaryData, 'session-write', { encoding: 'base64' });
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(new Response(
167
- JSON.stringify(errorResponse),
168
- { status: 403 }
169
- ));
177
+ mockFetch.mockResolvedValue(
178
+ new Response(JSON.stringify(errorResponse), { status: 403 })
179
+ );
170
180
 
171
- await expect(client.writeFile('/system/readonly.txt', 'content', 'session-err'))
172
- .rejects.toThrow(PermissionDeniedError);
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(new Response(
183
- JSON.stringify(errorResponse),
184
- { status: 507 }
185
- ));
193
+ mockFetch.mockResolvedValue(
194
+ new Response(JSON.stringify(errorResponse), { status: 507 })
195
+ );
186
196
 
187
- await expect(client.writeFile('/app/largefile.dat', 'x'.repeat(1000000), 'session-err'))
188
- .rejects.toThrow(FileSystemError);
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(new Response(
214
- JSON.stringify(mockResponse),
215
- { status: 200 }
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 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jYlkKQAAAABJRU5ErkJggg==';
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(new Response(
246
- JSON.stringify(mockResponse),
247
- { status: 200 }
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', { encoding: 'base64' });
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(new Response(
269
- JSON.stringify(errorResponse),
270
- { status: 404 }
271
- ));
284
+ mockFetch.mockResolvedValue(
285
+ new Response(JSON.stringify(errorResponse), { status: 404 })
286
+ );
272
287
 
273
- await expect(client.readFile('/app/missing.txt', 'session-read'))
274
- .rejects.toThrow(FileNotFoundError);
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(new Response(
285
- JSON.stringify(errorResponse),
286
- { status: 400 }
287
- ));
300
+ mockFetch.mockResolvedValue(
301
+ new Response(JSON.stringify(errorResponse), { status: 400 })
302
+ );
288
303
 
289
- await expect(client.readFile('/app/logs', 'session-read'))
290
- .rejects.toThrow(FileSystemError);
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(new TextEncoder().encode('data: {"type":"metadata","mimeType":"text/plain","size":100,"isBinary":false,"encoding":"utf-8"}\n\n'));
299
- controller.enqueue(new TextEncoder().encode('data: {"type":"chunk","data":"Hello"}\n\n'));
300
- controller.enqueue(new TextEncoder().encode('data: {"type":"complete","bytesRead":5}\n\n'));
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(new Response(mockStream, {
306
- status: 200,
307
- headers: { 'Content-Type': 'text/event-stream' }
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('/app/test.txt', 'session-stream');
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(new TextEncoder().encode('data: {"type":"metadata","mimeType":"image/png","size":1024,"isBinary":true,"encoding":"base64"}\n\n'));
329
- controller.enqueue(new TextEncoder().encode('data: {"type":"chunk","data":"iVBORw0K"}\n\n'));
330
- controller.enqueue(new TextEncoder().encode('data: {"type":"complete","bytesRead":1024}\n\n'));
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(new Response(mockStream, {
336
- status: 200,
337
- headers: { 'Content-Type': 'text/event-stream' }
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('/app/image.png', 'session-stream');
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(new Response(
353
- JSON.stringify(errorResponse),
354
- { status: 404 }
355
- ));
402
+ mockFetch.mockResolvedValue(
403
+ new Response(JSON.stringify(errorResponse), { status: 404 })
404
+ );
356
405
 
357
- await expect(client.readFileStream('/app/missing.txt', 'session-stream'))
358
- .rejects.toThrow(FileNotFoundError);
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(client.readFileStream('/app/file.txt', 'session-stream'))
365
- .rejects.toThrow('Network timeout');
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(new Response(
379
- JSON.stringify(mockResponse),
380
- { status: 200 }
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(new Response(
398
- JSON.stringify(errorResponse),
399
- { status: 404 }
400
- ));
447
+ mockFetch.mockResolvedValue(
448
+ new Response(JSON.stringify(errorResponse), { status: 404 })
449
+ );
401
450
 
402
- await expect(client.deleteFile('/app/nonexistent.txt', 'session-delete'))
403
- .rejects.toThrow(FileNotFoundError);
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(new Response(
414
- JSON.stringify(errorResponse),
415
- { status: 403 }
416
- ));
463
+ mockFetch.mockResolvedValue(
464
+ new Response(JSON.stringify(errorResponse), { status: 403 })
465
+ );
417
466
 
418
- await expect(client.deleteFile('/system/important.conf', 'session-delete'))
419
- .rejects.toThrow(PermissionDeniedError);
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(new Response(
434
- JSON.stringify(mockResponse),
435
- { status: 200 }
436
- ));
483
+ mockFetch.mockResolvedValue(
484
+ new Response(JSON.stringify(mockResponse), { status: 200 })
485
+ );
437
486
 
438
- const result = await client.renameFile('/app/old-name.txt', '/app/new-name.txt', 'session-rename');
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(new Response(
454
- JSON.stringify(errorResponse),
455
- { status: 409 }
456
- ));
506
+ mockFetch.mockResolvedValue(
507
+ new Response(JSON.stringify(errorResponse), { status: 409 })
508
+ );
457
509
 
458
- await expect(client.renameFile('/app/source.txt', '/app/existing.txt', 'session-rename'))
459
- .rejects.toThrow(FileExistsError);
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(new Response(
474
- JSON.stringify(mockResponse),
475
- { status: 200 }
476
- ));
530
+ mockFetch.mockResolvedValue(
531
+ new Response(JSON.stringify(mockResponse), { status: 200 })
532
+ );
477
533
 
478
- const result = await client.moveFile('/src/document.pdf', '/dest/document.pdf', 'session-move');
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(new Response(
494
- JSON.stringify(errorResponse),
495
- { status: 404 }
496
- ));
553
+ mockFetch.mockResolvedValue(
554
+ new Response(JSON.stringify(errorResponse), { status: 404 })
555
+ );
497
556
 
498
- await expect(client.moveFile('/app/file.txt', '/nonexistent/file.txt', 'session-move'))
499
- .rejects.toThrow(FileSystemError);
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(new Response(JSON.stringify(mockResponse), { status: 200 }));
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(new Response(JSON.stringify(mockResponse), { status: 200 }));
614
+ mockFetch.mockResolvedValue(
615
+ new Response(JSON.stringify(mockResponse), { status: 200 })
616
+ );
549
617
 
550
- await client.listFiles('/workspace', 'session-list', { recursive: true, includeHidden: true });
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(new Response(
566
- JSON.stringify({ success: true, path: '/empty', files: [], count: 0, timestamp: '2023-01-01T00:00:00Z' }),
567
- { status: 200 }
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(new Response(
578
- JSON.stringify({ error: 'Directory not found', code: 'FILE_NOT_FOUND' }),
579
- { status: 404 }
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 expect(client.listFiles('/nonexistent', 'session-list'))
583
- .rejects.toThrow(FileNotFoundError);
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(client.readFile('/app/file.txt', 'session-read'))
592
- .rejects.toThrow('Network connection failed');
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(new Response(
597
- 'invalid json {',
598
- { status: 200, headers: { 'Content-Type': 'application/json' } }
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(client.writeFile('/app/file.txt', 'content', 'session-err'))
602
- .rejects.toThrow(SandboxError);
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
- { status: 403, code: 'PERMISSION_DENIED', error: PermissionDeniedError },
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(new Response(
616
- JSON.stringify({
617
- error: 'Test error',
618
- code: scenario.code
619
- }),
620
- { status: scenario.status }
621
- ));
622
-
623
- await expect(client.readFile('/app/test.txt', 'session-read'))
624
- .rejects.toThrow(scenario.error);
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
+ });