import { describe, it, expect, afterEach } from "vitest";
import { sandboxCommand, cleanupSandbox, isSandboxAvailable } from "./sandbox.js";
import { existsSync, readFileSync } from "fs";
// ============================================================================
// sandboxCommand
// ============================================================================
describe("sandboxCommand", () => {
const profilePaths: (string | null)[] = [];
afterEach(() => {
// Clean up any sandbox profiles created during tests
for (const p of profilePaths) {
cleanupSandbox(p);
}
profilePaths.length = 0;
});
// These tests are macOS-specific since sandbox-exec only exists on Darwin
if (process.platform === "darwin") {
it("wraps command with sandbox-exec on macOS", () => {
const { wrapped, profilePath } = sandboxCommand("echo hello", "/tmp/test");
profilePaths.push(profilePath);
expect(wrapped).toContain("sandbox-exec");
expect(wrapped).toContain("/bin/bash");
expect(wrapped).toContain("echo hello");
expect(profilePath).not.toBeNull();
});
it("creates a profile file on disk", () => {
const { profilePath } = sandboxCommand("ls", "/tmp/test");
profilePaths.push(profilePath);
expect(profilePath).not.toBeNull();
expect(existsSync(profilePath!)).toBe(true);
});
it("profile includes cwd in allow rules", () => {
const cwd = "/Users/testuser/project";
const { profilePath } = sandboxCommand("ls", cwd);
profilePaths.push(profilePath);
const profile = readFileSync(profilePath!, "utf-8");
expect(profile).toContain(`(allow file-write* (subpath "${cwd}"))`);
});
it("profile includes /tmp in allow rules", () => {
const { profilePath } = sandboxCommand("ls", "/some/dir");
profilePaths.push(profilePath);
const profile = readFileSync(profilePath!, "utf-8");
expect(profile).toContain('(allow file-write* (subpath "/tmp"))');
expect(profile).toContain('(allow file-write* (subpath "/private/tmp"))');
});
it("profile includes ~/.swagmanager in allow rules", () => {
const { profilePath } = sandboxCommand("ls", "/some/dir");
profilePaths.push(profilePath);
const profile = readFileSync(profilePath!, "utf-8");
expect(profile).toContain(".swagmanager");
expect(profile).toMatch(/allow file-write\*.*\.swagmanager/);
});
it("profile includes /dev for terminal I/O", () => {
const { profilePath } = sandboxCommand("ls", "/some/dir");
profilePaths.push(profilePath);
const profile = readFileSync(profilePath!, "utf-8");
expect(profile).toContain('(allow file-write* (subpath "/dev"))');
});
it("profile includes /private/var/folders for macOS temp", () => {
const { profilePath } = sandboxCommand("ls", "/some/dir");
profilePaths.push(profilePath);
const profile = readFileSync(profilePath!, "utf-8");
expect(profile).toContain('(allow file-write* (subpath "/private/var/folders"))');
});
it("profile denies file-write by default", () => {
const { profilePath } = sandboxCommand("ls", "/some/dir");
profilePaths.push(profilePath);
const profile = readFileSync(profilePath!, "utf-8");
expect(profile).toContain("(deny file-write*)");
});
it("profile allows default (read, network, etc.)", () => {
const { profilePath } = sandboxCommand("ls", "/some/dir");
profilePaths.push(profilePath);
const profile = readFileSync(profilePath!, "utf-8");
expect(profile).toContain("(allow default)");
});
it("shell-escapes the profile path in the wrapped command", () => {
const { wrapped, profilePath } = sandboxCommand("echo test", "/tmp/test");
profilePaths.push(profilePath);
// Profile path should be single-quoted in the command
expect(wrapped).toContain(`'${profilePath}'`);
});
it("shell-escapes the command itself", () => {
const cmd = "echo 'hello world'";
const { wrapped, profilePath } = sandboxCommand(cmd, "/tmp/test");
profilePaths.push(profilePath);
// The command should be wrapped in single quotes (with inner quotes escaped)
expect(wrapped).toContain("/bin/bash -c");
// The inner single quotes in the command should be escaped
expect(wrapped).toContain("'\\''");
});
it("handles commands with special characters", () => {
const cmd = 'echo "hello" && cat /etc/passwd | grep root';
const { wrapped, profilePath } = sandboxCommand(cmd, "/tmp/test");
profilePaths.push(profilePath);
expect(wrapped).toContain("sandbox-exec");
expect(profilePath).not.toBeNull();
});
it("generates unique profile paths for sequential calls", () => {
const { profilePath: p1 } = sandboxCommand("echo 1", "/tmp/test");
const { profilePath: p2 } = sandboxCommand("echo 2", "/tmp/test");
profilePaths.push(p1, p2);
expect(p1).not.toBe(p2);
});
} else {
it("returns command unchanged on non-macOS", () => {
const { wrapped, profilePath } = sandboxCommand("echo hello", "/tmp/test");
expect(wrapped).toBe("echo hello");
expect(profilePath).toBeNull();
});
}
});
// ============================================================================
// cleanupSandbox
// ============================================================================
describe("cleanupSandbox", () => {
it("handles null profilePath gracefully", () => {
// Should not throw
expect(() => cleanupSandbox(null)).not.toThrow();
});
if (process.platform === "darwin") {
it("removes the profile file from disk", () => {
const { profilePath } = sandboxCommand("echo test", "/tmp/test");
expect(profilePath).not.toBeNull();
expect(existsSync(profilePath!)).toBe(true);
cleanupSandbox(profilePath);
expect(existsSync(profilePath!)).toBe(false);
});
it("handles already-deleted profile gracefully", () => {
const { profilePath } = sandboxCommand("echo test", "/tmp/test");
cleanupSandbox(profilePath);
// Second cleanup should not throw
expect(() => cleanupSandbox(profilePath)).not.toThrow();
});
}
});
// ============================================================================
// isSandboxAvailable
// ============================================================================
describe("isSandboxAvailable", () => {
if (process.platform === "darwin") {
it("returns true on macOS (sandbox-exec exists)", () => {
expect(isSandboxAvailable()).toBe(true);
});
} else {
it("returns false on non-macOS platforms", () => {
expect(isSandboxAvailable()).toBe(false);
});
}
});