@cloudflare/sandbox 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +54 -59
- package/README.md +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/package.json +13 -8
- package/.turbo/turbo-build.log +0 -23
- package/CHANGELOG.md +0 -441
- package/src/clients/base-client.ts +0 -356
- package/src/clients/command-client.ts +0 -133
- package/src/clients/file-client.ts +0 -300
- package/src/clients/git-client.ts +0 -98
- package/src/clients/index.ts +0 -64
- package/src/clients/interpreter-client.ts +0 -333
- package/src/clients/port-client.ts +0 -105
- package/src/clients/process-client.ts +0 -198
- package/src/clients/sandbox-client.ts +0 -39
- package/src/clients/types.ts +0 -88
- package/src/clients/utility-client.ts +0 -156
- package/src/errors/adapter.ts +0 -238
- package/src/errors/classes.ts +0 -594
- package/src/errors/index.ts +0 -109
- package/src/file-stream.ts +0 -169
- package/src/index.ts +0 -121
- package/src/interpreter.ts +0 -168
- package/src/openai/index.ts +0 -465
- package/src/request-handler.ts +0 -184
- package/src/sandbox.ts +0 -1937
- package/src/security.ts +0 -119
- package/src/sse-parser.ts +0 -144
- package/src/storage-mount/credential-detection.ts +0 -41
- package/src/storage-mount/errors.ts +0 -51
- package/src/storage-mount/index.ts +0 -17
- package/src/storage-mount/provider-detection.ts +0 -93
- package/src/storage-mount/types.ts +0 -17
- package/src/version.ts +0 -6
- package/tests/base-client.test.ts +0 -582
- package/tests/command-client.test.ts +0 -444
- package/tests/file-client.test.ts +0 -831
- package/tests/file-stream.test.ts +0 -310
- package/tests/get-sandbox.test.ts +0 -172
- package/tests/git-client.test.ts +0 -455
- package/tests/openai-shell-editor.test.ts +0 -434
- package/tests/port-client.test.ts +0 -283
- package/tests/process-client.test.ts +0 -649
- package/tests/request-handler.test.ts +0 -292
- package/tests/sandbox.test.ts +0 -890
- package/tests/sse-parser.test.ts +0 -291
- package/tests/storage-mount/credential-detection.test.ts +0 -119
- package/tests/storage-mount/provider-detection.test.ts +0 -77
- package/tests/utility-client.test.ts +0 -339
- package/tests/version.test.ts +0 -16
- package/tests/wrangler.jsonc +0 -35
- package/tsconfig.json +0 -11
- package/tsdown.config.ts +0 -13
- package/vitest.config.ts +0 -31
|
@@ -1,649 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import type {
|
|
3
|
-
ProcessCleanupResult,
|
|
4
|
-
ProcessInfoResult,
|
|
5
|
-
ProcessKillResult,
|
|
6
|
-
ProcessListResult,
|
|
7
|
-
ProcessLogsResult,
|
|
8
|
-
ProcessStartResult
|
|
9
|
-
} from '../src/clients';
|
|
10
|
-
import { ProcessClient } from '../src/clients/process-client';
|
|
11
|
-
import {
|
|
12
|
-
CommandNotFoundError,
|
|
13
|
-
ProcessError,
|
|
14
|
-
ProcessNotFoundError,
|
|
15
|
-
SandboxError
|
|
16
|
-
} from '../src/errors';
|
|
17
|
-
|
|
18
|
-
describe('ProcessClient', () => {
|
|
19
|
-
let client: ProcessClient;
|
|
20
|
-
let mockFetch: ReturnType<typeof vi.fn>;
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
vi.clearAllMocks();
|
|
24
|
-
|
|
25
|
-
mockFetch = vi.fn();
|
|
26
|
-
global.fetch = mockFetch as unknown as typeof fetch;
|
|
27
|
-
|
|
28
|
-
client = new ProcessClient({
|
|
29
|
-
baseUrl: 'http://test.com',
|
|
30
|
-
port: 3000
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
vi.restoreAllMocks();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('process lifecycle management', () => {
|
|
39
|
-
it('should start background processes successfully', async () => {
|
|
40
|
-
const mockResponse: ProcessStartResult = {
|
|
41
|
-
success: true,
|
|
42
|
-
processId: 'proc-web-server',
|
|
43
|
-
command: 'npm run dev',
|
|
44
|
-
pid: 12345,
|
|
45
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
mockFetch.mockResolvedValue(
|
|
49
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const result = await client.startProcess('npm run dev', 'session-123');
|
|
53
|
-
|
|
54
|
-
expect(result.success).toBe(true);
|
|
55
|
-
expect(result.command).toBe('npm run dev');
|
|
56
|
-
expect(result.pid).toBe(12345);
|
|
57
|
-
expect(result.processId).toBe('proc-web-server');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should start processes with custom process IDs', async () => {
|
|
61
|
-
const mockResponse: ProcessStartResult = {
|
|
62
|
-
success: true,
|
|
63
|
-
processId: 'my-api-server',
|
|
64
|
-
command: 'python app.py',
|
|
65
|
-
pid: 54321,
|
|
66
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
mockFetch.mockResolvedValue(
|
|
70
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const result = await client.startProcess('python app.py', 'session-456', {
|
|
74
|
-
processId: 'my-api-server'
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
expect(result.success).toBe(true);
|
|
78
|
-
expect(result.processId).toBe('my-api-server');
|
|
79
|
-
expect(result.command).toBe('python app.py');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should handle long-running process startup', async () => {
|
|
83
|
-
const mockResponse: ProcessStartResult = {
|
|
84
|
-
success: true,
|
|
85
|
-
processId: 'proc-database',
|
|
86
|
-
command: 'docker run postgres',
|
|
87
|
-
pid: 99999,
|
|
88
|
-
timestamp: '2023-01-01T00:00:05Z'
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
mockFetch.mockImplementation(
|
|
92
|
-
() =>
|
|
93
|
-
new Promise((resolve) =>
|
|
94
|
-
setTimeout(
|
|
95
|
-
() =>
|
|
96
|
-
resolve(
|
|
97
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
98
|
-
),
|
|
99
|
-
100
|
|
100
|
-
)
|
|
101
|
-
)
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const result = await client.startProcess(
|
|
105
|
-
'docker run postgres',
|
|
106
|
-
'session-789'
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
expect(result.success).toBe(true);
|
|
110
|
-
expect(result.processId).toBe('proc-database');
|
|
111
|
-
expect(result.command).toBe('docker run postgres');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should handle command not found errors', async () => {
|
|
115
|
-
const errorResponse = {
|
|
116
|
-
error: 'Command not found: invalidcmd',
|
|
117
|
-
code: 'COMMAND_NOT_FOUND'
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
mockFetch.mockResolvedValue(
|
|
121
|
-
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
await expect(
|
|
125
|
-
client.startProcess('invalidcmd', 'session-err')
|
|
126
|
-
).rejects.toThrow(CommandNotFoundError);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('should handle process startup failures', async () => {
|
|
130
|
-
const errorResponse = {
|
|
131
|
-
error: 'Process failed to start: permission denied',
|
|
132
|
-
code: 'PROCESS_ERROR'
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
mockFetch.mockResolvedValue(
|
|
136
|
-
new Response(JSON.stringify(errorResponse), { status: 500 })
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
await expect(
|
|
140
|
-
client.startProcess('sudo privileged-command', 'session-err')
|
|
141
|
-
).rejects.toThrow(ProcessError);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('process monitoring and inspection', () => {
|
|
146
|
-
it('should list running processes', async () => {
|
|
147
|
-
const mockResponse: ProcessListResult = {
|
|
148
|
-
success: true,
|
|
149
|
-
processes: [
|
|
150
|
-
{
|
|
151
|
-
id: 'proc-web',
|
|
152
|
-
command: 'npm run dev',
|
|
153
|
-
status: 'running',
|
|
154
|
-
pid: 12345,
|
|
155
|
-
startTime: '2023-01-01T00:00:00Z'
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
id: 'proc-api',
|
|
159
|
-
command: 'python api.py',
|
|
160
|
-
status: 'running',
|
|
161
|
-
pid: 12346,
|
|
162
|
-
startTime: '2023-01-01T00:00:30Z'
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
id: 'proc-worker',
|
|
166
|
-
command: 'node worker.js',
|
|
167
|
-
status: 'completed',
|
|
168
|
-
pid: 12347,
|
|
169
|
-
exitCode: 0,
|
|
170
|
-
startTime: '2023-01-01T00:01:00Z',
|
|
171
|
-
endTime: '2023-01-01T00:05:00Z'
|
|
172
|
-
}
|
|
173
|
-
],
|
|
174
|
-
timestamp: '2023-01-01T00:05:30Z'
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
mockFetch.mockResolvedValue(
|
|
178
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
const result = await client.listProcesses();
|
|
182
|
-
|
|
183
|
-
expect(result.success).toBe(true);
|
|
184
|
-
expect(result.processes).toHaveLength(3);
|
|
185
|
-
|
|
186
|
-
const runningProcesses = result.processes.filter(
|
|
187
|
-
(p) => p.status === 'running'
|
|
188
|
-
);
|
|
189
|
-
expect(runningProcesses).toHaveLength(2);
|
|
190
|
-
expect(runningProcesses[0].pid).toBeDefined();
|
|
191
|
-
expect(runningProcesses[1].pid).toBeDefined();
|
|
192
|
-
|
|
193
|
-
const completedProcess = result.processes.find(
|
|
194
|
-
(p) => p.status === 'completed'
|
|
195
|
-
);
|
|
196
|
-
expect(completedProcess?.exitCode).toBe(0);
|
|
197
|
-
expect(completedProcess?.endTime).toBeDefined();
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('should get specific process details', async () => {
|
|
201
|
-
const mockResponse: ProcessInfoResult = {
|
|
202
|
-
success: true,
|
|
203
|
-
process: {
|
|
204
|
-
id: 'proc-analytics',
|
|
205
|
-
command: 'python analytics.py --batch-size=1000',
|
|
206
|
-
status: 'running',
|
|
207
|
-
pid: 98765,
|
|
208
|
-
startTime: '2023-01-01T00:00:00Z'
|
|
209
|
-
},
|
|
210
|
-
timestamp: '2023-01-01T00:10:00Z'
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
mockFetch.mockResolvedValue(
|
|
214
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
const result = await client.getProcess('proc-analytics');
|
|
218
|
-
|
|
219
|
-
expect(result.success).toBe(true);
|
|
220
|
-
expect(result.process.id).toBe('proc-analytics');
|
|
221
|
-
expect(result.process.command).toContain('--batch-size=1000');
|
|
222
|
-
expect(result.process.status).toBe('running');
|
|
223
|
-
expect(result.process.pid).toBe(98765);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('should handle process not found error', async () => {
|
|
227
|
-
const errorResponse = {
|
|
228
|
-
error: 'Process not found: nonexistent-proc',
|
|
229
|
-
code: 'PROCESS_NOT_FOUND'
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
mockFetch.mockResolvedValue(
|
|
233
|
-
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
await expect(client.getProcess('nonexistent-proc')).rejects.toThrow(
|
|
237
|
-
ProcessNotFoundError
|
|
238
|
-
);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('should handle empty process list', async () => {
|
|
242
|
-
const mockResponse: ProcessListResult = {
|
|
243
|
-
success: true,
|
|
244
|
-
processes: [],
|
|
245
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
mockFetch.mockResolvedValue(
|
|
249
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
const result = await client.listProcesses();
|
|
253
|
-
|
|
254
|
-
expect(result.success).toBe(true);
|
|
255
|
-
expect(result.processes).toHaveLength(0);
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
describe('process termination', () => {
|
|
260
|
-
it('should kill individual processes', async () => {
|
|
261
|
-
const mockResponse: ProcessKillResult = {
|
|
262
|
-
success: true,
|
|
263
|
-
processId: 'test-process',
|
|
264
|
-
timestamp: '2023-01-01T00:10:00Z'
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
mockFetch.mockResolvedValue(
|
|
268
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
const result = await client.killProcess('proc-web');
|
|
272
|
-
|
|
273
|
-
expect(result.success).toBe(true);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('should handle kill non-existent process', async () => {
|
|
277
|
-
const errorResponse = {
|
|
278
|
-
error: 'Process not found: already-dead-proc',
|
|
279
|
-
code: 'PROCESS_NOT_FOUND'
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
mockFetch.mockResolvedValue(
|
|
283
|
-
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
await expect(client.killProcess('already-dead-proc')).rejects.toThrow(
|
|
287
|
-
ProcessNotFoundError
|
|
288
|
-
);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it('should kill all processes at once', async () => {
|
|
292
|
-
const mockResponse: ProcessCleanupResult = {
|
|
293
|
-
success: true,
|
|
294
|
-
cleanedCount: 0,
|
|
295
|
-
timestamp: '2023-01-01T00:15:00Z'
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
mockFetch.mockResolvedValue(
|
|
299
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
const result = await client.killAllProcesses();
|
|
303
|
-
|
|
304
|
-
expect(result.success).toBe(true);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it('should handle kill all when no processes running', async () => {
|
|
308
|
-
const mockResponse: ProcessCleanupResult = {
|
|
309
|
-
success: true,
|
|
310
|
-
cleanedCount: 0,
|
|
311
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
mockFetch.mockResolvedValue(
|
|
315
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
const result = await client.killAllProcesses();
|
|
319
|
-
|
|
320
|
-
expect(result.success).toBe(true);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it('should handle kill failures', async () => {
|
|
324
|
-
const errorResponse = {
|
|
325
|
-
error: 'Failed to kill process: process is protected',
|
|
326
|
-
code: 'PROCESS_ERROR'
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
mockFetch.mockResolvedValue(
|
|
330
|
-
new Response(JSON.stringify(errorResponse), { status: 500 })
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
await expect(client.killProcess('protected-proc')).rejects.toThrow(
|
|
334
|
-
ProcessError
|
|
335
|
-
);
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
describe('process log management', () => {
|
|
340
|
-
it('should retrieve process logs', async () => {
|
|
341
|
-
const mockResponse: ProcessLogsResult = {
|
|
342
|
-
success: true,
|
|
343
|
-
processId: 'proc-server',
|
|
344
|
-
stdout: `Server starting...
|
|
345
|
-
✓ Database connected
|
|
346
|
-
✓ Routes loaded
|
|
347
|
-
✓ Server listening on port 3000
|
|
348
|
-
[INFO] Request: GET /api/health
|
|
349
|
-
[INFO] Response: 200 OK`,
|
|
350
|
-
stderr: `[WARN] Deprecated function used in auth.js:45
|
|
351
|
-
[WARN] High memory usage: 85%`,
|
|
352
|
-
timestamp: '2023-01-01T00:10:00Z'
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
mockFetch.mockResolvedValue(
|
|
356
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
const result = await client.getProcessLogs('proc-server');
|
|
360
|
-
|
|
361
|
-
expect(result.success).toBe(true);
|
|
362
|
-
expect(result.processId).toBe('proc-server');
|
|
363
|
-
expect(result.stdout).toContain('Server listening on port 3000');
|
|
364
|
-
expect(result.stdout).toContain('Request: GET /api/health');
|
|
365
|
-
expect(result.stderr).toContain('Deprecated function used');
|
|
366
|
-
expect(result.stderr).toContain('High memory usage');
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it('should handle logs for non-existent process', async () => {
|
|
370
|
-
const errorResponse = {
|
|
371
|
-
error: 'Process not found: missing-proc',
|
|
372
|
-
code: 'PROCESS_NOT_FOUND'
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
mockFetch.mockResolvedValue(
|
|
376
|
-
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
await expect(client.getProcessLogs('missing-proc')).rejects.toThrow(
|
|
380
|
-
ProcessNotFoundError
|
|
381
|
-
);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it('should retrieve logs for processes with large output', async () => {
|
|
385
|
-
const largeStdout = 'Log entry with details\n'.repeat(10000);
|
|
386
|
-
const largeStderr = 'Error trace line\n'.repeat(1000);
|
|
387
|
-
|
|
388
|
-
const mockResponse: ProcessLogsResult = {
|
|
389
|
-
success: true,
|
|
390
|
-
processId: 'proc-batch',
|
|
391
|
-
stdout: largeStdout,
|
|
392
|
-
stderr: largeStderr,
|
|
393
|
-
timestamp: '2023-01-01T00:30:00Z'
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
mockFetch.mockResolvedValue(
|
|
397
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
const result = await client.getProcessLogs('proc-batch');
|
|
401
|
-
|
|
402
|
-
expect(result.success).toBe(true);
|
|
403
|
-
expect(result.stdout.length).toBeGreaterThan(200000);
|
|
404
|
-
expect(result.stderr.length).toBeGreaterThan(15000);
|
|
405
|
-
expect(result.stdout.split('\n')).toHaveLength(10001);
|
|
406
|
-
expect(result.stderr.split('\n')).toHaveLength(1001);
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
it('should handle empty process logs', async () => {
|
|
410
|
-
const mockResponse: ProcessLogsResult = {
|
|
411
|
-
success: true,
|
|
412
|
-
processId: 'proc-silent',
|
|
413
|
-
stdout: '',
|
|
414
|
-
stderr: '',
|
|
415
|
-
timestamp: '2023-01-01T00:05:00Z'
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
mockFetch.mockResolvedValue(
|
|
419
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
const result = await client.getProcessLogs('proc-silent');
|
|
423
|
-
|
|
424
|
-
expect(result.success).toBe(true);
|
|
425
|
-
expect(result.stdout).toBe('');
|
|
426
|
-
expect(result.stderr).toBe('');
|
|
427
|
-
expect(result.processId).toBe('proc-silent');
|
|
428
|
-
});
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
describe('log streaming', () => {
|
|
432
|
-
it('should stream process logs in real-time', async () => {
|
|
433
|
-
const logData = `data: {"type":"stdout","data":"Server starting...\\n","timestamp":"2023-01-01T00:00:01Z"}
|
|
434
|
-
|
|
435
|
-
data: {"type":"stdout","data":"Database connected\\n","timestamp":"2023-01-01T00:00:02Z"}
|
|
436
|
-
|
|
437
|
-
data: {"type":"stderr","data":"Warning: deprecated API\\n","timestamp":"2023-01-01T00:00:03Z"}
|
|
438
|
-
|
|
439
|
-
data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-01-01T00:00:04Z"}
|
|
440
|
-
|
|
441
|
-
`;
|
|
442
|
-
|
|
443
|
-
const mockStream = new ReadableStream({
|
|
444
|
-
start(controller) {
|
|
445
|
-
controller.enqueue(new TextEncoder().encode(logData));
|
|
446
|
-
controller.close();
|
|
447
|
-
}
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
mockFetch.mockResolvedValue(
|
|
451
|
-
new Response(mockStream, {
|
|
452
|
-
status: 200,
|
|
453
|
-
headers: { 'Content-Type': 'text/event-stream' }
|
|
454
|
-
})
|
|
455
|
-
);
|
|
456
|
-
|
|
457
|
-
const stream = await client.streamProcessLogs('proc-realtime');
|
|
458
|
-
|
|
459
|
-
expect(stream).toBeInstanceOf(ReadableStream);
|
|
460
|
-
|
|
461
|
-
const reader = stream.getReader();
|
|
462
|
-
const decoder = new TextDecoder();
|
|
463
|
-
let content = '';
|
|
464
|
-
|
|
465
|
-
try {
|
|
466
|
-
while (true) {
|
|
467
|
-
const { done, value } = await reader.read();
|
|
468
|
-
if (done) break;
|
|
469
|
-
content += decoder.decode(value);
|
|
470
|
-
}
|
|
471
|
-
} finally {
|
|
472
|
-
reader.releaseLock();
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
expect(content).toContain('Server starting');
|
|
476
|
-
expect(content).toContain('Database connected');
|
|
477
|
-
expect(content).toContain('Warning: deprecated API');
|
|
478
|
-
expect(content).toContain('Server ready on port 3000');
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
it('should handle streaming for non-existent process', async () => {
|
|
482
|
-
const errorResponse = {
|
|
483
|
-
error: 'Process not found: stream-missing',
|
|
484
|
-
code: 'PROCESS_NOT_FOUND'
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
mockFetch.mockResolvedValue(
|
|
488
|
-
new Response(JSON.stringify(errorResponse), { status: 404 })
|
|
489
|
-
);
|
|
490
|
-
|
|
491
|
-
await expect(client.streamProcessLogs('stream-missing')).rejects.toThrow(
|
|
492
|
-
ProcessNotFoundError
|
|
493
|
-
);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
it('should handle streaming setup failures', async () => {
|
|
497
|
-
const errorResponse = {
|
|
498
|
-
error: 'Failed to setup log stream: process not outputting logs',
|
|
499
|
-
code: 'PROCESS_ERROR'
|
|
500
|
-
};
|
|
501
|
-
|
|
502
|
-
mockFetch.mockResolvedValue(
|
|
503
|
-
new Response(JSON.stringify(errorResponse), { status: 500 })
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
await expect(client.streamProcessLogs('proc-no-logs')).rejects.toThrow(
|
|
507
|
-
ProcessError
|
|
508
|
-
);
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
it('should handle missing stream body', async () => {
|
|
512
|
-
mockFetch.mockResolvedValue(
|
|
513
|
-
new Response(null, {
|
|
514
|
-
status: 200,
|
|
515
|
-
headers: { 'Content-Type': 'text/event-stream' }
|
|
516
|
-
})
|
|
517
|
-
);
|
|
518
|
-
|
|
519
|
-
await expect(
|
|
520
|
-
client.streamProcessLogs('proc-empty-stream')
|
|
521
|
-
).rejects.toThrow('No response body for streaming');
|
|
522
|
-
});
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
describe('session integration', () => {
|
|
526
|
-
it('should include session in process operations', async () => {
|
|
527
|
-
const mockResponse: ProcessStartResult = {
|
|
528
|
-
success: true,
|
|
529
|
-
processId: 'proc-session-test',
|
|
530
|
-
command: 'echo session-test',
|
|
531
|
-
pid: 11111,
|
|
532
|
-
timestamp: '2023-01-01T00:00:00Z'
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
mockFetch.mockResolvedValue(
|
|
536
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
537
|
-
);
|
|
538
|
-
|
|
539
|
-
const result = await client.startProcess(
|
|
540
|
-
'echo session-test',
|
|
541
|
-
'session-test'
|
|
542
|
-
);
|
|
543
|
-
|
|
544
|
-
expect(result.success).toBe(true);
|
|
545
|
-
|
|
546
|
-
const [url, options] = mockFetch.mock.calls[0];
|
|
547
|
-
const requestBody = JSON.parse(options.body);
|
|
548
|
-
expect(requestBody.sessionId).toBe('session-test');
|
|
549
|
-
expect(requestBody.command).toBe('echo session-test');
|
|
550
|
-
});
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
describe('concurrent process operations', () => {
|
|
554
|
-
it('should handle multiple simultaneous process operations', async () => {
|
|
555
|
-
mockFetch.mockImplementation((url: string, options: RequestInit) => {
|
|
556
|
-
if (url.includes('/start')) {
|
|
557
|
-
return Promise.resolve(
|
|
558
|
-
new Response(
|
|
559
|
-
JSON.stringify({
|
|
560
|
-
success: true,
|
|
561
|
-
process: {
|
|
562
|
-
id: `proc-${Date.now()}`,
|
|
563
|
-
command: JSON.parse(options.body as string).command,
|
|
564
|
-
status: 'running',
|
|
565
|
-
pid: Math.floor(Math.random() * 90000) + 10000,
|
|
566
|
-
startTime: new Date().toISOString()
|
|
567
|
-
},
|
|
568
|
-
timestamp: new Date().toISOString()
|
|
569
|
-
})
|
|
570
|
-
)
|
|
571
|
-
);
|
|
572
|
-
} else if (url.includes('/list')) {
|
|
573
|
-
return Promise.resolve(
|
|
574
|
-
new Response(
|
|
575
|
-
JSON.stringify({
|
|
576
|
-
success: true,
|
|
577
|
-
processes: [],
|
|
578
|
-
timestamp: new Date().toISOString()
|
|
579
|
-
})
|
|
580
|
-
)
|
|
581
|
-
);
|
|
582
|
-
} else if (url.includes('/logs')) {
|
|
583
|
-
return Promise.resolve(
|
|
584
|
-
new Response(
|
|
585
|
-
JSON.stringify({
|
|
586
|
-
success: true,
|
|
587
|
-
processId: url.split('/')[4],
|
|
588
|
-
stdout: 'log output',
|
|
589
|
-
stderr: '',
|
|
590
|
-
timestamp: new Date().toISOString()
|
|
591
|
-
})
|
|
592
|
-
)
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
return Promise.resolve(new Response('{}', { status: 200 }));
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
const operations = await Promise.all([
|
|
599
|
-
client.startProcess('npm run dev', 'session-concurrent'),
|
|
600
|
-
client.startProcess('python api.py', 'session-concurrent'),
|
|
601
|
-
client.listProcesses(),
|
|
602
|
-
client.getProcessLogs('existing-proc'),
|
|
603
|
-
client.startProcess('node worker.js', 'session-concurrent')
|
|
604
|
-
]);
|
|
605
|
-
|
|
606
|
-
expect(operations).toHaveLength(5);
|
|
607
|
-
operations.forEach((result) => {
|
|
608
|
-
expect(result.success).toBe(true);
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
expect(mockFetch).toHaveBeenCalledTimes(5);
|
|
612
|
-
});
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
describe('error handling', () => {
|
|
616
|
-
it('should handle network failures gracefully', async () => {
|
|
617
|
-
mockFetch.mockRejectedValue(new Error('Network connection failed'));
|
|
618
|
-
|
|
619
|
-
await expect(client.listProcesses()).rejects.toThrow(
|
|
620
|
-
'Network connection failed'
|
|
621
|
-
);
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
it('should handle malformed server responses', async () => {
|
|
625
|
-
mockFetch.mockResolvedValue(
|
|
626
|
-
new Response('invalid json {', { status: 200 })
|
|
627
|
-
);
|
|
628
|
-
|
|
629
|
-
await expect(
|
|
630
|
-
client.startProcess('test-command', 'session-err')
|
|
631
|
-
).rejects.toThrow(SandboxError);
|
|
632
|
-
});
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
describe('constructor options', () => {
|
|
636
|
-
it('should initialize with minimal options', () => {
|
|
637
|
-
const minimalClient = new ProcessClient();
|
|
638
|
-
expect(minimalClient).toBeDefined();
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
it('should initialize with full options', () => {
|
|
642
|
-
const fullOptionsClient = new ProcessClient({
|
|
643
|
-
baseUrl: 'http://custom.com',
|
|
644
|
-
port: 8080
|
|
645
|
-
});
|
|
646
|
-
expect(fullOptionsClient).toBeDefined();
|
|
647
|
-
});
|
|
648
|
-
});
|
|
649
|
-
});
|