Test a module
A module does no I/O, so a test is the real host loop with your answers in place of the network.
rust/src/harness.mjs loads a build and drives it with runModule.
Answer each call
load(wasm, moduleJson) loads a build; given its module.json, the run is held to that file's calls
and anyRecords, as a host holds it, so a call outside them fails the test. handle(entry, input, { respond }) runs the entry; respond(call) returns { ok, output } or
{ ok: false, error }. It resolves to the run's { result, error, calls, events }.
examples/rust/hello/test.mjs
import assert from "node:assert/strict";
import { test } from "node:test";
import { did, load } from "../../../rust/src/harness.mjs";
const { handle } = await load(new URL("../../../target/wasm32-unknown-unknown/release/hello.wasm", import.meta.url), new URL("./module.json", import.meta.url));
// handle() runs the module under the real host loop; respond answers each call.
test("count lists records, then counts them", async () => {
const ran = await handle("count", { collection: "xyz.statusphere.status" }, { respond: () => ({ ok: true, output: { records: [{}, {}] } }) });
assert.equal(ran.calls[0].nsid, "com.atproto.repo.listRecords");
assert.deepEqual(ran.result, { collection: "xyz.statusphere.status", records: 2 });
});
test("a failed call is the run's error", async () => {
const ran = await handle("count", { collection: "xyz.statusphere.status" }, { respond: () => ({ ok: false, error: "RepoNotFound" }) });
assert.match(ran.error, /RepoNotFound/);
});
node --test examples/rust/hello/test.mjs 2>&1 | grep -E '^ℹ (pass|fail)'
ℹ pass 2
ℹ fail 0
did and now are fixed (harness.mjs exports them), so outputs are stable.
Subscriptions and input
For a call with subscribe: true, respond returns its messages (an array or an async iterable);
the subscription closes after the last. inputs feeds Event::Input, and sends in the result lists
what the module wrote up its subscriptions.
examples/rust/echo/test.mjs
const respond = (messages) => (call) => (call.subscribe ? messages : { ok: true, output: { handle: "alice.test", did } });
examples/rust/echo/test.mjs
test("inputs are echoed up the stream; the timer ends a quiet run", async () => {
async function* quiet() {
await sleep(1200);
}
async function* inputs() {
yield { $type: "#ping", n: 1 };
}
const ran = await handle("watch", { service, seconds: 1 }, { respond: respond(quiet()), inputs: inputs() });
assert.deepEqual(ran.sends, [{ nsid: SUB, data: { $type: "#ping", n: 1 } }]);
assert.equal(ran.result.stopped, "time");
});
One event at a time
run(entry, input) calls an entry once with the exact JSON the host would send, for checking one step:
examples/rust/statusphere/test.mjs
const start = (fields) => ({ did, now, ...fields, state: null, event: { type: "start" } });
examples/rust/statusphere/test.mjs
test("module has no imports and exports the ABI", () => {
assert.equal(WebAssembly.Module.imports(module).length, 0);
const names = WebAssembly.Module.exports(module).map((e) => e.name);
for (const name of ["memory", "alloc", ...ENTRIES]) assert.ok(names.includes(name), name);
});
Both hosts
The same tests run against the Rust host: swap.mjs points runModule at it.
node --import ./rust/host/swap.mjs --test examples/rust/*/test.mjs
A page
Test a page's cubit with the real modules and stubbed OAuth: Run modules in a page.
Everything
cargo wasm && cargo host && cargo loopback && cargo host-test
node --test examples/rust/*/test.mjs examples/web/*/*.test.mjs examples/node-hono/test.mjs javascript/src/*/*.test.mjs rust/host/*.test.mjs docs/docs.test.mjs
Previous: Calls | Next: Publish a module