feat(FN-4638): complete Step 1 — add sandbox-exec detection
Fusion-Task-Id: FN-4638 Fusion-Task-Lineage: a6dcc3d9-b7ab-42c1-af88-c9e09f770d92
This commit is contained in:
@@ -0,0 +1,80 @@
|
|||||||
|
import { promisify } from "node:util";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const execMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("node:child_process", () => ({
|
||||||
|
exec: execMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
|
||||||
|
function setPlatform(platform: NodeJS.Platform): void {
|
||||||
|
Object.defineProperty(process, "platform", { value: platform });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("detectSandboxExec", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
execMock.mockReset();
|
||||||
|
(execMock as unknown as Record<symbol, unknown>)[promisify.custom] = vi.fn();
|
||||||
|
vi.resetModules();
|
||||||
|
setPlatform(originalPlatform);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects sandbox-exec on darwin", async () => {
|
||||||
|
setPlatform("darwin");
|
||||||
|
((execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
stdout: "/usr/bin/sandbox-exec\n",
|
||||||
|
stderr: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { detectSandboxExec } = await import("../../sandbox/sandbox-exec-detect.js");
|
||||||
|
const result = await detectSandboxExec();
|
||||||
|
|
||||||
|
expect(result).toEqual({ available: true, path: "/usr/bin/sandbox-exec" });
|
||||||
|
expect(((execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>)).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns unavailable when sandbox-exec check throws", async () => {
|
||||||
|
setPlatform("darwin");
|
||||||
|
((execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("ENOENT"));
|
||||||
|
|
||||||
|
const { detectSandboxExec } = await import("../../sandbox/sandbox-exec-detect.js");
|
||||||
|
const result = await detectSandboxExec();
|
||||||
|
|
||||||
|
expect(result).toEqual({ available: false, reason: "not-installed" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("short-circuits on non-darwin", async () => {
|
||||||
|
setPlatform("linux");
|
||||||
|
|
||||||
|
const { detectSandboxExec } = await import("../../sandbox/sandbox-exec-detect.js");
|
||||||
|
const result = await detectSandboxExec();
|
||||||
|
|
||||||
|
expect(result).toEqual({ available: false, reason: "not-darwin" });
|
||||||
|
expect(((execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses cache until reset", async () => {
|
||||||
|
setPlatform("darwin");
|
||||||
|
const execAsyncMock = (execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>;
|
||||||
|
execAsyncMock.mockResolvedValue({
|
||||||
|
stdout: "/usr/bin/sandbox-exec\n",
|
||||||
|
stderr: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { detectSandboxExec, resetSandboxExecDetectCache } = await import("../../sandbox/sandbox-exec-detect.js");
|
||||||
|
const first = await detectSandboxExec();
|
||||||
|
const second = await detectSandboxExec();
|
||||||
|
|
||||||
|
expect(first).toEqual(second);
|
||||||
|
expect(execAsyncMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
resetSandboxExecDetectCache();
|
||||||
|
execAsyncMock.mockResolvedValue({ stdout: "/opt/homebrew/bin/sandbox-exec\n", stderr: "" });
|
||||||
|
|
||||||
|
const third = await detectSandboxExec();
|
||||||
|
expect(third.path).toBe("/opt/homebrew/bin/sandbox-exec");
|
||||||
|
expect(execAsyncMock).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
54
packages/engine/src/sandbox/sandbox-exec-detect.ts
Normal file
54
packages/engine/src/sandbox/sandbox-exec-detect.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { exec } from "node:child_process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
export interface SandboxExecDetectResult {
|
||||||
|
available: boolean;
|
||||||
|
path?: string;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let detectPromise: Promise<SandboxExecDetectResult> | null = null;
|
||||||
|
|
||||||
|
async function detectSandboxExecUncached(): Promise<SandboxExecDetectResult> {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
return { available: false, reason: "not-darwin" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync("command -v sandbox-exec && sandbox-exec -p '(version 1)(allow default)' /usr/bin/true", {
|
||||||
|
timeout: 5_000,
|
||||||
|
maxBuffer: 256 * 1024,
|
||||||
|
encoding: "utf-8",
|
||||||
|
shell: "/bin/bash",
|
||||||
|
});
|
||||||
|
|
||||||
|
const lines = (stdout ?? "")
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
available: true,
|
||||||
|
...(lines[0] ? { path: lines[0] } : {}),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
reason: message.includes("not found") || message.includes("ENOENT") ? "not-installed" : message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function detectSandboxExec(): Promise<SandboxExecDetectResult> {
|
||||||
|
if (!detectPromise) {
|
||||||
|
detectPromise = detectSandboxExecUncached();
|
||||||
|
}
|
||||||
|
return detectPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetSandboxExecDetectCache(): void {
|
||||||
|
detectPromise = null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user