diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 049a758..2e09cd6 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -11,19 +11,27 @@ TTRPG Initiative Tracker — fork with self-hosted backend. Monorepo via npm wor ``` / - package.json # workspaces root - src/ # React frontend (CRA, existing) - App.js # ~2935 lines, Firebase direct (M2 abstracts this) - server/ # Backend: generic KV doc store (firebase mirror) - index.js # REST (doc/coll/batch) + WS bootstrap - db.js # SQLite docs table, KV ops, broadcast - ws-contract.test.js # adapter vs live backend (Layer 2) - shared/ # Pure logic, no I/O (client + server + tests import) - turn.js # turn-order state machine - turn.characterization.test.js + 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/audit tools (NOT unit tests) + replay-combat.js # live backend demo + audit-rotation.js # exploratory rotation bug-finder + audit-state.js # exploratory invariant bug-finder (9 classes) docs/ - REWORK_PLAN.md # milestone plan - DEVELOPMENT.md # this file + REWORK_PLAN.md + DEVELOPMENT.md # this file + GLOSSARY.md # domain terms (turn vs round, etc) ``` ## Setup @@ -32,24 +40,17 @@ TTRPG Initiative Tracker — fork with self-hosted backend. Monorepo via npm wor 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 -### Frontend (dev server) - -```bash -npm start # http://localhost:3000 -``` - -Still uses Firebase by default. Set `REACT_APP_FIREBASE_*` in `.env.local` (copy `env.example`). - ### Backend (dev) ```bash npm run server:dev # :4001, db: server/data/tracker.sqlite -# or direct with env: -DB_PATH=/tmp/tracker.sqlite PORT=4001 node server/index.js +# or direct: +DB_PATH=./data/tracker.sqlite PORT=4001 node server/index.js ``` Smoke check: @@ -57,44 +58,114 @@ Smoke check: curl http://127.0.0.1:4001/health # -> {"ok":true} ``` -Frontend not yet wired to backend — that is M2 (storage adapter + WS client). +Never put db in `/tmp` (wipe risk). Use `./data/` (gitignored) or docker volume. + +### Frontend (dev server, ws mode) + +```bash +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 -Three commands: +### Commands ```bash -npm run test:all # runs shared/ + server/ suites in sequence -npm run shared:test # turn logic only (shared/ folder) -npm run server:test # backend ws-contract (adapter vs live backend) +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) ``` -What each runs: +### Suites -| Suite | What | Count | -|---|---|---| -| `shared/*.test.js` | turn FSM, pure functions | 39 | -| `server/*.test.js` | REST + combat flow, in-memory db | 7 | +| 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 | -Server tests use `--forceExit` (open WS handles). Tests spin server on random port, in-memory sqlite, tear down per test. +Total: 134 green + 1 validated RED (skipped). -### Local pipeline (pre-push hook) +### Test types -Private repo = no free GitHub Actions. Tests run locally via git hook. +- **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. -`.githooks/pre-push` runs `npm run test:all` before every push. +### Running one file / pattern -Enable on clone (do once): ```bash -git config core.hooksPath .githooks +npm test --workspace shared -- --testPathPattern=round-rotation +CI=true npx react-scripts test --watchAll=false src/tests/App.characterization.test.js ``` -Already configured on this checkout. Skip with: +### Scenario test is slow + +`Combat.scenario.test.js` runs 100 combat rounds through rendered App — 240s timeout by design. Skip when iterating: + ```bash -git push --no-verify +CI=true npx react-scripts test --watchAll=false --testPathIgnorePatterns="Combat.scenario" ``` -Future: when repo goes public, free GH Actions viable. Then add `.github/workflows/ci.yml`. +## Manual tools (NOT tests) + +`scripts/` = exploratory. Math.random, non-deterministic. Used to find bugs, unit tests lock them. + +### replay-combat.js — live demo + +Drives full combat through real backend via ws adapter (same contract as App). Player display live-updates. + +```bash +# start backend + frontend first (see Run) +node scripts/replay-combat.js [rounds] [delayMs] +# defaults: 100 rounds, 200ms/step +``` + +Coverage per round: damage, heal, all 22 conditions, toggleActive (mark inactive/reactivate), removeParticipant, addParticipant (reinforcements), updateParticipant (edit), pause/resume, reorderParticipants, endEncounter. Revives dead each round to sustain 100 rounds. + +### audit-rotation.js — rotation bug-finder + +Pure turn.js simulation of replay op sequence. Detects rotation violations (skip/dupe per round). Pinpointed BUG-1 (addParticipant + pause corrupts rotation). + +```bash +node scripts/audit-rotation.js +``` + +Bisect mode: comment/uncomment op blocks to isolate which combo triggers. + +### audit-state.js — expanded invariant bug-finder + +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 + +```bash +node scripts/audit-state.js [rounds] +``` + +100-round run: 128 violations, all BUG-1/BUG-2 family (4 symptom faces). Clean: HP, isActive, deathSave, conditions, removal. + +See `TODO.md` for known bugs. ## Build @@ -110,33 +181,56 @@ docker run -p 8080:80 --rm ttrpg-initiative-tracker Full-stack docker-compose arrives in M5. -## Backend architecture +## Storage architecture -Server-authoritative. Kills client-side last-write-wins races (root cause of skip bug). +### 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 | | - |-- POST /api/.../nextTurn ----->| action - | | store.nextTurn(): - | | shared.nextTurn(enc) -> patch - | | db tx: apply patch - | | addLog - | | broadcast(change) - |<---- WS {change} --------------| push to all subscribers + |-- 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 ``` -- **SQLite** owns truth. Single writer (server). WAL mode. -- **shared/turn.js** = pure logic, ported verbatim from `App.js`. Bugs preserved for M3 characterization, fixed in M4. -- **WS** = real-time push (replaces Firebase `onSnapshot`). Client subscribes to a key (`'campaigns'`, `'encounter:id'`, `'activeDisplay'`...), server pushes on change. -- **Actions not results.** Client sends "do X", server computes X, persists, broadcasts. No client-side state mutation. +### Path normalization -## Storage backend choice +App passes firebase-prefixed paths (`artifacts/{APP_ID}/public/data/campaigns/...`). Adapter `norm()` strips prefix → bare canonical (`campaigns/...`). All impls share identity (contract test). -Browser sandbox cannot touch filesystem. Cross-device (DM + tablet + player view) requires a real backend owning the DB file. SQLite = single file, docker volume, trivial backup. Postgres deferred until public multiuser exposure. +### 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: +```bash +git push --no-verify +``` + +Already configured on this checkout after `git config core.hooksPath .githooks`. ## Status @@ -144,14 +238,14 @@ Browser sandbox cannot touch filesystem. Cross-device (DM + tablet + player view |---|---| | 0 repo/branch | ✅ done | | 1 backend + tests | ✅ done | -| 2 frontend WS adapter | ⬜ next | -| 3 characterization tests | ⬜ | -| 4 skip fix + manual override | ⬜ | +| 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. +See `docs/REWORK_PLAN.md` for full plan, `TODO.md` for known bugs. ## Git diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..1b54bf2 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,52 @@ +# scripts/ + +Manual tools. NOT unit tests. Math.random, non-deterministic. + +Used to FIND bugs. Unit tests (in `*/tests/`) LOCK them. + +## replay-combat.js + +Drives full combat through live backend via ws adapter (same contract as App). +Player display live-updates. Use to watch UI react to state changes. + +```bash +# start backend + frontend first (see docs/DEVELOPMENT.md) +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-rotation.js + +Pure turn.js simulation of replay op sequence. Detects rotation violations +(skip/dupe per round). Pinpointed BUG-1 (addParticipant + pause corrupts). + +```bash +node scripts/audit-rotation.js +``` + +Bisect: comment/uncomment op blocks to isolate triggering combo. + +## audit-state.js + +Expanded invariant bug-finder. 9 check classes per round: + +1. rotation integrity +2. HP bounds +3. isActive consistency +4. turnOrder no dup ids +5. turnOrder ids all active +6. currentTurn valid + active +7. deathSave range + reset on revive +8. removeParticipant orphans +9. undo support + +```bash +node scripts/audit-state.js [rounds] # default 100 +``` + +See TODO.md for bugs found.