| |
| |
| |
| |
| |
|
|
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; |
| import { LinuxSandboxManager } from './LinuxSandboxManager.js'; |
| import fs from 'node:fs'; |
| import path from 'node:path'; |
|
|
| vi.mock('node:fs', async () => { |
| const actual = await vi.importActual<typeof import('node:fs')>('node:fs'); |
| return { |
| ...actual, |
| default: { |
| |
| ...actual.default, |
| existsSync: vi.fn(() => true), |
| realpathSync: vi.fn((p) => p.toString()), |
| statSync: vi.fn(() => ({ isDirectory: () => true }) as fs.Stats), |
| mkdirSync: vi.fn(), |
| mkdtempSync: vi.fn((prefix: string) => prefix + 'mocked'), |
| openSync: vi.fn(), |
| closeSync: vi.fn(), |
| writeFileSync: vi.fn(), |
| readdirSync: vi.fn(() => []), |
| chmodSync: vi.fn(), |
| unlinkSync: vi.fn(), |
| rmSync: vi.fn(), |
| }, |
| existsSync: vi.fn(() => true), |
| realpathSync: vi.fn((p) => p.toString()), |
| statSync: vi.fn(() => ({ isDirectory: () => true }) as fs.Stats), |
| mkdirSync: vi.fn(), |
| mkdtempSync: vi.fn((prefix: string) => prefix + 'mocked'), |
| openSync: vi.fn(), |
| closeSync: vi.fn(), |
| writeFileSync: vi.fn(), |
| readdirSync: vi.fn(() => []), |
| chmodSync: vi.fn(), |
| unlinkSync: vi.fn(), |
| rmSync: vi.fn(), |
| }; |
| }); |
|
|
| vi.mock('../../utils/shell-utils.js', async (importOriginal) => { |
| const actual = |
| await importOriginal<typeof import('../../utils/shell-utils.js')>(); |
| return { |
| ...actual, |
| spawnAsync: vi.fn(() => |
| Promise.resolve({ status: 0, stdout: Buffer.from('') }), |
| ), |
| initializeShellParsers: vi.fn(), |
| isStrictlyApproved: vi.fn().mockResolvedValue(true), |
| }; |
| }); |
|
|
| describe('LinuxSandboxManager', () => { |
| const workspace = '/home/user/workspace'; |
| let manager: LinuxSandboxManager; |
|
|
| beforeEach(() => { |
| vi.clearAllMocks(); |
| vi.mocked(fs.existsSync).mockReturnValue(true); |
| vi.mocked(fs.realpathSync).mockImplementation((p) => p.toString()); |
| manager = new LinuxSandboxManager({ workspace }); |
| }); |
|
|
| afterEach(() => { |
| vi.restoreAllMocks(); |
| }); |
|
|
| describe('prepareCommand', () => { |
| it('wraps the command and arguments correctly using a temporary file', async () => { |
| const result = await manager.prepareCommand({ |
| command: 'ls', |
| args: ['-la'], |
| cwd: workspace, |
| env: { PATH: '/usr/bin' }, |
| }); |
|
|
| expect(result.program).toBe('sh'); |
| expect(result.args[0]).toBe('-c'); |
| expect(result.args[1]).toContain( |
| 'exec bwrap --args 8 "$@" 8< "$args_path" 9< "$bpf_path"', |
| ); |
| expect(result.args[result.args.length - 3]).toBe('--'); |
| expect(result.args[result.args.length - 2]).toBe('ls'); |
| expect(result.args[result.args.length - 1]).toBe('-la'); |
| expect(result.env['PATH']).toBe('/usr/bin'); |
| }); |
|
|
| it('cleans up the temporary arguments file', async () => { |
| const result = await manager.prepareCommand({ |
| command: 'ls', |
| args: [], |
| cwd: workspace, |
| env: {}, |
| }); |
|
|
| expect(result.cleanup).toBeDefined(); |
| result.cleanup!(); |
|
|
| expect(fs.unlinkSync).toHaveBeenCalled(); |
| const unlinkCall = vi.mocked(fs.unlinkSync).mock.calls[0]; |
| expect(unlinkCall[0]).toMatch(/gemini-cli-bwrap-args-.*\.args$/); |
| }); |
|
|
| it('translates virtual commands', async () => { |
| const readResult = await manager.prepareCommand({ |
| command: '__read', |
| args: [path.join(workspace, 'file.txt')], |
| cwd: workspace, |
| env: {}, |
| }); |
| |
| expect(readResult.args[readResult.args.length - 2]).toBe('/bin/cat'); |
|
|
| const writeResult = await manager.prepareCommand({ |
| command: '__write', |
| args: [path.join(workspace, 'file.txt')], |
| cwd: workspace, |
| env: {}, |
| }); |
| |
| expect(writeResult.args[writeResult.args.length - 5]).toBe('/bin/sh'); |
| expect(writeResult.args[writeResult.args.length - 1]).toBe( |
| path.join(workspace, 'file.txt'), |
| ); |
| }); |
|
|
| it('allows virtual commands targeting includeDirectories', async () => { |
| const includeDir = '/opt/tools'; |
| const testFile = path.join(includeDir, 'tool.sh'); |
| const customManager = new LinuxSandboxManager({ |
| workspace, |
| includeDirectories: [includeDir], |
| }); |
|
|
| const result = await customManager.prepareCommand({ |
| command: '__read', |
| args: [testFile], |
| cwd: workspace, |
| env: {}, |
| }); |
|
|
| expect(result.args[result.args.length - 2]).toBe('/bin/cat'); |
| expect(result.args[result.args.length - 1]).toBe(testFile); |
| }); |
|
|
| it('rejects overrides in plan mode', async () => { |
| const customManager = new LinuxSandboxManager({ |
| workspace, |
| modeConfig: { allowOverrides: false }, |
| }); |
| await expect( |
| customManager.prepareCommand({ |
| command: 'ls', |
| args: [], |
| cwd: workspace, |
| env: {}, |
| policy: { networkAccess: true }, |
| }), |
| ).rejects.toThrow(/Cannot override/); |
| }); |
| }); |
| }); |
|
|