/** * Trusted vs wasm parity test. For every overlapping route, dispatches * the same logical request through: * 1. The trusted-tier handler in `src/trusted/routes/routes.ts` * 2. The wasm-tier `plugin.handleRequest` in `src/wasm/index.ts` * and asserts the responses have the same HTTP status and JSON body. * * The streaming endpoint `/example/stream/large-report` is skipped with * an explanatory `it.skip` — chunked `ReadableStream` responses cannot * be expressed under the WASI-stdio contract. * * Both surfaces consume the SAME `domain/invoice-rules.ts` module, so * the parity contract reduces to "both adapters serialise the same * domain output identically". */ import { afterEach, beforeEach, describe, expect, it, } from "@suaveplan/testing/runner"; import { buildRoutes, createInMemoryLedger } from "../../src/trusted/index.js"; import { plugin } from "../../src/wasm/index.js"; import { resetState } from "../../src/wasm/wasm-state.js"; interface ParityInput { readonly name: string; readonly method: string; readonly path: string; readonly scopes: readonly string[]; readonly body?: unknown; } const ADMIN_SCOPES = ["example:read", "example:write"] as const; const URL_BASE = "http://t"; function encodeBase64Url(text: string): string { let binary = ""; const bytes = new TextEncoder().encode(text); for (let i = 0; i < bytes.length; i += 1) { binary += String.fromCharCode(bytes[i] ?? 0); } return btoa(binary) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/g, ""); } function decodeBase64Url(b64url: string): string { if (b64url.length === 0) return ""; const padLen = (4 - (b64url.length % 4)) % 4; const padded = b64url.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(padLen); return atob(padded); } interface TrustedTable { readonly table: ReturnType; } function freshTrusted(): TrustedTable { const ledger = createInMemoryLedger(); const table = buildRoutes({ ledger, publisher: { publish() { // Publishing is a trusted-tier-only concern; the wasm tier // does not emit events. The parity test asserts response // equality, not side effects, so we discard. }, }, }); return { table }; } interface NormalisedResponse { readonly status: number; readonly bodyText: string; } async function runTrusted( table: TrustedTable["table"], input: ParityInput ): Promise { const method = input.method; const fullPath = input.path; // Look up via path pattern: trusted uses `:id` segments. const lookupPath = toPattern(method, fullPath); const route = table.find( (r) => r.method === method && r.path === lookupPath ); if (route === undefined) { throw new Error(`trusted route not found: ${method} ${lookupPath}`); } const params = extractParams(lookupPath, fullPath); const principal = { id: "p", scopes: input.scopes as unknown as ReadonlyArray< "example:read" | "example:write" >, claims: {}, }; const url = new URL(`${URL_BASE}${fullPath}`); const query: Record = {}; url.searchParams.forEach((value, key) => { query[key] = value; }); const ctxBase = { principal, tenant: null, request: new Request(`${URL_BASE}${fullPath}`, { method, headers: input.body !== undefined ? { "content-type": "application/json" } : {}, body: input.body !== undefined ? JSON.stringify(input.body) : undefined, }), body: input.body, query, params, }; const out = await route.handler(ctxBase); if (out instanceof Response) { return { status: out.status, bodyText: await out.text(), }; } return { status: 200, bodyText: JSON.stringify(out) }; } async function runWasm(input: ParityInput): Promise { const res = await plugin.handleRequest({ url: `${URL_BASE}${input.path}`, method: input.method, headers: input.body !== undefined ? [["content-type", "application/json"]] : [], bodyBase64: input.body !== undefined ? encodeBase64Url(JSON.stringify(input.body)) : "", principalId: null, tenantId: null, capabilities: input.scopes, requestId: "r", timestamp: 0, }); return { status: res.status, bodyText: decodeBase64Url(res.bodyBase64) }; } function toPattern(method: string, path: string): string { // /example/invoices/inv-1 -> /example/invoices/:id if (path.startsWith("/example/invoices/") && path !== "/example/invoices") { const tail = path.slice("/example/invoices/".length); if (tail.endsWith("/finalise")) { return "/example/invoices/:id/finalise"; } if (!tail.includes("/")) return "/example/invoices/:id"; } // Strip query string for matching. const q = path.indexOf("?"); if (q !== -1) return path.slice(0, q); return path; } function extractParams( pattern: string, actual: string ): Record { const patternParts = pattern.split("/"); const actualPath = actual.split("?")[0] ?? actual; const actualParts = actualPath.split("/"); const params: Record = {}; for (let i = 0; i < patternParts.length; i += 1) { const p = patternParts[i]; if (p?.startsWith(":")) { params[p.slice(1)] = actualParts[i] ?? ""; } } return params; } beforeEach(() => { resetState(); }); afterEach(() => { resetState(); }); const SHARED_BODIES: readonly ParityInput[] = [ { name: "public ping", method: "GET", path: "/example/public/ping", scopes: [], }, { name: "health", method: "GET", path: "/example/health", scopes: [], }, { name: "rate-limited", method: "GET", path: "/example/rate-limited", scopes: [], }, { name: "list invoices (no scope -> 403)", method: "GET", path: "/example/invoices", scopes: [], }, { name: "list invoices (with scope -> 200)", method: "GET", path: "/example/invoices", scopes: ADMIN_SCOPES, }, { name: "createInvoice (bad body -> 400)", method: "POST", path: "/example/invoices", scopes: ADMIN_SCOPES, body: { customerId: "" }, }, { name: "createInvoice (no scope -> 403)", method: "POST", path: "/example/invoices", scopes: [], body: { customerId: "c", amountCents: 1, currency: "USD" }, }, { name: "fail-response default", method: "GET", path: "/_/test/fail-response", scopes: [], }, { name: "fail-response with flag", method: "GET", path: "/_/test/fail-response?failResponse=1", scopes: [], }, ]; describe("trusted vs wasm parity for every shared route", () => { for (const input of SHARED_BODIES) { it(input.name, { timeout: 5000 }, async () => { resetState(); const trustedHandle = freshTrusted(); const [t, w] = await Promise.all([ runTrusted(trustedHandle.table, input), runWasm(input), ]); expect(w.status).toBe(t.status); expect(w.bodyText).toBe(t.bodyText); }); } it("createInvoice happy path body matches across surfaces", { timeout: 5000, }, async () => { resetState(); const trustedHandle = freshTrusted(); const input: ParityInput = { name: "createInvoice", method: "POST", path: "/example/invoices", scopes: ADMIN_SCOPES, body: { customerId: "c", amountCents: 1500, currency: "USD" }, }; const t = await runTrusted(trustedHandle.table, input); const w = await runWasm(input); expect(w.status).toBe(t.status); expect(w.bodyText).toBe(t.bodyText); }); it("getInvoice happy path body matches", { timeout: 5000 }, async () => { resetState(); const trustedHandle = freshTrusted(); const createBody = { customerId: "c", amountCents: 1, currency: "USD", }; const create: ParityInput = { name: "create", method: "POST", path: "/example/invoices", scopes: ADMIN_SCOPES, body: createBody, }; await runTrusted(trustedHandle.table, create); await runWasm(create); const get: ParityInput = { name: "get", method: "GET", path: "/example/invoices/inv-1", scopes: ADMIN_SCOPES, }; const t = await runTrusted(trustedHandle.table, get); const w = await runWasm(get); expect(w.status).toBe(t.status); expect(w.bodyText).toBe(t.bodyText); }); it.skip("streamLargeReport is skipped — streaming responses are out of scope for the wasm tier", { timeout: 5000, }, () => { // Documented omission: the wasm subpath omits this endpoint // because Javy/QuickJS lacks ReadableStream and the // WASI-stdio envelope has no chunked-response shape. }); });