Testing
@firemoot/test boots a real Firemoot for your test suite: the actual Docker image plus a Postgres, on a private Testcontainers network, healthy in seconds. No mock server, no fixture drift - if your test passes, it passed against the thing you deploy.
Booting an instance
import { startFiremoot, type FiremootInstance } from "@firemoot/test";
let fm: FiremootInstance;
beforeAll(async () => {
fm = await startFiremoot();
}, 180_000);
afterAll(async () => {
await fm.stop();
});startFiremoot() returns the mapped baseUrl and wsUrl, a server-trusted server (a FiremootServer) and rest client, a createToken(userId) helper, seed(), and stop(). Ports are randomly mapped, so suites run in parallel safely, and the containers are reaped on stop() - or by Testcontainers' Ryuk if your process dies first.
It expects firemoot:latest to exist locally; build it once with sbt "server/Docker/publishLocal" (or pass { image } to point at a published tag). The options are image, postgresImage, apiKey, apiSecret and startupTimeoutMs.
Seeding a fixture
seed() applies a spec in dependency order - users, then channels with their members, then messages - over the server credential:
await fm.seed({
users: [{ id: "alice" }, { id: "bob" }],
channels: [
{
type: "messaging",
id: "general",
createdBy: "alice",
members: [{ userId: "bob", role: "member" }],
},
],
messages: [{ type: "messaging", id: "general", userId: "alice", text: "hello" }],
});Then drive it exactly as a browser would:
const token = await fm.createToken("alice");
const client = new FiremootClient({
baseUrl: fm.baseUrl,
wsUrl: fm.wsUrl,
userId: "alice",
token,
});
await client.connect();
const channel = client.channel("messaging", "general");
await channel.watch();
await channel.sendMessage({ text: "hello from the test" });Chaos: severing the connection
TcpProxy is a pass-through TCP proxy you can put in front of the instance and then break on demand. Because it forwards raw bytes it tunnels the WebSocket upgrade transparently, which makes it a genuine mid-stream network drop rather than a polite close:
import { TcpProxy } from "@firemoot/test";
const proxy = new TcpProxy("127.0.0.1", port);
const proxyPort = await proxy.start();
// ...connect the client through ws://127.0.0.1:${proxyPort}/v1/ws, send traffic...
proxy.drop(); // every live socket dies; the listener stays up so reconnects landdrop() cuts the live connections and lets reconnects straight back through. setAccepting(false) also refuses new ones, which gives you a hard outage window to test backoff against; setAccepting(true) ends it.
This is how the gapless-resume guarantee is tested: send while the socket is severed, let the client reconnect and re-subscribe from its last seq, and assert that every message arrives exactly once. See the realtime protocol for the mechanism it exercises.
A note on timers
Do not assert against the wall clock that a timer has or has not yet fired - it flakes on loaded CI runners. Assert in the generous direction only ("wait, then check the thing arrived").