Files
fusion/packages/engine/src/sandbox/__tests__/audit.test.ts
Fusion (runfusion.ai) ad73d76586 feat(FN-4640): complete Step 6 — dashboard sandbox audit surface
Fusion-Task-Id: FN-4640
Fusion-Task-Lineage: 4a91265f-1714-4854-b08d-7ddb06074253
2026-05-15 16:19:42 -07:00

89 lines
3.3 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type { RunAuditor } from "../../run-audit.js";
import { withSandboxAudit } from "../audit.js";
import type { SandboxBackend, SandboxPolicy, SandboxRunOptions, SandboxRunResult } from "../types.js";
function makeBackend(runImpl?: (command: string, options: SandboxRunOptions) => Promise<SandboxRunResult>): SandboxBackend {
return {
capabilities: () => ({
id: "native",
supportsNetworkPolicy: false,
supportsFilesystemPolicy: false,
supportsStreaming: true,
platform: "any",
}),
prepare: vi.fn(async (policy: SandboxPolicy) => {
policy.onFallback?.({ fromBackendId: "sandbox-exec", toBackendId: "native", reason: "unavailable" });
}),
run: runImpl ?? vi.fn(async () => ({ stdout: "ok", stderr: "", exitCode: 0, signal: null, timedOut: false, bufferExceeded: false })),
runStreaming: vi.fn(async () => ({ outcome: "success" as const, stdout: "", stderr: "", bufferOverflow: false })),
dispose: vi.fn(async () => {}),
};
}
function makeAuditor() {
return {
git: vi.fn(async () => {}),
database: vi.fn(async () => {}),
filesystem: vi.fn(async () => {}),
sandbox: vi.fn(async () => {}),
} satisfies RunAuditor;
}
describe("withSandboxAudit", () => {
it("emits prepare once and fallback callback", async () => {
const auditor = makeAuditor();
const backend = withSandboxAudit(makeBackend(), auditor);
await backend.prepare({ allowNetwork: false });
await backend.prepare({ allowNetwork: false });
const sandboxCalls = auditor.sandbox.mock.calls as unknown as Array<[Parameters<RunAuditor["sandbox"]>[0]]>;
const prepareEvents = sandboxCalls.filter(([input]) => input.type === "sandbox:prepare");
const fallbackEvents = sandboxCalls.filter(([input]) => input.type === "sandbox:fallback");
expect(prepareEvents).toHaveLength(1);
expect(fallbackEvents).toHaveLength(2);
});
it("emits run on success", async () => {
const auditor = makeAuditor();
const backend = withSandboxAudit(makeBackend(), auditor);
await backend.run("echo hello", { cwd: "/tmp", timeoutMs: 10_000, maxBuffer: 1000 });
expect(auditor.sandbox).toHaveBeenCalledWith(
expect.objectContaining({ type: "sandbox:run", target: "native" }),
);
});
it("emits failure for non-zero exit, timeout, and buffer overflow", async () => {
const auditor = makeAuditor();
const backend = withSandboxAudit(
makeBackend(async () => ({ stdout: "", stderr: "err", exitCode: 1, signal: null, timedOut: true, bufferExceeded: true })),
auditor,
);
await backend.run("bad", { cwd: "/tmp", timeoutMs: 1000, maxBuffer: 10 });
expect(auditor.sandbox).toHaveBeenCalledWith(
expect.objectContaining({ type: "sandbox:failure", target: "native" }),
);
});
it("emits failure and rethrows on thrown error", async () => {
const auditor = makeAuditor();
const backend = withSandboxAudit(
makeBackend(async () => {
throw new Error("boom");
}),
auditor,
);
await expect(backend.run("explode", { cwd: "/tmp", timeoutMs: 1000, maxBuffer: 10 })).rejects.toThrow("boom");
expect(auditor.sandbox).toHaveBeenCalledWith(
expect.objectContaining({ type: "sandbox:failure", target: "native" }),
);
});
});