feat(FN-4640): complete Step 3 — add sandbox audit decorator
Fusion-Task-Id: FN-4640 Fusion-Task-Lineage: 4a91265f-1714-4854-b08d-7ddb06074253
This commit is contained in:
committed by
gsxdsm
parent
b28b1949a6
commit
115db18d17
87
packages/engine/src/sandbox/__tests__/audit.test.ts
Normal file
87
packages/engine/src/sandbox/__tests__/audit.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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", 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 prepareEvents = auditor.sandbox.mock.calls.filter(([input]) => input.type === "sandbox:prepare");
|
||||
const fallbackEvents = auditor.sandbox.mock.calls.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" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user