Files
david raistrick 4406fd2045 docs: ENCOUNTER_BUILDER + TESTING guides for LLM session handoff
ENCOUNTER_BUILDER.md: DM interface — entity model (campaign/encounter/
participant), build flow (campaign→chars→encounter→participants), combat
controls (start/next/pause/HP/deathsaves/conditions), player display,
1-list turn order model, storage paths quick-ref.

TESTING.md: test+automation ops — commands, suites (90+24+66+4), layers
(L1 mock vs L2 live backend), types, TDD discipline, replay tool,
analyze-turns.js, audit tools, docker stack (single caddy+node container),
dev servers, storage modes, known RED backlog.

Both aimed at another LLM session picking up repo. DEVELOPMENT.md
cross-refs updated.
2026-07-01 19:16:12 -04:00

7.8 KiB

Testing & Automation — Operating Guide

How to run tests, demos, audits, docker stack, and understand the test layers. For any LLM session picking up this repo.

Test commands

npm run test:all        # shared + server (fast, ~2s) — pre-push gate
npm run shared:test     # pure turn logic (shared/turn.js)
npm run server:test     # ws adapter vs live backend
npm test                # CRA frontend (src/tests/, slow w/ scenario)

Pre-push hook (.githooks/pre-push) runs npm run test:all. Frontend not gated (slow). Skip: git push --no-verify.

Setup hook once per clone:

git config core.hooksPath .githooks

Test suites

Suite Location What Count
Unit (turn logic) shared/tests/ pure nextTurn, rotation, pause-add, dead-skip, reorder, round, invariant, dry 90
Integration (adapter vs backend) server/tests/ ws adapter through live REST/WS 24
Characterization (UI) src/tests/ locks current App.js behavior 66
ESM guard src/tests/StorageEsm.test.js no CJS in adapters 4

Total: ~184. 1 known RED (BUG-4 HideHpToggle, backlog).

Run one file / pattern

npm test --workspace shared -- --testPathPattern=round-rotation
npm run server:test -- tests/ws-reconnect
CI=true npx react-scripts test --watchAll=false --testPathPattern="DisplayView.drag-order"

Frontend uses react-scripts test (CRA). Always set CI=true + --watchAll=false for single runs.

Test layers (important)

Two layers, both required:

  • Layer 1: App vs firebase mock (src/__mocks__/firebase/). Proves adapter call shape. Never exercises ws adapter.
  • Layer 2: ws adapter vs live backend (server/tests/). Proves translation + path identity.

Layer 1 alone misses adapter bugs (path mismatch, no-op players, ws event handler bugs). Layer 2 catches those.

Test types

Type Purpose
Unit pure logic, fast, no I/O. Locks single function behavior.
Integration real backend per test (port 0 = OS picks free). Adapter translation verified.
Characterization render App via mock, assert current UI behavior (buggy or not). NOT desired-state.
Contract same spec run against every storage impl (memory, ws, firebase). Catches adapter drift.
Scenario end-to-end flow through rendered App. Combat.scenario.test.js = 100 rounds, ~240s. Pre-existing crash (BUG-11).

TDD discipline

RED first → fix → GREEN. Never change functional code to pass tests for existing state without test driving it.

  • Find bug → write failing test (RED)
  • Fix code → test passes (GREEN)
  • Log confirmed bug in TODO.md
  • One bug at a time, commit with evidence

Replay tool (demo, NOT unit test)

scripts/replay-combat.js — drives full combat via ws adapter (same contract as App) against live backend. UI updates in real-time if frontend running.

# start backend + frontend first
node scripts/replay-combat.js [rounds] [delayMs]
# defaults: 100 rounds, 200ms/step
# faster: 20 400 = 20 rounds, 400ms each

# against docker stack:
BACKEND_URL=http://127.0.0.1:8080 node scripts/replay-combat.js 20 400

Coverage per round: damage, heal, all 22 conditions, toggleActive, removeParticipant, addParticipant (reinforcements), updateParticipant, pause/resume, reorderParticipants, endEncounter. Revives dead each round to sustain count.

Output → log file, then analyze:

node scripts/replay-combat.js 20 400 > tmp/run.log 2>&1
node scripts/analyze-turns.js tmp/run.log

Exit 0 = clean. Reports skips, double-acts, order shifts.

analyze-turns.js

Parses replay log. Detects:

  • real skips: active participant not acted in a round
  • double-acts: same participant twice in a round
  • order shifts: turnOrderIds changed unexpectedly

Handles [pointer X→Y wrap] events (mutation-driven advance) and [reorder A→before B]. Logs order=[Name:init,...] + parts=[Name:init,...] per turn. Parser blind to DisplayView render (separate concern — FE test covers that).

Round marker: --- round N starting --- (top of loop, post-fix).

Audit tools (NOT unit tests)

tests/audit/ — exploratory, Math.random, non-deterministic. Manual run. NOT jest.

audit-rotation.js

Pure turn.js simulation of replay op sequence. Detects rotation violations. Found BUG-1.

node tests/audit/audit-rotation.js

audit-state.js

Runs pure turn.js combat. Audits 9 invariant classes per round:

  1. rotation integrity (skip/dupe)
  2. HP bounds (0 ≤ hp ≤ max, no NaN)
  3. isActive consistency (dead = inactive)
  4. turnOrder no dup ids
  5. turnOrder ids all active
  6. currentTurn valid + active
  7. deathSave range (0-3, reset on revive)
  8. removeParticipant orphans
  9. undo support
node tests/audit/audit-state.js [rounds]   # default 100

Current state: 0 violations / 100 rounds (post BUG-1/2 fix).

Docker stack

Single container: caddy (front, static + proxy) + node backend (internal :4001).

# build + run (from repo root)
docker compose -f docker/docker-compose.yml up --build -d
# → http://127.0.0.1:8080

# logs
docker compose -f docker/docker-compose.yml logs app --tail 20

# stop
docker compose -f docker/docker-compose.yml down

# rebuild after code change
docker compose -f docker/docker-compose.yml up -d --build

Files:

  • docker/Dockerfile — build FE + BE, runtime caddy+node
  • docker/Caddyfile — proxy /api + /ws to node, static SPA fallback
  • docker/entrypoint.sh — runs node bg + caddy fg
  • docker/docker-compose.yml — one app service, volume for sqlite

Verify docker stack

# REST roundtrip
curl -s -X PUT http://127.0.0.1:8080/api/doc -H 'Content-Type: application/json' \
  -d '{"path":"campaigns/test","data":{"name":"X"}}' >/dev/null
curl -s "http://127.0.0.1:8080/api/doc?path=campaigns/test"

# WS subscribe + push (node one-liner, see scripts)
# Full combat: replay against docker
BACKEND_URL=http://127.0.0.1:8080 node scripts/replay-combat.js 20 400 > tmp/docker.log 2>&1
node scripts/analyze-turns.js tmp/docker.log

Inspect docker sqlite

docker exec docker-app-1 sh -c 'node -e "
const db=require(\"better-sqlite3\")(\"/data/tracker.sqlite\");
const rows=db.prepare(\"SELECT path, substr(data,1,50) as d FROM docs\").all();
console.log(\"count=\"+rows.length);
rows.forEach(r=>console.log(r.path+\" => \"+r.d));
"'

Dev servers (non-docker)

Backend

npm run server:dev        # :4001, db: ./data/tracker.sqlite
# or:
DB_PATH=./data/tracker.sqlite PORT=4001 node server/index.js
curl http://127.0.0.1:4001/health   # → {"ok":true}

Never db in /tmp (wipe risk). Use ./data/ (gitignored) or docker volume.

Frontend (ws mode)

REACT_APP_STORAGE=ws \
REACT_APP_BACKEND_URL=http://127.0.0.1:4001 \
REACT_APP_BACKEND_WS=ws://127.0.0.1:4001/ws \
BROWSER=none PORT=3999 \
npm start

http://127.0.0.1:3999/. Admin /, player /display.

Firebase mode (default): set REACT_APP_FIREBASE_* in .env.local (copy env.example).

Storage modes

STORAGE_MODE = getStorageMode() reads REACT_APP_STORAGE:

  • firebase (default) → real SDK
  • ws → backend (docker/prod)
  • memory → in-process (test seed)

All adapters ESM. Adapter contract: src/storage/contract.js — same spec vs memory/ws/firebase.

Known RED / backlog

  • BUG-4: HideHpToggle RED (setDoc→updateDoc, clobbers activeDisplay)
  • BUG-10: deact+reactivate double-act
  • BUG-11: Combat.scenario test crash
  • BUG-13: reorder cross-pointer semantics
  • BUG-14: addParticipant init-insert post-drag

See TODO.md for full list + status.

Scratch

scratch/ — gitignored throwaway. Repro scripts, exploration, debug. Not committed. Use freely, delete anytime.

Status

See docs/REWORK_PLAN.md for milestones, TODO.md for bugs, docs/DEVELOPMENT.md for setup, docs/GLOSSARY.md for terms, docs/ENCOUNTER_BUILDER.md for DM interface.