Skip to main content

Testing

Every public part of Moonspell has its own checks. Run the ones for what you changed before opening a pull request (Contributing has the short list).

Server​

npm run typecheck # tsc over the app, Nova (agent/) and the workflows
npm run lint # eslint (next core-web-vitals + typescript)
npm test # node --test over src/**/*.test.ts and edge/src/**/*.test.ts
npm run build # next build, with eve and the Workflow SDK compiled in

Builds and tests need no environment variables: the database and SDK clients are created lazily.

The unit tests sit next to the code they test, in src/lib/:

TestCovers
client.test.tsEach app gets its own export of a version, and the other platforms' URLs never leave the server
web-play.test.tsThe page that runs a web export, and what may reach it
community-drafts.test.tsHow the run script picks a draft's branch
judge.test.tsReading the judge's verdict
credit-prices.test.tsCredit arithmetic: model calls, sandbox time, asset quotes and refunds
sign-in.test.tsGoogle and Apple sign-in rules
models.test.ts, sprite-sheet.test.tsThe builder's 3D model and sprite requests, turning generated meshes to face forward, and cutting generated images into sprite cells
playtest.test.ts, gameplay-clip.test.tsParsing playtest.json, and picking a feed clip from a recording

edge/src/meter.test.ts runs with them and covers reading Anthropic's streamed usage.

An end-to-end run needs the whole hosted environment, including the private bundling CLI in the sandbox snapshot, so it cannot be run from a public clone. See Development setup.

Edge Worker​

cd edge
npm install
npm run typecheck
printf 'REALTIME_SECRET=any-local-test-secret\n' > .dev.vars
npx wrangler dev --local --var ORIGIN_URL:http://localhost:3000 # with npm run dev on :3000

Then, in another terminal:

curl -s localhost:8787/rt/health # {"ok":true,"realtime":true}
curl -si localhost:8787/.well-known/apple-app-site-association # 200, application/json, no location header
curl -si localhost:8787/g/00000000-0000-0000-0000-000000000000 # proxied to Next (not found for an unknown build)

Publish and subscribe to one run with tokens signed by the same secret:

mint() { REALTIME_SECRET=any-local-test-secret node -e '
const c=require("node:crypto"); const [run,scope]=process.argv.slice(1);
const p=Buffer.from(JSON.stringify({run,scope,exp:Math.floor(Date.now()/1000)+600})).toString("base64url");
console.log("crt1."+p+"."+c.createHmac("sha256",process.env.REALTIME_SECRET).update(p).digest("base64url"))' "$1" "$2"; }

curl -s -X POST localhost:8787/rt/runs/test-run/events \
-H "Authorization: Bearer $(mint test-run publish)" -H 'Content-Type: application/json' \
-d '{"events":[{"type":"progress","at":"2026-01-01T12:00:00Z","text":"hello"}]}' # {"ok":true,"accepted":1,...}

npx wscat -c "ws://localhost:8787/rt/runs/test-run?token=$(mint test-run subscribe)" # first message: the snapshot

Also worth checking: a wrong token gets 401 on both routes, 101 events get 413, and an event without type gets 400. The protocol is described in Live progress.

Engine and game template​

From templates/crescent-game/:

npm run typecheck # tsc --noEmit
npm test # unit tests in src/engine/**/*.test.ts
npm run export:web # a static web build in dist/

The unit tests cover the pure parts of the engine: physics, paths, zones, the level pieces, the character controller, cameras, bots and the nav grid, the combat kit, saves, the turns kit, game feel and the modules. A change that touches how a game looks or plays is also checked by playing it in a browser with npm run web; see Web. The same commands check any game repo made from the template.

The worker in a hosted run checks itself the same way: a typecheck and an export on every checkpoint (see Nova and the worker).

Game backends​

The backend scaffold that games copy (templates/crescent-game/.claude/skills/realtime-backend/scaffold/backend/) has its own smoke test:

cd backend
npm ci
npm run typecheck
npm run db:local
npm run dev & # wrangler dev --local
node test/smoke.mjs # WebSocket and HTTP checks against the local Worker
node test/smoke.mjs <url> --read-only # against a deployed backend

See Backend overview.

Contracts, specs and docs​

WhatCommand
Generated contract files are currentnpm run contracts -- --check
What each app is missingnpm run parity
These docs build, links includednpm --prefix docs-site run build

Playtests and feed videos​

A game's video on the feed is the recording of a playtest it passed. Each game's builder writes a playtest.json (the template's playtest skill): steps such as press a label, hold, tap or drag a point, expect a label, and a {"mark": "playing"} step after which the recording counts. src/lib/playtest.ts reads it, and src/lib/gameplay-clip.ts picks the liveliest stretch after the mark. The playtests themselves run against the private iOS app, with npm run playtest, so they cannot be run from a public clone. A crash, a missing label or a state that never changes fails the test; a failed game keeps its cover on the feed, and game_playtests records why.