Test a tool¶
This page shows you how to test a tool with the repository's harness, which runs the built script the way the sandbox does. It is for tool authors. At the end your tool's commands, events, bus messages and transmit requests are covered by tests that run in CI.
Before you start¶
- A clone of apachler/aprscaching-tools with its dependencies
installed (
corepack enable && pnpm install --frozen-lockfile --ignore-scripts). - Your tool in
tools/<name>/, with a builttool.js(Build with the shared libraries).
The harness¶
test/harness.mjs is a stand-in for the sandbox. loadTool(name) reads tools/<name>/tool.json, runs its script
as the body of a function of register, ipc and tool, with the API of tool API 1.0 and the same permission
checks, and records what the tool asks of the app. Values cross the boundary through structuredClone, as
postMessage copies them, and bus payloads travel as JSON, so a Date arrives as its string, as in the app. The
harness keeps the app's rules where a test could otherwise pass wrongly:
- The bus.
provideis refused for a service another tool holds, and for names that start withsession.orhost.orlink.; a tool may not publish those topics either, exceptlink.ping.request. A call tosession.scriptwithouttxis refused with the app's reason (The tool bus). - Transmit format.
requestTx()holds what the app would not send, such as a status over 62 characters or one that starts with a grid locator, and resolvesfalse(The sandbox API). - Message size. A colour-rule set over 64 KB of JSON is dropped and logged, as the app drops a message over its budget (Limits and budgets).
| Call | Does |
|---|---|
loadTool(name, opts?) |
Loads the tool. opts.permissions overrides the manifest's grants; opts.bus shares a bus between tools; opts.tx(info) and opts.beacon(spec) answer transmit and beacon requests (granted by default) and may throw, as the app does when its gate is closed. |
createBus() |
A tool bus several tools can share. bus.emit(topic, data, from) publishes as any sender, such as "(host)". |
t.run(word, args?, { remote? }) |
Runs a command and resolves its lines; as a remote station, only a remote tool's remote commands answer (null otherwise). |
t.decode(id, input) |
Runs a decoder. |
t.dispatch(event, payload?, reply?) |
Raises an event; reply stands for the connected session's reply. Resolves once every handler settled. |
t.panel, t.map |
The tool's panel and map layer. |
t.colour(line) |
The colour token the monitor draws for { src, dst, text }, from the tool's colour rules. |
t.state |
What the tool did: logs, txs (sent), txHeld (held for their format), beacons, colourRules and colourPublishes (how often it published them), commands, decoders. |
t.commands() |
The registered command words. |
Steps¶
-
Add
test/<name>.test.mjs:// SPDX-License-Identifier: MIT import { describe, expect, it } from "vitest"; import { createBus, loadTool } from "./harness.mjs"; describe("my-station-log", () => { it("lists a station Station DB announces", async () => { const bus = createBus(); const t = loadTool("my-station-log", { bus }); expect(await t.run("seen")).toEqual(["No stations heard yet."]); bus.emit("station.seen", { call: "OE8APR-9", type: "user", source: "RF" }, "station-db"); expect(await t.run("seen")).toEqual(["OE8APR-9 user RF"]); }); }); -
Run it:
What to test¶
- Each command, its usage line and a bad input.
-
Remote use. A command meant for connected stations answers with
{ remote: true }; one that is the operator's alone answersnull: -
Events. Raise them with
dispatch, including the minute tick. Auto-status stays idle until/autostatussets its interval, so the test runs that first: -
A closed gate. Make the app refuse, and check the tool says so:
-
Missing permissions. Load the tool with fewer grants and check it degrades instead of throwing at load:
-
The bus. Share one
createBus()between your tool and the tool it talks to, or publish as"(host)"for what the app sends.
The repository checks¶
pnpm test also runs test/repository.test.mjs, which checks every tool folder the way the review does:
- a manifest the app accepts, with
"api": "1.0"and"entry": "tool.js"; a tool built here lives in a folder of its manifest's name; - an entry in
registry.jsonwith the manifest's name, title, author, version and description; - a README with a row for every permission and a Licence section, and an
SPDX-License-Identifier: MITfirst line in the script and its source.
A new tool has no registry entry until the maintainer adds it. Until then the check prints a warning and passes; with
STRICT=1, which CI sets on main, on pull requests into it and on release tags, it fails.
Check that it worked¶
CI runs the same three on every pull request. verify.mjs lists a tool that is not yet signed as UNSIG without
failing; it fails only with --strict, which main runs.
Next¶
- Sign a tool: sign what you tested.
- Limits and budgets: the limits a test should stay within.