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>; }> { const collected = exporter.getMetrics(); const points: { value: number; attributes: Record }[] = []; 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); }); });