import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import type { WasmSandboxHandle } from "@suaveplan/api-sandbox-wasm"; import { createSandboxedWasmPluginModule } from "@suaveplan/api-sandbox-wasm-host-bridge"; import { describe, expect, it } from "@suaveplan/testing/runner"; import { compilePluginToWasm } from "../../src/index.js"; const HERE = dirname(fileURLToPath(import.meta.url)); const FIXTURE = resolve(HERE, "../fixtures/tiny-plugin/src/index.ts"); const MANIFEST = { name: "tiny-plugin", version: "0.1.0", apiHost: "@suaveplan/api-host@0" as const, isolation: "sandboxed" as const, trustLevel: "first-party" as const, requiredCapabilities: [], }; /** * Structural sandbox stub. The wasi-stdio bridge variant only reaches * for `dispose` and `disposed` — `instance.exports` is never touched. */ function createSandboxStub(): WasmSandboxHandle { let disposed = false; return { pluginName: "tiny-plugin", instance: { exports: {} } as unknown as WebAssembly.Instance, preopens: [], outbound: { allow: () => false } as never, dispose: () => { disposed = true; }, get disposed() { return disposed; }, } as unknown as WasmSandboxHandle; } describe("tiny-plugin compile + deploy", () => { it("compiles, validates, extracts routes, and deploys via wasi-stdio bridge", { timeout: 120000, }, async () => { const outDir = mkdtempSync(join(tmpdir(), "tiny-compile-")); const result = await compilePluginToWasm({ pluginEntry: FIXTURE, outDir, pluginName: "tiny-plugin", }); const paths = result.manifest.routes.map((r) => r.path); expect(paths).toContain("/tiny/hello"); expect(paths).toContain("/tiny/echo"); expect(result.sizeBytes).toBeGreaterThan(1000); const wasmBytes = new Uint8Array(readFileSync(result.wasmPath)); const wasmModule = await WebAssembly.compile(wasmBytes); const sandbox = createSandboxStub(); const install = createSandboxedWasmPluginModule({ abiVariant: "wasi-stdio", sandbox, manifest: MANIFEST, wasmModule, }); expect(install.routes.length).toBe(2); install.lifecycle.start(); const helloRoute = install.routes.find((r) => r.path === "/tiny/hello"); expect(helloRoute).toBeDefined(); const ctx = buildRequestContext( "https://example.com/tiny/hello", "GET" ); const resp = await ( helloRoute as never as { handler: (c: unknown) => Promise; } ).handler(ctx); expect(resp.status).toBe(200); const text = await resp.text(); expect(text.includes("hello")).toBe(true); // Lifecycle dispose roundtrip install.lifecycle.dispose(); rmSync(outDir, { recursive: true, force: true }); }); it("exercises the echo POST route", { timeout: 120000 }, async () => { const outDir = mkdtempSync(join(tmpdir(), "tiny-echo-")); const result = await compilePluginToWasm({ pluginEntry: FIXTURE, outDir, pluginName: "tiny-plugin", }); const wasmBytes = new Uint8Array(readFileSync(result.wasmPath)); const wasmModule = await WebAssembly.compile(wasmBytes); const sandbox = createSandboxStub(); const install = createSandboxedWasmPluginModule({ abiVariant: "wasi-stdio", sandbox, manifest: MANIFEST, wasmModule, }); install.lifecycle.start(); const echoRoute = install.routes.find((r) => r.path === "/tiny/echo"); const ctx = buildRequestContext( "https://example.com/tiny/echo", "POST", "hello-echo-body" ); const resp = await ( echoRoute as never as { handler: (c: unknown) => Promise; } ).handler(ctx); expect(resp.status).toBe(200); const body = await resp.text(); expect(body).toBe("hello-echo-body"); install.lifecycle.dispose(); rmSync(outDir, { recursive: true, force: true }); }); }); function buildRequestContext( url: string, method: string, body?: string ): unknown { const init: RequestInit = { method }; if (body !== undefined) init.body = body; return { request: new Request(url, init), params: {}, plugin: { name: "tiny-plugin" }, principal: null, tenant: null, capabilities: {} as Record, requestId: "tiny-req-1", timestamp: Date.now(), }; }