@cloudflare/sandbox 0.4.18 → 0.5.2

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