Initial Commit
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve as resolvePath } from "node:path";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
|
||||
interface PackageJsonShape {
|
||||
readonly dependencies?: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
describe("@suaveplan/api-sandbox-wasm — dep graph", () => {
|
||||
it("pins the runtime dep set to the documented allowlist", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(resolvePath(__dirname, "..", "package.json"), "utf8")
|
||||
) as PackageJsonShape;
|
||||
const deps = Object.keys(pkg.dependencies ?? {}).sort();
|
||||
// The proposal pins the @suaveplan/* runtime surface; external
|
||||
// deps (zod, tslib, @opentelemetry/api) come from the host
|
||||
// stack and are intentionally not enumerated here.
|
||||
expect(deps).toEqual([
|
||||
"@opentelemetry/api",
|
||||
"@suaveplan/api-types",
|
||||
"@suaveplan/error",
|
||||
"@suaveplan/logger",
|
||||
"tslib",
|
||||
"zod",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not depend on any sandbox-host sibling", { timeout: 5000 }, () => {
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(resolvePath(__dirname, "..", "package.json"), "utf8")
|
||||
) as PackageJsonShape;
|
||||
const deps = Object.keys(pkg.dependencies ?? {});
|
||||
expect(deps.some((d) => d.startsWith("@suaveplan/api-sandbox-"))).toBe(
|
||||
false
|
||||
);
|
||||
expect(deps).not.toContain("@suaveplan/api-host");
|
||||
expect(deps).not.toContain("@suaveplan/api-worker-host");
|
||||
});
|
||||
});
|
||||
80
packages/api-stack/api/api-sandbox-wasm/tests/fixtures/build-wasm.test.ts
vendored
Normal file
80
packages/api-stack/api/api-sandbox-wasm/tests/fixtures/build-wasm.test.ts
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { buildFixtureWasm } from "./build-wasm.js";
|
||||
|
||||
describe("buildFixtureWasm", () => {
|
||||
it("emits the WASM magic + version preamble", { timeout: 5000 }, () => {
|
||||
const bytes = buildFixtureWasm({ importProcExit: false });
|
||||
expect([...bytes.subarray(0, 4)]).toEqual([0x00, 0x61, 0x73, 0x6d]);
|
||||
expect([...bytes.subarray(4, 8)]).toEqual([0x01, 0x00, 0x00, 0x00]);
|
||||
});
|
||||
|
||||
it("produces a module valid under WebAssembly.validate", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
WebAssembly.validate(buildFixtureWasm({ importProcExit: true }))
|
||||
).toBe(true);
|
||||
expect(
|
||||
WebAssembly.validate(buildFixtureWasm({ importProcExit: false }))
|
||||
).toBe(true);
|
||||
expect(
|
||||
WebAssembly.validate(
|
||||
buildFixtureWasm({
|
||||
capabilityImports: ["secrets_get", "secrets_require"],
|
||||
})
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("threads the requested WASI version into the import module name", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const bytes = buildFixtureWasm({
|
||||
wasiVersion: "wasi_snapshot_preview0",
|
||||
importProcExit: true,
|
||||
});
|
||||
const module = await WebAssembly.compile(bytes);
|
||||
const imports = WebAssembly.Module.imports(module);
|
||||
expect(imports.some((i) => i.module === "wasi_snapshot_preview0")).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("omits the WASI import when importProcExit is false", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const module = await WebAssembly.compile(
|
||||
buildFixtureWasm({ importProcExit: false })
|
||||
);
|
||||
const imports = WebAssembly.Module.imports(module);
|
||||
expect(imports.some((i) => i.module.startsWith("wasi_"))).toBe(false);
|
||||
});
|
||||
|
||||
it("emits capability imports under suaveplan_caps", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const module = await WebAssembly.compile(
|
||||
buildFixtureWasm({ capabilityImports: ["secrets_get"] })
|
||||
);
|
||||
const imports = WebAssembly.Module.imports(module);
|
||||
expect(imports.find((i) => i.name === "secrets_get")?.module).toBe(
|
||||
"suaveplan_caps"
|
||||
);
|
||||
});
|
||||
|
||||
it("always exports _start", { timeout: 5000 }, async () => {
|
||||
const module = await WebAssembly.compile(buildFixtureWasm());
|
||||
const exports = WebAssembly.Module.exports(module);
|
||||
expect(exports.find((e) => e.name === "_start")?.kind).toBe("function");
|
||||
});
|
||||
|
||||
it("defaults wasiVersion to wasi_snapshot_preview1", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const module = await WebAssembly.compile(buildFixtureWasm());
|
||||
const imports = WebAssembly.Module.imports(module);
|
||||
expect(imports.some((i) => i.module === "wasi_snapshot_preview1")).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
138
packages/api-stack/api/api-sandbox-wasm/tests/fixtures/build-wasm.ts
vendored
Normal file
138
packages/api-stack/api/api-sandbox-wasm/tests/fixtures/build-wasm.ts
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Tiny WASM-module builder used by in-package fixtures + the
|
||||
* conformance harness. Emits hand-encoded WebAssembly bytes per the
|
||||
* binary format spec (https://webassembly.github.io/spec/core/binary/).
|
||||
*
|
||||
* Only enough to construct test modules — one functype `() -> ()`, one
|
||||
* import, one export, an `_start` body. NOT a general-purpose WAT
|
||||
* compiler. Kept inside the package so the test suite has no external
|
||||
* build step (`wabt` / `wat2wasm`); the fixture wasm is produced
|
||||
* deterministically at runtime.
|
||||
*/
|
||||
|
||||
const MAGIC = new Uint8Array([0x00, 0x61, 0x73, 0x6d]);
|
||||
const VERSION = new Uint8Array([0x01, 0x00, 0x00, 0x00]);
|
||||
|
||||
interface BuilderOptions {
|
||||
/** WASI version module name to import (default `wasi_snapshot_preview1`). */
|
||||
readonly wasiVersion?: string;
|
||||
/** Whether to import `wasi_snapshot_preview1.proc_exit`. */
|
||||
readonly importProcExit?: boolean;
|
||||
/** Additional capability imports — emitted under `suaveplan_caps`. */
|
||||
readonly capabilityImports?: readonly string[];
|
||||
/** Export a `memory` of `initial` pages — exercises sandbox memory binding. */
|
||||
readonly exportMemory?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a deterministic, validator-clean WASM module with the requested
|
||||
* imports + an exported `_start` whose body is just `end`. Adequate for
|
||||
* `WebAssembly.compile` + `WebAssembly.instantiate` against a matching
|
||||
* imports object.
|
||||
*/
|
||||
export function buildFixtureWasm(options: BuilderOptions = {}): Uint8Array {
|
||||
const wasiVersion = options.wasiVersion ?? "wasi_snapshot_preview1";
|
||||
const importProcExit = options.importProcExit !== false;
|
||||
const capImports = options.capabilityImports ?? [];
|
||||
|
||||
// Type section: index 0 = () -> () (for _start + capability imports);
|
||||
// index 1 = (i32) -> () (for proc_exit).
|
||||
const types: number[] = [
|
||||
0x02 /* count */,
|
||||
...funcType([], []),
|
||||
...funcType([0x7f], []),
|
||||
];
|
||||
const typeSection = section(0x01, types);
|
||||
|
||||
// Import section.
|
||||
const importEntries: number[][] = [];
|
||||
if (importProcExit) {
|
||||
importEntries.push(importEntry(wasiVersion, "proc_exit", 0x00, 1));
|
||||
}
|
||||
for (const cap of capImports) {
|
||||
importEntries.push(importEntry("suaveplan_caps", cap, 0x00, 0));
|
||||
}
|
||||
const importSection = section(0x02, [
|
||||
importEntries.length,
|
||||
...flatten(importEntries),
|
||||
]);
|
||||
|
||||
// Function section: one local function, type 0.
|
||||
const funcSection = section(0x03, [0x01, 0x00]);
|
||||
|
||||
// Optional memory section: 1 limits entry, flags=0 (no max), initial=1 page.
|
||||
const exportMemory = options.exportMemory === true;
|
||||
const memorySection = exportMemory ? section(0x05, [0x01, 0x00, 0x01]) : [];
|
||||
|
||||
// Export section: "_start" + optional "memory".
|
||||
const localFnIndex = importEntries.length;
|
||||
const exportItems: number[][] = [exportEntry("_start", 0x00, localFnIndex)];
|
||||
if (exportMemory) {
|
||||
exportItems.push(exportEntry("memory", 0x02, 0));
|
||||
}
|
||||
const exportSection = section(0x07, [
|
||||
exportItems.length,
|
||||
...flatten(exportItems),
|
||||
]);
|
||||
|
||||
// Code section: one body, locals=0, end.
|
||||
const codeSection = section(0x0a, [0x01, 0x02, 0x00, 0x0b]);
|
||||
|
||||
return concatBytes([
|
||||
MAGIC,
|
||||
VERSION,
|
||||
new Uint8Array(typeSection),
|
||||
new Uint8Array(importSection),
|
||||
new Uint8Array(funcSection),
|
||||
new Uint8Array(memorySection),
|
||||
new Uint8Array(exportSection),
|
||||
new Uint8Array(codeSection),
|
||||
]);
|
||||
}
|
||||
|
||||
function funcType(
|
||||
params: readonly number[],
|
||||
results: readonly number[]
|
||||
): number[] {
|
||||
return [0x60, params.length, ...params, results.length, ...results];
|
||||
}
|
||||
|
||||
function importEntry(
|
||||
module: string,
|
||||
field: string,
|
||||
kind: number,
|
||||
typeIndex: number
|
||||
): number[] {
|
||||
return [...encodeName(module), ...encodeName(field), kind, typeIndex];
|
||||
}
|
||||
|
||||
function exportEntry(name: string, kind: number, index: number): number[] {
|
||||
return [...encodeName(name), kind, index];
|
||||
}
|
||||
|
||||
function encodeName(value: string): number[] {
|
||||
const bytes = new TextEncoder().encode(value);
|
||||
return [bytes.length, ...bytes];
|
||||
}
|
||||
|
||||
function section(id: number, payload: readonly number[]): number[] {
|
||||
return [id, payload.length, ...payload];
|
||||
}
|
||||
|
||||
function flatten(arrays: readonly number[][]): number[] {
|
||||
const out: number[] = [];
|
||||
for (const a of arrays) out.push(...a);
|
||||
return out;
|
||||
}
|
||||
|
||||
function concatBytes(parts: readonly Uint8Array[]): Uint8Array {
|
||||
let total = 0;
|
||||
for (const p of parts) total += p.length;
|
||||
const out = new Uint8Array(total);
|
||||
let offset = 0;
|
||||
for (const p of parts) {
|
||||
out.set(p, offset);
|
||||
offset += p.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user