/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createSandboxManager } from './sandboxManagerFactory.js';
import { ShellExecutionService } from './shellExecutionService.js';
import { getSecureSanitizationConfig } from './environmentSanitization.js';
import { NoopSandboxManager, LocalSandboxManager, } from './sandboxManager.js';
import { execFile, execSync } from 'node:child_process';
import { promisify } from 'node:util';
import os from 'node:os';
import fs from 'node:fs';
import path from 'node:path';
import http from 'node:http';
/**
 * Abstracts platform-specific shell commands for integration testing.
 */
const Platform = {
    isWindows: os.platform() === 'win32',
    /** Returns a command to create an empty file. */
    touch(filePath) {
        return this.isWindows
            ? {
                command: 'powershell.exe',
                args: [
                    '-NoProfile',
                    '-Command',
                    `New-Item -Path "${filePath}" -ItemType File -Force`,
                ],
            }
            : { command: 'touch', args: [filePath] };
    },
    /** Returns a command to read a file's content. */
    cat(filePath) {
        return this.isWindows
            ? { command: 'cmd.exe', args: ['/c', `type "${filePath}"`] }
            : { command: 'cat', args: [filePath] };
    },
    /** Returns a command to echo a string. */
    echo(text) {
        return this.isWindows
            ? { command: 'cmd.exe', args: ['/c', `echo ${text}`] }
            : { command: 'echo', args: [text] };
    },
    /** Returns a command to perform a network request. */
    curl(url) {
        return { command: 'curl', args: ['-s', '--connect-timeout', '1', url] };
    },
    /** Returns a command that checks if the current terminal is interactive. */
    isPty() {
        return this.isWindows
            ? 'powershell.exe -NoProfile -Command "echo True"'
            : 'bash -c "if [ -t 1 ]; then echo True; else echo False; fi"';
    },
    /** Returns a path that is strictly outside the workspace and likely blocked. */
    getExternalBlockedPath() {
        return this.isWindows
            ? 'C:\\Windows\\System32\\drivers\\etc\\hosts'
            : '/Users/Shared/.gemini_test_blocked';
    },
};
async function runCommand(command) {
    try {
        const { stdout, stderr } = await promisify(execFile)(command.program, command.args, {
            cwd: command.cwd,
            env: command.env,
            encoding: 'utf-8',
        });
        return { status: 0, stdout, stderr };
    }
    catch (error) {
        const err = error;
        return {
            status: err.code ?? 1,
            stdout: err.stdout ?? '',
            stderr: err.stderr ?? '',
        };
    }
}
/**
 * Determines if the system has the necessary binaries to run the sandbox.
 * Throws an error if a supported platform is missing its required tools.
 */
function ensureSandboxAvailable() {
    const platform = os.platform();
    if (platform === 'win32') {
        // Windows sandboxing relies on icacls, which is a core system utility and
        // always available.
        // TODO: reenable once flakiness is addressed
        return false;
    }
    if (platform === 'darwin') {
        if (fs.existsSync('/usr/bin/sandbox-exec')) {
            try {
                execSync('sandbox-exec -p "(version 1)(allow default)" echo test', {
                    stdio: 'ignore',
                });
                return true;
            }
            catch {
                // eslint-disable-next-line no-console
                console.warn('sandbox-exec is present but cannot be used (likely running inside a sandbox already). Skipping sandbox tests.');
                return false;
            }
        }
        throw new Error('Sandboxing tests on macOS require /usr/bin/sandbox-exec to be present.');
    }
    if (platform === 'linux') {
        try {
            execSync('which bwrap', { stdio: 'ignore' });
            return true;
        }
        catch {
            throw new Error('Sandboxing tests on Linux require bubblewrap (bwrap) to be installed.');
        }
    }
    return false;
}
describe('SandboxManager Integration', () => {
    const workspace = process.cwd();
    const manager = createSandboxManager({ enabled: true }, { workspace });
    // Skip if we are on an unsupported platform or if it's a NoopSandboxManager
    const shouldSkip = manager instanceof NoopSandboxManager ||
        manager instanceof LocalSandboxManager ||
        !ensureSandboxAvailable();
    describe.skipIf(shouldSkip)('Cross-platform Sandbox Behavior', () => {
        describe('Basic Execution', () => {
            it('executes commands within the workspace', async () => {
                const { command, args } = Platform.echo('sandbox test');
                const sandboxed = await manager.prepareCommand({
                    command,
                    args,
                    cwd: workspace,
                    env: process.env,
                });
                const result = await runCommand(sandboxed);
                expect(result.status).toBe(0);
                expect(result.stdout.trim()).toBe('sandbox test');
            });
            // The Windows sandbox wrapper (GeminiSandbox.exe) uses standard pipes
            // for I/O interception, which breaks ConPTY pseudo-terminal inheritance.
            it.skipIf(Platform.isWindows)('supports interactive pseudo-terminals (node-pty)', async () => {
                const handle = await ShellExecutionService.execute(Platform.isPty(), workspace, () => { }, new AbortController().signal, true, {
                    sanitizationConfig: getSecureSanitizationConfig(),
                    sandboxManager: manager,
                });
                const result = await handle.result;
                expect(result.exitCode).toBe(0);
                expect(result.output).toContain('True');
            });
        });
        describe('File System Access', () => {
            it('blocks access outside the workspace', async () => {
                const blockedPath = Platform.getExternalBlockedPath();
                const { command, args } = Platform.touch(blockedPath);
                const sandboxed = await manager.prepareCommand({
                    command,
                    args,
                    cwd: workspace,
                    env: process.env,
                });
                const result = await runCommand(sandboxed);
                expect(result.status).not.toBe(0);
            });
            it('allows dynamic expansion of permissions after a failure', async () => {
                const tempDir = fs.mkdtempSync(path.join(workspace, '..', 'expansion-'));
                const testFile = path.join(tempDir, 'test.txt');
                try {
                    const { command, args } = Platform.touch(testFile);
                    // First attempt: fails due to sandbox restrictions
                    const sandboxed1 = await manager.prepareCommand({
                        command,
                        args,
                        cwd: workspace,
                        env: process.env,
                    });
                    const result1 = await runCommand(sandboxed1);
                    expect(result1.status).not.toBe(0);
                    expect(fs.existsSync(testFile)).toBe(false);
                    // Second attempt: succeeds with additional permissions
                    const sandboxed2 = await manager.prepareCommand({
                        command,
                        args,
                        cwd: workspace,
                        env: process.env,
                        policy: { allowedPaths: [tempDir] },
                    });
                    const result2 = await runCommand(sandboxed2);
                    expect(result2.status).toBe(0);
                    expect(fs.existsSync(testFile)).toBe(true);
                }
                finally {
                    if (fs.existsSync(testFile))
                        fs.unlinkSync(testFile);
                    fs.rmSync(tempDir, { recursive: true, force: true });
                }
            });
            it('grants access to explicitly allowed paths', async () => {
                const allowedDir = fs.mkdtempSync(path.join(workspace, '..', 'allowed-'));
                const testFile = path.join(allowedDir, 'test.txt');
                try {
                    const { command, args } = Platform.touch(testFile);
                    const sandboxed = await manager.prepareCommand({
                        command,
                        args,
                        cwd: workspace,
                        env: process.env,
                        policy: { allowedPaths: [allowedDir] },
                    });
                    const result = await runCommand(sandboxed);
                    expect(result.status).toBe(0);
                    expect(fs.existsSync(testFile)).toBe(true);
                }
                finally {
                    if (fs.existsSync(testFile))
                        fs.unlinkSync(testFile);
                    fs.rmSync(allowedDir, { recursive: true, force: true });
                }
            });
            it('blocks access to forbidden paths within the workspace', async () => {
                const tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-'));
                const forbiddenDir = path.join(tempWorkspace, 'forbidden');
                const testFile = path.join(forbiddenDir, 'test.txt');
                fs.mkdirSync(forbiddenDir);
                try {
                    const osManager = createSandboxManager({ enabled: true }, {
                        workspace: tempWorkspace,
                        forbiddenPaths: async () => [forbiddenDir],
                    });
                    const { command, args } = Platform.touch(testFile);
                    const sandboxed = await osManager.prepareCommand({
                        command,
                        args,
                        cwd: tempWorkspace,
                        env: process.env,
                    });
                    const result = await runCommand(sandboxed);
                    expect(result.status).not.toBe(0);
                }
                finally {
                    fs.rmSync(tempWorkspace, { recursive: true, force: true });
                }
            });
            it('blocks access to files inside forbidden directories recursively', async () => {
                const tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-'));
                const forbiddenDir = path.join(tempWorkspace, 'forbidden');
                const nestedDir = path.join(forbiddenDir, 'nested');
                const nestedFile = path.join(nestedDir, 'test.txt');
                fs.mkdirSync(nestedDir, { recursive: true });
                fs.writeFileSync(nestedFile, 'secret');
                try {
                    const osManager = createSandboxManager({ enabled: true }, {
                        workspace: tempWorkspace,
                        forbiddenPaths: async () => [forbiddenDir],
                    });
                    const { command, args } = Platform.cat(nestedFile);
                    const sandboxed = await osManager.prepareCommand({
                        command,
                        args,
                        cwd: tempWorkspace,
                        env: process.env,
                    });
                    const result = await runCommand(sandboxed);
                    expect(result.status).not.toBe(0);
                }
                finally {
                    fs.rmSync(tempWorkspace, { recursive: true, force: true });
                }
            });
            it('prioritizes forbiddenPaths over allowedPaths', async () => {
                const tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-'));
                const conflictDir = path.join(tempWorkspace, 'conflict');
                const testFile = path.join(conflictDir, 'test.txt');
                fs.mkdirSync(conflictDir);
                try {
                    const osManager = createSandboxManager({ enabled: true }, {
                        workspace: tempWorkspace,
                        forbiddenPaths: async () => [conflictDir],
                    });
                    const { command, args } = Platform.touch(testFile);
                    const sandboxed = await osManager.prepareCommand({
                        command,
                        args,
                        cwd: tempWorkspace,
                        env: process.env,
                        policy: {
                            allowedPaths: [conflictDir],
                        },
                    });
                    const result = await runCommand(sandboxed);
                    expect(result.status).not.toBe(0);
                }
                finally {
                    fs.rmSync(tempWorkspace, { recursive: true, force: true });
                }
            });
            it('gracefully ignores non-existent paths in allowedPaths and forbiddenPaths', async () => {
                const tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-'));
                const nonExistentPath = path.join(tempWorkspace, 'does-not-exist');
                try {
                    const osManager = createSandboxManager({ enabled: true }, {
                        workspace: tempWorkspace,
                        forbiddenPaths: async () => [nonExistentPath],
                    });
                    const { command, args } = Platform.echo('survived');
                    const sandboxed = await osManager.prepareCommand({
                        command,
                        args,
                        cwd: tempWorkspace,
                        env: process.env,
                        policy: {
                            allowedPaths: [nonExistentPath],
                        },
                    });
                    const result = await runCommand(sandboxed);
                    expect(result.status).toBe(0);
                    expect(result.stdout.trim()).toBe('survived');
                }
                finally {
                    fs.rmSync(tempWorkspace, { recursive: true, force: true });
                }
            });
            it('prevents creation of non-existent forbidden paths', async () => {
                // Windows icacls cannot explicitly protect paths that have not yet been created.
                if (Platform.isWindows)
                    return;
                const tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-'));
                const nonExistentFile = path.join(tempWorkspace, 'never-created.txt');
                try {
                    const osManager = createSandboxManager({ enabled: true }, {
                        workspace: tempWorkspace,
                        forbiddenPaths: async () => [nonExistentFile],
                    });
                    // We use touch to attempt creation of the file
                    const { command: cmdTouch, args: argsTouch } = Platform.touch(nonExistentFile);
                    const sandboxedCmd = await osManager.prepareCommand({
                        command: cmdTouch,
                        args: argsTouch,
                        cwd: tempWorkspace,
                        env: process.env,
                    });
                    // Execute the command, we expect it to fail (permission denied or read-only file system)
                    const result = await runCommand(sandboxedCmd);
                    expect(result.status).not.toBe(0);
                    expect(fs.existsSync(nonExistentFile)).toBe(false);
                }
                finally {
                    fs.rmSync(tempWorkspace, { recursive: true, force: true });
                }
            });
            it('blocks access to both a symlink and its target when the symlink is forbidden', async () => {
                if (Platform.isWindows)
                    return;
                const tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-'));
                const targetFile = path.join(tempWorkspace, 'target.txt');
                const symlinkFile = path.join(tempWorkspace, 'link.txt');
                fs.writeFileSync(targetFile, 'secret data');
                fs.symlinkSync(targetFile, symlinkFile);
                try {
                    const osManager = createSandboxManager({ enabled: true }, {
                        workspace: tempWorkspace,
                        forbiddenPaths: async () => [symlinkFile],
                    });
                    // Attempt to read the target file directly
                    const { command: cmdTarget, args: argsTarget } = Platform.cat(targetFile);
                    const commandTarget = await osManager.prepareCommand({
                        command: cmdTarget,
                        args: argsTarget,
                        cwd: tempWorkspace,
                        env: process.env,
                    });
                    const resultTarget = await runCommand(commandTarget);
                    expect(resultTarget.status).not.toBe(0);
                    // Attempt to read via the symlink
                    const { command: cmdLink, args: argsLink } = Platform.cat(symlinkFile);
                    const commandLink = await osManager.prepareCommand({
                        command: cmdLink,
                        args: argsLink,
                        cwd: tempWorkspace,
                        env: process.env,
                    });
                    const resultLink = await runCommand(commandLink);
                    expect(resultLink.status).not.toBe(0);
                }
                finally {
                    fs.rmSync(tempWorkspace, { recursive: true, force: true });
                }
            });
        });
        describe('Network Access', () => {
            let server;
            let url;
            beforeAll(async () => {
                server = http.createServer((_, res) => {
                    res.setHeader('Connection', 'close');
                    res.writeHead(200);
                    res.end('ok');
                });
                await new Promise((resolve, reject) => {
                    server.on('error', reject);
                    server.listen(0, '127.0.0.1', () => {
                        const addr = server.address();
                        url = `http://127.0.0.1:${addr.port}`;
                        resolve();
                    });
                });
            });
            afterAll(async () => {
                if (server)
                    await new Promise((res) => server.close(() => res()));
            });
            // Windows Job Object rate limits exempt loopback (127.0.0.1) traffic,
            // so this test cannot verify loopback blocking on Windows.
            it.skipIf(Platform.isWindows)('blocks network access by default', async () => {
                const { command, args } = Platform.curl(url);
                const sandboxed = await manager.prepareCommand({
                    command,
                    args,
                    cwd: workspace,
                    env: process.env,
                });
                const result = await runCommand(sandboxed);
                expect(result.status).not.toBe(0);
            });
            it('grants network access when explicitly allowed', async () => {
                const { command, args } = Platform.curl(url);
                const sandboxed = await manager.prepareCommand({
                    command,
                    args,
                    cwd: workspace,
                    env: process.env,
                    policy: { networkAccess: true },
                });
                const result = await runCommand(sandboxed);
                expect(result.status).toBe(0);
                if (!Platform.isWindows) {
                    expect(result.stdout.trim()).toBe('ok');
                }
            });
        });
    });
});
//# sourceMappingURL=sandboxManager.integration.test.js.map