Skip to content

Testing ​

Three layers, each with a job. The folder layout is in tests/README.md. What to try in the running game, level by level, and how, is in Testing by hand.

Unit (*.test.ts, vitest project unit) ​

Next to the code. No services, one world at a time. Highlights:

  • packages/sim/src/vehicle.test.ts: the driving envelope (see Driving model).
  • packages/sim/src/car-hits.test.ts: what a car does to a character: kills at speed (a crash hit owned by the driver), knocks down and lets it stand up again when slower, shoves at a crawl, sets a character down beside a car it ended up inside, and the invincible flag (no damage to the character or the car it drives).
  • packages/protocol/src/codec.test.ts: binary round-trips, truncated and unknown frames.
  • packages/net-client/src/predictor.test.ts: prediction against a second "server" world; zero correction when they agree, a snap when they do not, prediction off.
  • apps/server/src/game-server.test.ts: the server core over the memory transport: welcome, input validation, queue cap, snapshot cadence, disconnects.
  • apps/client/src/renderer/*.test.ts: the pure modules (camera math, key map, HUD text, latency simulator, the synthesized sounds).
  • packages/world/src/coplanar.test.ts: no z-fighting in any level: no two faces in one plane, facing the same way, in different colours, within a placed piece or across two (the search itself is coplanarOverlaps in @foi/world/testing).
  • apps/tools/src/kits.test.ts and recipe.test.ts: the kit reader (a glTF fixture with its sidecar, a broken one), the same search over every piece of every kit, and the recipes (kit pieces and blocks placed, exported back as level data with their roads; a block level dressed with the city kit).
  • apps/tools/src/models.test.ts: the chassis hulls read from the vehicle models (one per class, the body and its wheels above the body's floor, within the class's chassis footprint) and the committed vehicle-hulls.json in step with the model.
  • apps/api/src/stores.test.ts: one contract for every store (memory, a JSON file, Postgres) and artifact store (memory, files, S3): documents kept whole, the latest build and versions in order, an artifact once by its hash. The Postgres and S3 runs need a database and a bucket (FOI_TEST_DATABASE_URL, FOI_TEST_S3_ENDPOINT) and are skipped without them; CI's adapters job runs them against Postgres and MinIO containers.
  • packages/world/src/blocks.test.ts and apps/tools/src/*.test.ts: the level pipeline, from the block mapping (every BlockError) to the editor's folder format, the mirror and the gate, with a headless Babylon engine and temporary folders; no editor involved.

Integration (*.integration.test.ts, vitest project integration) ​

Several components in one process, still no external services:

  • packages/sim/src/determinism.integration.test.ts: two worlds, the same 3000-tick input log, equal hashes at every sample. The gate of Technical specs §5.4.
  • apps/server/src/server.integration.test.ts: the server with two bot clients over the memory transport at a 5-tick one-way delay; the driving bot predicts with the real netcode, the other interpolates it. Corrections must stay under 0.5 m.
  • packages/ai/src/chase.integration.test.ts: chasers on the AI level, test-ai-pathfinding: a pursuit reaches a target round a building and holds its approach distance, one shoots its target down and then watches the body without firing, a guard spots, chases, loses a target that gets away, searches and goes home, the obstacle course (the alley cross, the zigzag walls) is crossed without getting stuck; at the wheel, a chaser drives round the corner onto the south road and closes on a car, a tail keeps its gap, one backs out of the spur's dead end, one leaves a wreck and carries on on foot, an aggressive one rams, one on foot takes a free car after a target that drives off, a crew of three shoots out of the windows, a dismounting crew gets out beside a stopped target, and a chaser run over tumbles for its downSeconds and chases on. determinism.integration.test.ts runs two worlds with players, pedestrians and chasers and compares hashSimState and hashAiState; budget.integration.test.ts pins the planning budget with sixteen chasers.
  • apps/worker/src/generate.integration.test.ts: the one that needs a program beside Node: the real Blender, headless, generates test-generated-town from its committed spec (the level must be the committed file, byte for byte), and generate.py's own checks answer the catalogue's spec cases as the schema does. It skips itself where Blender is not found (CI, FOI_BLENDER otherwise); generate.test.ts covers the same path everywhere with a fake Blender.

npm test runs both projects with the coverage gate: 90 % on packages/sim and packages/protocol, 70 % overall. Process entrypoints and the Babylon/DOM/socket wiring are excluded from coverage and covered by the e2e tiers instead (listed in vitest.config.ts).

End-to-end (Playwright, tests/e2e) ​

  • smoke (npm run test:e2e:smoke): builds the client, launches the Electron bundle against a test server, checks the HUD says connected.
  • core (npm run test:e2e), the gate: the renderer in headless Chromium against one game server per level folder under tests/e2e/specs/levels/ (main-riverbend and every test-* level): driving, prediction at 150 ms, two players, and each level's mechanic. specs/levels.spec.ts boots every exported level and proves the Colliders box on the legend's View tab draws the collision wireframe over it; a spec that needs another level starts its own server (tests/e2e/support/server.ts). Each mechanic's fast tests, 20 seconds at most each on CI's runner, run on every pull request.
  • long (npm run test:e2e -- --project=long): the rest in the browser: the missions played through, the galleries, the tours of generated levels and specs/levels.spec.ts; nightly and before a release is published.

CI runs a tier in lanes packed by the tests' recorded seconds (tests/e2e/support/lanes.ts, tests/e2e/timings.json), each lane with the servers of its own files' levels, on the self-hosted runner and GitHub's GPU runners side by side; E2E_ONLY=<folder or file under specs> npm run test:e2e runs part of it locally with its levels' servers only, npm run test:e2e:lanes -- --lanes 3 the whole tier in lanes, each on ports of its own (see Quality gates).

The settings of the runs (E2E_ONLY, E2E_RUN, E2E_CLIENT, E2E_CHROMIUM_ARGS, E2E_RENDERER, a lane's own and the route's), and the adapters' FOI_TEST_* for npm test, are in the root .env.example's tests section.

The specs read window.__poc() instead of the canvas. The tiers start their own servers on ports 7900 and up (one per level folder) and Vite on 5174, so a running npm run dev is not in the way. tests/e2e/TEST-PLAN.md lists every covered behavior; npm run e2e-plan keeps it honest.

Mini test levels: the QA baseline ​

Every mechanic gets a tiny test-* level that exercises it and nothing else, with its own folder of tagged specs; together they are the regression baseline every change runs against, and a failing level names the broken mechanic. Today: test-core-gameplay, test-ai-pathfinding, test-breakables, test-collisions (cars ramming the player's drawn where the prediction meets them, getting out beside a wall, the sprint and the jump), test-curves (the first hillside's curves: a roundabout, a crescent, a tunnel whose roof fades, a serpentine, a dirt rally track), test-generated-town (a grid town built from a level spec by the Blender generator), test-drift-pad (tyre marks, smoke and dust, a landing, hit sparks, riders on two-wheelers), test-mission-basics (the objectives of the first missions), test-mission-logic (the mission language: waves of enemies, counters, while rules, branches), test-mission-verbs (the verbs past v1: give, escort, chase, photograph, wait, escape, destroy, a parallel group, a message read, the Wanted level's rules, the route line, a party; with police), test-police (the Wanted level and the police: crimes, stars, losing them, arrests; with test-locks-garages, the level servers the core tier runs with police, FOI_POLICE=0 elsewhere), test-traffic-junction (ambient traffic: a cross with traffic lights, T junctions without), test-inventory (items: picked up, dropped, used, a crate's panel, a car's trunk, a mission of items), test-interaction (the interaction key on doors, locked and not, and a locker), test-phone (the phone: messages, calls, a contact in person, an app that unlocks a car, a keypad door, a terminal, the map's waypoint), test-locks-garages (locked cars and their keys, picking a lock, vehicle spawners, a garage's respray and storage, a mission of a locked van; with police, like test-police), test-stealth (guards that see and hear, a security camera, darts and knockouts, searching a body, a keycard gate, a mission done unseen), test-compound (a walled compound generated from a level spec: its ways in, its lab, its cameras, its guards on shifts and breaks), test-safe-zones (the griefing controls: the chat's keys, a safe zone where nobody hurts or carjacks anybody, a ghost after coming back) and test-new-vehicles (the taxi, the bus, the ambulance, the fire truck and the garbage truck: V to each, the light bars, the siren, boarding the bus). Riverbend's folder covers the level that ships (its first mission, a car driven off the quay into the river, the roofs, the squares and the docks) and the default level. The planned list (missions, rooms, drift pad, on foot, seats, interaction, inventory, phone, locks, carjacking and garages, mission rules, stealth, non-lethal, traffic, police, new vehicles, day and night and weather, water) and the rules (small, fixed spawns, fast to boot and to draw in software rendering, a contract test per fixture) are in Technical specs §10.2. A block of the game is done only when its mini level and its tests exist.

Load (planned) ​

npm run test:load (tests/load/run.ts, no gate) joins a game server with bots that speak the game's binary protocol over real WebSockets, a room of up to 32 with its traffic and its crowd, and reports what each client receives, the server's step (median, 99th percentile, worst), a round of snapshots, the CPU and the memory; --loss <percent> puts the bots on the acked snapshots with that share of the frames lost each way, --rtc on WebRTC data channels of their own (werift peers, as the server's RTC worker). The numbers are in Performance; the platform API has no load test yet. See Technical specs §10.3.

The netcode probe ​

npm run test:netcode (tests/netcode/, no gate) measures the other cars and people on a client's screen against what the server said: a bot records a server's frames with their arrival times, the client stack replays the trace exactly as the renderer runs it (the reader, the clock, the buffers, the frame's ticks, the clip chooser), and the metrics count the bursts, freezes, jumps and people sliding per area-of-interest tier, each laid at its mechanism, beside the snapshots' inter-arrival and the server's step; a browser run reads the renderer's own frames (window.__remotes()) for the ground truth. Its deterministic reproductions (traces made through the protocol's encoder with the server's tiers, and a real server over the memory transport) are vitest tests in the same folder. See its README.

Writing a test for a tuning change ​

  1. Decide the envelope first ("the handbrake must rotate the car at least 30 % more than steering alone").
  2. Write it in vehicle.test.ts with the helpers there (run, settle).
  3. Tune until green, then drive to confirm the feel.