feat(FN-4637): complete Step 1 — detect bubblewrap availability
Fusion-Task-Id: FN-4637 Fusion-Task-Lineage: 564c5692-3aaf-4396-9306-a395703cf365
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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("detectBwrap", () => {
|
||||
beforeEach(async () => {
|
||||
execMock.mockReset();
|
||||
(execMock as unknown as Record<symbol, unknown>)[promisify.custom] = vi.fn();
|
||||
vi.resetModules();
|
||||
setPlatform(originalPlatform);
|
||||
});
|
||||
|
||||
it("detects bubblewrap on linux and parses version/path", async () => {
|
||||
setPlatform("linux");
|
||||
((execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
stdout: "/usr/bin/bwrap\nbubblewrap 0.9.0\n",
|
||||
stderr: "",
|
||||
});
|
||||
|
||||
const { detectBwrap } = await import("../../sandbox/bubblewrap-detect.js");
|
||||
const result = await detectBwrap();
|
||||
|
||||
expect(result).toEqual({ available: true, path: "/usr/bin/bwrap", version: "0.9.0" });
|
||||
expect(((execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>)).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns unavailable when bwrap is missing", async () => {
|
||||
setPlatform("linux");
|
||||
((execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("command not found"));
|
||||
|
||||
const { detectBwrap } = await import("../../sandbox/bubblewrap-detect.js");
|
||||
const result = await detectBwrap();
|
||||
|
||||
expect(result.available).toBe(false);
|
||||
expect(result.reason).toBe("not-installed");
|
||||
});
|
||||
|
||||
it("short-circuits on non-linux platforms", async () => {
|
||||
setPlatform("darwin");
|
||||
|
||||
const { detectBwrap } = await import("../../sandbox/bubblewrap-detect.js");
|
||||
const result = await detectBwrap();
|
||||
|
||||
expect(result).toEqual({ available: false, reason: "not-linux" });
|
||||
expect(((execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses cached result until reset", async () => {
|
||||
setPlatform("linux");
|
||||
((execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
stdout: "/usr/bin/bwrap\nbwrap 1.0.0\n",
|
||||
stderr: "",
|
||||
});
|
||||
|
||||
const { detectBwrap, resetBwrapDetectCache } = await import("../../sandbox/bubblewrap-detect.js");
|
||||
const first = await detectBwrap();
|
||||
const second = await detectBwrap();
|
||||
|
||||
expect(first).toEqual(second);
|
||||
expect(((execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>)).toHaveBeenCalledTimes(1);
|
||||
|
||||
resetBwrapDetectCache();
|
||||
((execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
stdout: "/opt/bin/bwrap\nbwrap 2.0.0\n",
|
||||
stderr: "",
|
||||
});
|
||||
|
||||
const third = await detectBwrap();
|
||||
expect(third.path).toBe("/opt/bin/bwrap");
|
||||
expect(((execMock as unknown as Record<symbol, unknown>)[promisify.custom] as ReturnType<typeof vi.fn>)).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
61
packages/engine/src/sandbox/bubblewrap-detect.ts
Normal file
61
packages/engine/src/sandbox/bubblewrap-detect.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export interface BwrapDetectResult {
|
||||
available: boolean;
|
||||
version?: string;
|
||||
path?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
let detectPromise: Promise<BwrapDetectResult> | null = null;
|
||||
|
||||
function parseVersion(raw: string): string | undefined {
|
||||
const match = raw.match(/(\d+\.\d+(?:\.\d+)?)/);
|
||||
return match?.[1];
|
||||
}
|
||||
|
||||
async function detectBwrapUncached(): Promise<BwrapDetectResult> {
|
||||
if (process.platform !== "linux") {
|
||||
return { available: false, reason: "not-linux" };
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync("command -v bwrap && bwrap --version", {
|
||||
timeout: 5_000,
|
||||
maxBuffer: 256 * 1024,
|
||||
encoding: "utf-8",
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
|
||||
const combined = `${stdout ?? ""}\n${stderr ?? ""}`.trim();
|
||||
const lines = (stdout ?? "").split("\n").map((line) => line.trim()).filter(Boolean);
|
||||
const path = lines[0];
|
||||
const version = parseVersion(combined);
|
||||
|
||||
return {
|
||||
available: true,
|
||||
...(version !== undefined && { version }),
|
||||
...(path !== undefined && { path }),
|
||||
};
|
||||
} 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 detectBwrap(): Promise<BwrapDetectResult> {
|
||||
if (!detectPromise) {
|
||||
detectPromise = detectBwrapUncached();
|
||||
}
|
||||
return detectPromise;
|
||||
}
|
||||
|
||||
export function resetBwrapDetectCache(): void {
|
||||
detectPromise = null;
|
||||
}
|
||||
Reference in New Issue
Block a user