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

8.5 KiB

Development

TTRPG Initiative Tracker — fork with self-hosted backend. Monorepo via npm workspaces.

Prerequisites

  • Node.js 22+
  • npm 10+

Layout

/
  package.json              # workspaces root
  src/                      # React frontend (CRA)
    App.js                  # main app (~2900 lines)
    storage/                # adapter layer (firebase/ws/memory + contract)
    __mocks__/firebase/     # firebase SDK mock (Layer 1 tests)
    tests/                  # frontend tests
  server/                   # Backend: generic KV doc store (firebase mirror)
    index.js                # REST (doc/coll/batch) + WS bootstrap
    db.js                   # SQLite docs table, KV ops, broadcast
    tests/                  # backend + adapter-vs-live tests
  shared/                   # Pure logic, no I/O (client + server + tests import)
    turn.js                 # turn-order state machine
    tests/                  # turn logic tests
  scripts/                  # manual demo tool (NOT test)
    replay-combat.js        # live backend demo
  tests/
    audit/                  # exploratory bug-finders (manual, Math.random)
      audit-rotation.js     # rotation invariant
      audit-state.js        # 9 invariant classes
  scratch/                  # gitignored: throwaway repro/exploration
  docs/
    REWORK_PLAN.md
    DEVELOPMENT.md          # this file
    GLOSSARY.md             # domain terms (turn vs round, etc)
    ENCOUNTER_BUILDER.md    # DM interface guide
    TESTING.md              # test + automation ops

Setup

git clone git@github.com:keen99/ttrpg-initiative-tracker.git
cd ttrpg-initiative-tracker
npm install
git config core.hooksPath .githooks   # enable pre-push test gate

Run

Backend (dev)

npm run server:dev        # :4001, db: server/data/tracker.sqlite
# or direct:
DB_PATH=./data/tracker.sqlite PORT=4001 node server/index.js

Smoke check:

curl http://127.0.0.1:4001/health   # -> {"ok":true}

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

Frontend (dev server, 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

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

Firebase mode (default, upstream): set REACT_APP_FIREBASE_* in .env.local (copy env.example). STORAGE_MODE=firebase falls through to real SDK.

Test

Commands

npm run test:all          # shared + server (fast, no frontend)
npm run shared:test       # pure turn logic
npm run server:test       # adapter vs live backend
npm test                  # CRA frontend (src/tests/, slow with scenario)

Suites

Suite Location What Count
Unit (turn logic) shared/tests/ pure nextTurn, rotation, pause-add 50 (1 skip)
Integration (adapter vs backend) server/tests/ ws adapter through live REST/WS 23
Characterization (UI) src/tests/ locks current App.js behavior 62
Scenario src/tests/Combat.scenario.test.js 100-round full combat (240s) 289 phases

Total: 134 green + 1 validated RED (skipped).

Test types

  • Unit = pure logic, fast, no I/O. Locks behavior of single functions.
  • Integration = real backend per test, adapter translation verified.
  • Characterization = render App via mock, assert current (buggy or not) UI behavior. Not desired-state.
  • Scenario = end-to-end flow through rendered App, asserts full sequence completes.
  • Contract = same spec run against every storage impl (memory, ws, firebase). Catches adapter drift.

Running one file / pattern

npm test --workspace shared -- --testPathPattern=round-rotation
CI=true npx react-scripts test --watchAll=false src/tests/App.characterization.test.js

Scenario test is slow

Combat.scenario.test.js runs 100 combat rounds through rendered App — 240s timeout by design. Skip when iterating:

CI=true npx react-scripts test --watchAll=false --testPathIgnorePatterns="Combat.scenario"

Demo tool (NOT test)

scripts/replay-combat.js = live backend demo. Watch UI react to state changes.

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

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

Audit tools (NOT unit tests)

tests/audit/ = exploratory, Math.random, non-deterministic. Manual run. Unit tests ({shared,server,src}/tests/) lock known bugs deterministically.

audit-rotation.js

Pure turn.js simulation of replay op sequence. Detects rotation violations (skip/dupe per round). Found BUG-1 (addParticipant + pause corrupts rotation).

node tests/audit/audit-rotation.js

Bisect: comment/uncomment op blocks to isolate triggering combo.

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 ≤ saves ≤ 3, reset on revive)
  8. removeParticipant orphans
  9. undo support
node tests/audit/audit-state.js [rounds]   # default 100

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

See TODO.md for known bugs.

Scratch

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

Build

npm run build             # CRA production build -> build/

Docker build (existing, frontend-only):

docker build -t ttrpg-initiative-tracker .
docker run -p 8080:80 --rm ttrpg-initiative-tracker

Full-stack docker-compose arrives in M5.

Storage architecture

Generic KV doc store

Backend = firebase mirror. Single docs table: path (PK), parent, data (JSON), updated_at. Opaque JSON at arbitrary path strings. No shape-specific endpoints. App logic stays client-side.

Client (browser)                  Server
  |                                 |
  |-- storage.setDoc(path,data) -->|  REST PUT /api/doc
  |<---- 200 ----------------------|
  |                                 |
  |-- storage.subscribeDoc(path) -->|  WS subscribe
  |<---- WS {initial} --------------|  immediate value
  |       ...                       |
  |<---- WS {change} --------------|  on any write to path
  |                                 |
  Display / tablet                 |
  |<---- WS {change} --------------|  same push

Path normalization

App passes firebase-prefixed paths (artifacts/{APP_ID}/public/data/campaigns/...). Adapter norm() strips prefix → bare canonical (campaigns/...). All impls share identity (contract test).

STORAGE_MODE flow

getStorageMode() reads REACT_APP_STORAGE env (default firebase).

  • firebase → real SDK init
  • ws/memory → stub auth + db sentinel, route via storage.* adapter

Test layers

  • Layer 1: App vs firebase mock. Proves adapter call shape. Never exercises ws adapter.
  • Layer 2: ws adapter vs live backend. Proves translation + path identity.

Both required — Layer 1 alone misses adapter bugs (path mismatch, no-op players, ws.on EventEmitter vs browser handlers).

Local pipeline (pre-push hook)

Private repo = no free GitHub Actions. Tests run locally via git hook.

.githooks/pre-push runs npm run test:all (shared + server, fast). Frontend tests not gated (slow).

Skip:

git push --no-verify

Already configured on this checkout after git config core.hooksPath .githooks.

Status

Milestone State
0 repo/branch done
1 backend + tests done
2 frontend WS adapter done
3 characterization tests done (134 green)
4 skip fix + manual override next
5 docker compose
6 undo rework
7 playwright e2e deferred

See docs/REWORK_PLAN.md for full plan, TODO.md for known bugs.

Git

  • origin = github.com:keen99/ttrpg-initiative-tracker (this fork)
  • upstream = code.draft13.com/robert/ttrpg-initiative-tracker (friend's Gitea, read-only)
  • work branch: rework-backend (off main)
git fetch upstream         # pull friend's changes
git merge upstream/main    # rebase our branch onto his