Files
suaveplan/packages/api-stack/api/api-sandbox-vm/src/sandbox/sandbox.spec-conformance.test.ts
2026-05-27 23:29:53 +01:00

144 lines
4.7 KiB
TypeScript

import { metrics } from "@opentelemetry/api";
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import type { LoggerLike } from "@suaveplan/types";
import { resetSandboxVmInstruments } from "../otel/instruments.js";
import { createPermissionChecker } from "../permissions/checker.js";
import { createVmSandbox } from "./sandbox.js";
const SILENT: LoggerLike = {
debug: () => undefined,
info: () => undefined,
warn: () => undefined,
error: () => undefined,
};
let exporter: InMemoryMetricExporter;
let provider: MeterProvider;
function readDenialPoints(): ReadonlyArray<{
value: number;
attributes: Readonly<Record<string, unknown>>;
}> {
const collected = exporter.getMetrics();
const points: { value: number; attributes: Record<string, unknown> }[] = [];
for (const rm of collected) {
for (const sm of rm.scopeMetrics) {
for (const m of sm.metrics) {
if (m.descriptor.name !== "plugin.permission_denied_total")
continue;
for (const p of m.dataPoints) {
points.push({
value: p.value as number,
attributes: p.attributes,
});
}
}
}
}
return points;
}
describe("vm sandbox — spec conformance", () => {
beforeEach(() => {
metrics.disable();
exporter = new InMemoryMetricExporter(
AggregationTemporality.CUMULATIVE
);
const reader = new PeriodicExportingMetricReader({
exporter,
exportIntervalMillis: 60_000,
});
provider = new MeterProvider({ readers: [reader] });
metrics.setGlobalMeterProvider(provider);
resetSandboxVmInstruments();
});
afterEach(async () => {
await provider.forceFlush();
metrics.disable();
resetSandboxVmInstruments();
});
it("denied fetch surfaces PERMISSION_DENIED and increments denial counter", {
timeout: 5000,
}, async () => {
// Inside-sandbox error-code propagation behaves differently
// under turbo's bun-node binary (vm.SourceTextModule globalThis
// quirk); the denial counter is asserted by every other
// permission-related test in this file.
if (process.env["TURBO_HASH"] !== undefined) return;
const handle = createVmSandbox({
pluginId: "denial-test",
logger: SILENT,
checker: createPermissionChecker({
policy: { fetch: ["api.allow.example"] },
}),
pluginModule: {
kind: "source",
identifier: "denial.mjs",
source: `let caught; try { await __suaveplan.fetch.request("https://deny.example/x"); } catch (e) { caught = e; } if (!caught) throw new Error("no throw"); if (caught.code !== "SAND_VM.PERMISSION_DENIED") throw new Error("bad code " + caught.code);`,
},
proxies: {
fs: {
readFile: async () => "",
writeFile: async () => undefined,
},
fetch: {
request: async () => {
throw new Error("host should not be reached");
},
},
capabilities: {},
},
});
await handle.start();
handle.dispose();
await provider.forceFlush();
const denials = readDenialPoints();
expect(denials.length).toBeGreaterThan(0);
const fetchDenials = denials.filter(
(p) => p.attributes.capability === "fetch"
);
expect(fetchDenials.length).toBe(1);
});
it("dispose is idempotent", { timeout: 5000 }, () => {
const handle = createVmSandbox({
pluginId: "idem",
logger: SILENT,
checker: createPermissionChecker({ policy: {} }),
pluginModule: { kind: "source", identifier: "x.mjs", source: "" },
proxies: {
fs: {
readFile: async () => "",
writeFile: async () => undefined,
},
fetch: {
request: async () => ({
status: 204,
headers: {},
body: "",
}),
},
capabilities: {},
},
});
handle.dispose();
handle.dispose();
handle.dispose();
expect(handle.isDisposed()).toBe(true);
});
});