~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
113 lines · 3.9 KB Raw History
# 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)