146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
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<Response>;
|
|
}
|
|
).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<Response>;
|
|
}
|
|
).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<string, true>,
|
|
requestId: "tiny-req-1",
|
|
timestamp: Date.now(),
|
|
};
|
|
}
|