Skip to content

Testing

The high-risk surface - WebGPU and React - can't be fully unit-tested, so Awaken layers several gates: fast Vitest unit tests, a headless Deno WebGPU shader/pipeline check, two Puppeteer boot smokes, and lint/typecheck via ESLint and Husky.

The gate stack

No single test can prove a WebGPU app is correct, so correctness is assembled from layers that each catch a different class of failure.

Each layer runs independently; the ones a machine can run without a GPU also run in CI and on Vercel. Visual correctness - does the shadow actually look right - still needs eyes on a pnpm dev run.

Unit tests (Vitest)

The bulk of the coverage is 1,200+ unit tests across 160+ colocated *.test.ts files run by Vitest in the Node environment. vitest.config.ts is the whole setup:

ts
export default defineConfig({
  test: {
    include: ["packages/**/src/**/*.test.ts", "apps/**/src/**/*.test.ts"],
    environment: "node",
  },
});

Tests live beside the code they cover (e.g. distanceCull.ts + distanceCull.test.ts). The project is TDD-first: each feature lands with headless tests. What's covered directly is everything that's pure logic - math (mat4/quat/vec3), the ECS, the six importers, cull/batch/cache, the colour grade, and command undo/redo round-trips. The Unity-import parity harness asserts statistical invariants against a real asset pack and skips cleanly if the pack isn't present, so a fresh clone still goes green.

bash
pnpm test          # run everything once (headless)
pnpm test:watch    # watch mode

GPU shader & pipeline check (Deno)

Unit tests run in Node, which has no GPU - so the WGSL shaders and render pipelines are validated separately, headlessly, using Deno's experimental WebGPU implementation.

bash
pnpm test:gpu
# → deno run --unstable-webgpu --allow-read scripts/gpu-check.ts

scripts/gpu-check.ts requests a real adapter and device, then:

  1. Compiles every WGSL shader - MESH_WGSL, GRID_WGSL, ID_WGSL, GIZMO_WGSL, SKY_WGSL, SKY_CUBE_WGSL, MASK_WGSL, OUTLINE_WGSL, SHADOW_WGSL, POST_WGSL, the static-batch and position-prepass variants, and the SSAO_WGSL/AOBLUR_WGSL pair, plus each shipped starter-content material composed into the mesh shader (MESH_MATERIAL_WGSL) - reporting compile errors with line/column.
  2. Builds the real pipelines from those modules against the actual bind-group and vertex-buffer layouts, catching layout mismatches a shader compile alone wouldn't.
  3. Runs the cull compute kernel and reads the result back, asserting it matches a CPU reference set.
  4. Probes GPU capabilities it depends on (e.g. indirect-first-instance, and whether @builtin(instance_index) includes firstInstance).

It exits non-zero if any shader fails to compile, any pipeline fails validation, or the cull readback disagrees with the reference - so a broken shader is caught before it ever reaches a browser. This requires a local Deno install and is run on demand rather than in the standard CI gate.

Boot smoke tests (Puppeteer)

Two smokes exercise the boot paths that typecheck and unit tests can't reach - the parts where "it compiles" and "it actually runs in a browser" diverge. Both use puppeteer-core (you supply Chrome via PUPPETEER_EXECUTABLE_PATH / CHROME_PATH).

Editor boot - pnpm smoke

bash
pnpm --filter @awaken/editor build   # smoke needs a prior build
pnpm smoke                          # → node scripts/smoke.mjs

scripts/smoke.mjs serves the built apps/editor/dist, loads /editor in headless Chrome, and fails if the bundle doesn't execute, if React never mounts (it waits for the .app selector), or if any non-WebGPU error or unhandled rejection fires. WebGPU itself is allowlisted - CI machines have no GPU, and the editor already surfaces that gracefully in #gpu-error, so a missing adapter is not a boot failure. This catches boot-path breakage - a bad import, a crash in a top-level effect, a broken lazy chunk - that pure tests miss.

Shipped-game boot - pnpm smoke:game

bash
pnpm build:player-template          # needs a fresh template
pnpm smoke:game                     # → node scripts/smoke-game.mjs

scripts/smoke-game.mjs stamps a minimal scene - a falling RigidBody plus a probe script - into player-template.html exactly the way Export Game does, writes a temp game.html, and opens it via file:// in headless Chrome. It passes only if the probe script runs and physics actually stepped (the body falls and the script writes SMOKE-OK to the HUD). This guards the entire shipped-game path the unit tests can't reach: single-file bundling, inline-scene pickup (the gzip + base64 __AWAKEN_SCENE_BIN__ container), precompiled behaviours picked up from an inline window.__AWAKEN_BEHAVIORS__ script (the mechanism a real export uses - not a blob: import, which Chrome blocks from a file: origin), and the player frame loop - the same path the stale-template rule protects.

Lint & Husky

Linting is ESLint with a flat config (eslint.config.mjs), and it is deliberately a correctness net, not a style enforcer - there is no Prettier. The codebase has a dense, hand-tuned style; blanket reformatting is off the table. ESLint's job is catching real bugs: dead code, accidental globals, unhandled switch cases, and React hook-dependency mistakes. Intentional patterns (non-null assertions, terse one-liners) stay allowed.

bash
pnpm lint        # eslint .
pnpm lint:fix    # eslint . --fix

A Husky pre-commit hook (installed by the prepare: "husky" script) runs lint-staged, which applies eslint --fix to staged *.ts / *.tsx files - so obvious problems are caught before they land, without linting the whole tree on every commit.

Where each gate runs

GateCommandCIVercel deploy
Unit testspnpm test✅ (pnpm test && pnpm build)
Typecheckpnpm -r typecheckvia editor build
Lintpnpm lint-
Editor boot smokepnpm smoke✅ (real Chrome)✗ (no browser)
GPU checkpnpm test:gpuon demandon demand
Game boot smokepnpm smoke:gameon demandon demand

An optional CI workflow runs lint → pnpm -r typecheckpnpm test → editor build → pnpm smoke (with a real Chrome) on every push to main and every PR, adding a merge-blocking check that the Vercel gate - which only gates the deploy, and has no browser - can't provide.

📸 Screenshot - save as img/architecture-testing-gpu-check.png

A terminal running pnpm test:gpu, showing the per-shader ok / per-pipeline pipeline ok lines, cull-compute: readback matches CPU reference ✓, and the closing RESULT: all clean.

See also

Awaken — browser-native WebGPU game engine.