david raistrick
6630fd9158
M3: add combat replay script + TODO for M4 skip/dead fixes
...
scripts/replay-combat.js: drives full combat via live backend REST, computes
turns through shared/turn.js. Player display (subscribed via WS) live-updates.
Usage: node scripts/replay-combat.js [rounds] [delayMs]
TODO.md: tracks M4 work.
- Dead participants must NOT be skipped (still occupy initiative slot,
death saves resolve on their turn). Saw in game Saturday.
- JUMP_TURN_TO manual turn override.
2026-06-29 14:36:02 -04:00
david raistrick
1d4c561c09
M3: add full 100-round combat scenario test
...
Drives same UI buttons a DM clicks. Exercises:
- campaign + character roster (3 PCs)
- monsters, NPC, add-all, hidden hp toggle
- start combat, 100 rounds of nextTurn
- damage/heal, conditions (stunned), toggle-active
- edit initiative, edit name, remove participant
- pause/resume with reinforcement mid-combat
- damage-to-0 + death saves (x3) + revive
- end combat + confirm
Harness: each phase wrapped in recordAsync try/catch. Failures collected,
reported at end, do NOT abort run. Found no app bugs; selector bugs in harness
fixed (char form ids vs placeholders, encounter row scope via Init: marker,
conditions panel async render, dead-participant damage skip as expected game
state, end-encounter modal title 'End Encounter?' not 'End Combat').
289/289 phases ok. 58 other frontend + 39 shared + 19 ws-contract = 116 total green.
2026-06-29 13:51:32 -04:00
david raistrick
84a8b78acd
M2: refactor DisplayView to storage adapter (red test first)
...
DisplayView missed in original M2 refactor — raw onSnapshot(doc(db,path))
survived. In ws/memory mode db is a stub sentinel, so raw SDK calls crash
('Expected first argument to collection() to be a CollectionReference...').
Reported by human testing player display after start combat.
TDD:
1. RED: DisplayView.characterization.test.js asserts adapter.subscribeDoc
called for campaign + encounter + activeDisplay paths. Adapter recorder
(getAdapterCalls/resetAdapterCalls in firebase.js) instruments subscribe
calls — catches raw-SDK bypass that firebase mock alone cannot (mock db
satisfies raw onSnapshot, hiding the bug).
2. Fix: 2 raw onSnapshot sites in DisplayView -> storage.subscribeDoc.
3. GREEN: 2 new tests pass, 116 total green.
Audit confirmed DisplayView was the ONLY remaining raw SDK site in App.js.
2026-06-29 13:13:46 -04:00
david raistrick
52866784b2
M2: replace shape-specific backend with generic KV doc store (firebase mirror)
...
Backend was endpoint-mapper (POST /api/campaigns ignores path id, etc). Adapter
translation layer brittle, untested, lost doc identity. Generic contract (Layer 2)
test caught 15 bugs immediately.
Rewrite to firebase-mirror KV model:
- server/db.js: docs table (path PK, parent, data JSON). getDoc/setDoc/updateDoc/
deleteDoc/getCollection/batchWrite. parent = path prefix for collection queries.
- server/index.js: generic REST (GET/PUT/PATCH/DELETE /api/doc, GET /api/collection,
POST /api/collection addDoc, POST /api/batch). WS subscribe by path (doc|collection),
broadcast to doc subs at changed path + collection subs at parent path.
- src/storage/ws.js: thin passthrough adapter. norm() strips firebase prefix.
initial value via REST (independent of WS connect), subsequent changes via WS.
- shared/turn.js kept (M4 use). server/handlers.js + server.test.js removed (logic
now in App, backend is dumb KV).
- src/storage/contract.js: flush() bumped to 50ms (WS roundtrip > setTimeout(0)).
Layer 2 test (server/ws-contract.test.js): spins fresh backend per test, runs same
storage contract spec against createWsStorage. Catches adapter translation bugs
that firebase-mock Layer 1 tests cannot.
nanoid v5 ESM breaks jest CJS -> replaced with crypto.randomUUID (Node builtin).
Tests: 114 green (39 shared + 19 ws-contract + 56 frontend).
2026-06-29 13:00:24 -04:00
david raistrick
35cd1581e3
Reapply "M3: stub db sentinel in ws/memory mode so legacy guards pass"
...
This reverts commit d1ee69a70a .
2026-06-29 12:36:16 -04:00
david raistrick
ed67535b1f
Reapply "M2: fix ws adapter for browser WebSocket + firebase path prefix"
...
This reverts commit 74b4c2c42d .
2026-06-29 12:36:16 -04:00
david raistrick
17245dfa1b
Reapply "M2: gate storage init on STORAGE mode (firebase vs ws/memory)"
...
This reverts commit e843acdf8a .
2026-06-29 12:36:16 -04:00
david raistrick
e843acdf8a
Revert "M2: gate storage init on STORAGE mode (firebase vs ws/memory)"
...
This reverts commit 3e84f28325 .
2026-06-29 12:03:57 -04:00
david raistrick
74b4c2c42d
Revert "M2: fix ws adapter for browser WebSocket + firebase path prefix"
...
This reverts commit e743d40e8d .
2026-06-29 12:03:57 -04:00
david raistrick
d1ee69a70a
Revert "M3: stub db sentinel in ws/memory mode so legacy guards pass"
...
This reverts commit a5a4df78f0 .
2026-06-29 12:03:57 -04:00
david raistrick
a5a4df78f0
M3: stub db sentinel in ws/memory mode so legacy guards pass
...
ws/memory storage mode never set the module-level `db` variable. 24 handlers
guarded with `if (!db) return` early-exited, silently dropping all writes
(create campaign, add encounter, participant CRUD, combat, logs).
db stays a truthy sentinel object { __localStub: true } in non-firebase mode.
All real reads/writes route through storage.*; db only used by guards.
56 frontend tests green. Verified via headed browser: create campaign flow
works end-to-end (modal closes, campaign appears via WS realtime push).
2026-06-29 11:30:08 -04:00
david raistrick
b095e37bfe
M3: remove project dev-serve skill (moving to generic global skill)
2026-06-29 11:25:13 -04:00
david raistrick
54e8df9ffa
M3: add dev-serve skill (boot stack + headed browser for human testing)
...
- pkill stale procs, boot backend + frontend in background
- STORAGE=ws default (firebase opt-in)
- agent_browser --headed fresh session for user-visible window
- troubleshooting table + teardown
Skill loads via /skill:dev-serve
2026-06-29 11:24:23 -04:00
david raistrick
e743d40e8d
M2: fix ws adapter for browser WebSocket + firebase path prefix
...
- WebSocketImpl: native browser WebSocket if present, else ws pkg (node/jest)
- ensureWs: use onopen/onmessage/onerror/onclose property handlers (browser API)
instead of ws.on('open') EventEmitter (node-only) — was silent no-op in browser
- norm(): strip 'artifacts/{APP_ID}/public/data/' prefix from all paths
App passes firebase-prefixed paths; backend uses canonical campaigns/...
- apply norm() to getDoc/getCollection/setDoc/updateDoc/deleteDoc/addDoc/
subscribeDoc/subscribeCollection/changeTypesForDocPath/changeTypesForCollPath
Verified: STORAGE=ws boots, WS subscribe fires, backend broadcast -> browser
live update (curl POST campaign -> appears without reload). Cross-device sync
confirmed end-to-end.
2026-06-28 22:43:30 -04:00
david raistrick
3e84f28325
M2: gate storage init on STORAGE mode (firebase vs ws/memory)
...
- STORAGE_MODE = getStorageMode()
- initializeStorage(): firebase = real SDK init; ws/memory = stub auth
- App auth flow: ws/memory skip signInAnonymously, unblock UI
- error screen mode-aware message
56 tests green. STORAGE=ws now boots past config error.
2026-06-28 21:11:56 -04:00
david raistrick
812298fa73
M2: refactor all firebase write sites to storage adapter
...
- 37 call sites: setDoc/updateDoc/deleteDoc/addDoc/getDocs/writeBatch -> storage.*
- adapter wraps SDK, path-string interface
- storage instance app-wide (getStorage)
- firebase.js: static imports (getDoc/getDocs alias), no dynamic import
56 frontend tests green. STORAGE=firebase = identical behavior.
2026-06-28 21:05:39 -04:00
david raistrick
5bb9e5fc19
M2: refactor hooks to storage adapter (subscribe)
...
- src/storage/index.js: getStorage() factory + SDK re-exports
- App.js: useFirestoreDocument/Collection call storage.subscribeDoc/Collection
- getStorage import added
56 frontend tests green. Hooks now impl-agnostic (firebase vs ws).
2026-06-28 19:03:44 -04:00
david raistrick
35b5a1d238
test: logs + deathSave characterization (6 tests)
...
- Logs.characterization.test.js: logAction (write + undo payload), clearLogs batch delete, undo (updateDoc encounter + mark undone), deathSave increment + isDying
- mock firestore getDocs: return .ref.path on docs (batch.delete support)
- mock addDoc: record full doc path not collection path
All write sites characterized. 56 frontend tests green.
2026-06-28 19:00:08 -04:00
david raistrick
d581e60ba3
test: combat characterization (9 tests)
...
- Combat.characterization.test.js: startEncounter (state + activeDisplay), nextTurn, round wrap, pause, resume, endEncounter (reset + clear activeDisplay), toggleHidePlayerHp
Locks combat control write paths.
2026-06-28 18:52:49 -04:00
david raistrick
4158a1634d
test: participant characterization (9 tests)
...
- Participant.characterization.test.js: addMonster (shape, initiative range, NPC), deleteParticipant, toggleActive, applyDamage, damage-to-0, heal revive, toggleCondition
- testHelpers.js: getParticipantForm (scoped), addMonsterViaUI, setupReady, startCombatViaUI
Locks participant write paths + payload shapes. Refactor guard.
2026-06-28 18:50:42 -04:00
david raistrick
0c1196aee1
test: encounter characterization (6 tests)
...
- Encounter.characterization.test.js: createEncounter, path nesting, togglePlayerDisplay on/off, deleteEncounter + clears activeDisplay
- testHelpers.js: createEncounterViaUI, selectEncounterByName
Locks encounter write paths + payload shapes.
2026-06-28 18:30:57 -04:00
david raistrick
672f042b60
chore: alphabetize package.json deps after install/uninstall churn
...
trivial reorder, no version changes
2026-06-28 18:29:09 -04:00
david raistrick
b6555648ee
test: campaign characterization (7 tests)
...
- src/testHelpers.js: renderApp, createCampaignViaUI, selectCampaignByName
- App.characterization.test.js: createCampaign, addCharacter, updateCharacter, deleteCharacter, deleteCampaign + path namespace + bg url
- mock firestore writeBatch sync (was async, app no-await)
Locks path + payload shape per action. Refactor guard.
2026-06-28 18:12:27 -04:00
david raistrick
84dd17e174
test: Firebase mock harness + createCampaign characterization
...
- src/__mocks__/firebase/*: jest manual mocks (app/auth/firestore)
- src/__mocks__/firebase/_mock-db.js: in-memory DB + call recorder
- src/setupTests.js: jest-dom, env stubs, crypto polyfill, DB reset
- src/App.characterization.test.js: createCampaign -> setDoc path/payload locked
- src/storage/contract.js (renamed from .test.js, helper not suite)
21 tests green (memory 19 + createCampaign 2).
2026-06-28 17:59:50 -04:00
david raistrick
12b24eb707
M2 (C): storage barrel re-export, App.js imports swapped
...
- src/storage/index.js: re-exports Firebase SDK
- App.js: imports from ./storage (was firebase/* direct)
- STORAGE=firebase = identical behavior
- dev server compiles clean
Safe refactor proof. Next: per-call-site path-based rewrite for ws adapter.
2026-06-28 17:51:39 -04:00
david raistrick
2ee2bba93b
M2 (TDD): storage contract test + memory impl
...
- src/storage/contract.test.js: storage interface spec (19 assertions)
- src/storage/memory.js: in-process impl (Map + EventEmitter)
- src/storage/storage.test.js: runner, memory first
TDD: contract RED first, memory built to satisfy, 19/19 green.
Next impls (ws, firebase) run same contract.
2026-06-28 17:18:14 -04:00
david raistrick
9457f48b23
ci: local pre-push hook instead of GH Actions (private repo)
...
- remove .github/workflows/ci.yml
- add .githooks/pre-push: runs npm run test:all
- git config core.hooksPath .githooks (set)
- docs/DEVELOPMENT.md: document local pipeline
Private repo = no free Actions. Revisit when public.
2026-06-28 17:16:23 -04:00
david raistrick
fa19913e23
ci: add GitHub Actions workflow + dev docs + test:all script
...
- .github/workflows/ci.yml: runs shared + server tests on push/PR
- docs/DEVELOPMENT.md: setup, run, test, architecture, status
- package.json: test:all script (shared + server suites)
2026-06-28 17:14:51 -04:00
david raistrick
0e76fb2fc7
M1: backend (Express+ws+better-sqlite3) + integration tests
...
- server/db.js: SQLite schema mirroring Firestore doc tree
- server/handlers.js: action -> shared turn fn -> tx persist -> broadcast
- server/index.js: REST endpoints + WebSocket real-time push
- server/server.test.js: 7 integration tests (REST CRUD + combat flow)
- --forceExit for jest (open WS handles)
Backend boots, serves state, persists to SQLite.
2026-06-28 17:01:53 -04:00
david raistrick
e06adaa081
M1: shared turn logic + characterization tests (39 green)
...
- npm workspaces: shared/, server/
- shared/turn.js: port turn logic verbatim from App.js (bugs preserved)
- 39 characterization tests lock current behavior
- gitignore: sqlite data, logs
2026-06-28 16:57:43 -04:00
david raistrick
d679c9d1e9
docs: restore approved milestone plan (backend=M1, drop FSM-extract milestone)
2026-06-28 16:53:18 -04:00
david raistrick
ad7979d8fd
docs: add rework plan (backend-first, test-ecosystem baseline)
2026-06-28 16:47:48 -04:00