/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { SandboxedFileSystemService } from './sandboxedFileSystemService.js'; import type { SandboxManager, SandboxRequest, SandboxedCommand, } from './sandboxManager.js'; import { spawn, type ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; import type { Writable } from 'node:stream'; vi.mock('node:child_process', () => ({ spawn: vi.fn(), })); class MockSandboxManager implements SandboxManager { async prepareCommand(req: SandboxRequest): Promise { return { program: 'sandbox.exe', args: ['0', req.cwd, req.command, ...req.args], env: req.env || {}, }; } } describe('SandboxedFileSystemService', () => { let sandboxManager: MockSandboxManager; let service: SandboxedFileSystemService; const cwd = '/test/cwd'; beforeEach(() => { sandboxManager = new MockSandboxManager(); service = new SandboxedFileSystemService(sandboxManager, cwd); vi.clearAllMocks(); }); it('should read a file through the sandbox', async () => { const mockChild = new EventEmitter() as unknown as ChildProcess; Object.assign(mockChild, { stdout: new EventEmitter(), stderr: new EventEmitter(), }); vi.mocked(spawn).mockReturnValue(mockChild); const readPromise = service.readTextFile('/test/file.txt'); // Use setImmediate to ensure events are emitted after the promise starts executing setImmediate(() => { mockChild.stdout!.emit('data', Buffer.from('file content')); mockChild.emit('close', 0); }); const content = await readPromise; expect(content).toBe('file content'); expect(spawn).toHaveBeenCalledWith( 'sandbox.exe', ['0', cwd, '__read', '/test/file.txt'], expect.any(Object), ); }); it('should write a file through the sandbox', async () => { const mockChild = new EventEmitter() as unknown as ChildProcess; Object.assign(mockChild, { stdin: { write: vi.fn(), end: vi.fn(), } as unknown as Writable, stderr: new EventEmitter(), }); vi.mocked(spawn).mockReturnValue(mockChild); const writePromise = service.writeTextFile('/test/file.txt', 'new content'); setImmediate(() => { mockChild.emit('close', 0); }); await writePromise; expect(mockChild.stdin!.write).toHaveBeenCalledWith('new content'); expect(mockChild.stdin!.end).toHaveBeenCalled(); expect(spawn).toHaveBeenCalledWith( 'sandbox.exe', ['0', cwd, '__write', '/test/file.txt'], expect.any(Object), ); }); it('should reject if sandbox command fails', async () => { const mockChild = new EventEmitter() as unknown as ChildProcess; Object.assign(mockChild, { stdout: new EventEmitter(), stderr: new EventEmitter(), }); vi.mocked(spawn).mockReturnValue(mockChild); const readPromise = service.readTextFile('/test/file.txt'); setImmediate(() => { mockChild.stderr!.emit('data', Buffer.from('access denied')); mockChild.emit('close', 1); }); await expect(readPromise).rejects.toThrow( 'Sandbox Error: Command failed with exit code 1. Details: access denied', ); }); });