# Testing

> The checks for the Moonspell server, the edge Worker, the engine and template, game backends, the contracts and these docs, and how playtests decide which games get a feed video.

Every public part of Moonspell has its own checks. Run the ones for what you changed before opening a pull request ([Contributing](https://docs.moonspell.app/contributing.md) has the short list).

## Server

```bash
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/`:

| Test | Covers |
|---|---|
| `client.test.ts` | Each app gets its own export of a version, and the other platforms' URLs never leave the server |
| `web-play.test.ts` | The page that runs a web export, and what may reach it |
| `community-drafts.test.ts` | How the run script picks a draft's branch |
| `judge.test.ts` | Reading the judge's verdict |
| `credit-prices.test.ts` | Credit arithmetic: model calls, sandbox time, asset quotes and refunds |
| `sign-in.test.ts` | Google and Apple sign-in rules |
| `models.test.ts`, `sprite-sheet.test.ts` | The 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.ts` | Parsing `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](https://docs.moonspell.app/moonspell/dev-setup.md#what-works-from-a-public-clone).

## Edge Worker

```bash
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:

```bash
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:

```bash
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](https://docs.moonspell.app/moonspell/realtime.md).

## Engine and game template

From `templates/crescent-game/`:

```bash
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](https://docs.moonspell.app/platforms/web.md). 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](https://docs.moonspell.app/moonspell/nova-and-the-worker.md#checkpoints-a-run-always-lands-a-version)).

## Game backends

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

```bash
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](https://docs.moonspell.app/backend/overview.md).

## Contracts, specs and docs

| What | Command |
|---|---|
| Generated contract files are current | `npm run contracts -- --check` |
| What each app is missing | `npm run parity` |
| These docs build, links included | `npm --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.
