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
+29 -2
View File
@@ -19,7 +19,11 @@ export function limit(n) { return { __type: 'limit', n }; }
// writes
export async function setDoc(docRef, data, opts) {
recordCall({ fn: 'setDoc', path: docRef.path, data: clone(data), opts: opts || null });
MOCK_DB.set(docRef.path, clone(data));
if (opts && opts.merge) {
MOCK_DB.merge(docRef.path, clone(data));
} else {
MOCK_DB.set(docRef.path, clone(data));
}
return undefined;
}
export async function updateDoc(docRef, patch) {
@@ -70,6 +74,7 @@ export async function getDocs(collRefOrQuery) {
// realtime — emit from mock DB, capture unsub
export function onSnapshot(refOrQuery, onSuccess, onError) {
const path = refOrQuery.path || (refOrQuery.ref && refOrQuery.ref.path);
const constraints = refOrQuery.constraints || [];
// fire immediately with current state
const emit = () => {
if (refOrQuery.__ref && refOrQuery.path && path.split('/').length % 2 === 0) {
@@ -80,7 +85,8 @@ export function onSnapshot(refOrQuery, onSuccess, onError) {
data: () => data,
});
} else {
const docs = MOCK_DB.collection(path);
let docs = MOCK_DB.collection(path);
docs = applyConstraints(docs, constraints);
onSuccess({ docs: docs.map(d => ({ id: d.id, data: () => d.data })) });
}
};
@@ -90,6 +96,27 @@ export function onSnapshot(refOrQuery, onSuccess, onError) {
return unsub;
}
// Apply Firestore-style query constraints (orderBy desc/asc, limit) to mock docs.
// Mirrors real SDK semantics enough for contract tests. Only orderBy + limit
// supported (App's LOG_QUERY uses exactly these).
function applyConstraints(docs, constraints) {
let out = [...docs];
for (const c of constraints) {
if (c.__type === 'orderBy') {
out.sort((a, b) => {
const av = a.data[c.field];
const bv = b.data[c.field];
if (av === bv) return 0;
const cmp = av > bv ? 1 : -1;
return c.dir === 'desc' ? -cmp : cmp;
});
} else if (c.__type === 'limit') {
out = out.slice(0, c.n);
}
}
return out;
}
function clone(v) {
if (v === null || v === undefined) return v;
return JSON.parse(JSON.stringify(v));