# 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` ```js 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/); }); ``` ```bash run node --test examples/rust/hello/test.mjs 2>&1 | grep -E '^ℹ (pass|fail)' ``` ```output ℹ 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` ```js const respond = (messages) => (call) => (call.subscribe ? messages : { ok: true, output: { handle: "alice.test", did } }); ``` `examples/rust/echo/test.mjs` ```js 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` ```js const start = (fields) => ({ did, now, ...fields, state: null, event: { type: "start" } }); ``` `examples/rust/statusphere/test.mjs` ```js 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. ```bash run=gate 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](../browser/run-modules.md#test-it). ## Everything ```bash run=gate 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](calls.md) | Next: [Publish a module](publish.md)