match main firebase behavior; fix contract + mismatches

Contract rewritten to match main prod truth:
- require id injection in all doc/collection results
- honor setDoc({merge:true}) (main L1624 + 4 activeDisplay sites)
- drop invented bare<->prefixed cross-lookup tests (main never does this)
- add setDoc{merge}, batch set-only/update-only contract coverage

Fix mismatches vs main:
- subscribeCollection hook now forwards queryConstraints (was dropped;
  LOG_QUERY sort+limit honored again). Mock onSnapshot honors orderBy+limit.
  2 new contract tests prove the chain.
- subscribeDoc/subscribeCollection adapters now forward errCb. App hooks +
  DisplayView propagate subscribe errors to UI (match main onSnapshot 3rd arg).
- ws adapter signatures accept queryConstraints + errCb (interface match).

activeDisplay writes match main:
- 5 unguarded sites -> setDoc({merge:true}) (create-if-missing, not updateDoc)
- 3 guarded sites stay updateDoc

Delete memory adapter: third storage system added complexity, zero value.
firebase-mock covers fast tests, ws covers server path. Factory throws on
unknown mode now.

Delete phantom 'Phase A/B' comment in firebase.js header.

Tests updated to assert setDoc{merge} (not updateDoc) for activeDisplay writes.
83/83 green.
This commit is contained in:
david raistrick
2026-07-04 12:22:19 -04:00
parent b12ac904a6
commit 79af61cb8b
17 changed files with 240 additions and 251 deletions
+9 -7
View File
@@ -1,8 +1,8 @@
// BUG-4 repro: toggling hidePlayerHp must not clobber activeDisplay doc.
// setDoc = replace (contract). {merge:true} arg ignored.
// Toggling hide-HP writes {hidePlayerHp:X} alone → activeCampaignId + activeEncounterId → null.
// Display reads null → "Game Session Paused". Recover requires re-activating encounter.
// Fix: use updateDoc (patch), not setDoc.
// setDoc = replace would clobber. setDoc({merge:true}) patches only — matches main.
// Toggling hide-HP writes {hidePlayerHp:X} via setDoc{merge} → activeCampaignId + activeEncounterId preserved.
// Display keeps reading them → stays active.
// Regression: if caller swaps to plain setDoc (no merge) or updateDoc-throws-on-missing, re-breaks.
import React from 'react';
import { render, waitFor, screen, fireEvent } from '@testing-library/react';
@@ -50,14 +50,16 @@ describe('BUG-4: hide-player-HP toggle preserves activeDisplay', () => {
await waitFor(() => {
const writes = getAdapterCalls().filter(
c => c.fn === 'updateDoc' && c.path.includes('activeDisplay/status')
c => c.fn === 'setDoc' && c.path.includes('activeDisplay/status')
);
expect(writes.length).toBeGreaterThan(0);
const last = writes[writes.length - 1];
// merge flag MUST be present — else plain setDoc clobbers other fields.
expect(last.opts && last.opts.merge).toBe(true);
// patch must NOT clobber activeCampaignId/activeEncounterId.
// BUG: setDoc replace writes only {hidePlayerHp:true} → clobbers.
// Fix: updateDoc patch — other fields untouched.
expect(last.patch.hidePlayerHp).toBe(true);
// Fix: setDoc{merge} patch — other fields untouched.
expect(last.data.hidePlayerHp).toBe(true);
}, { timeout: 3000 });
});
});