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:
+44
-32
@@ -27,10 +27,10 @@ function runStorageContract(name, factory) {
|
||||
afterEach(async () => { if (storage && storage.dispose) await storage.dispose(); });
|
||||
|
||||
describe('getDoc / setDoc', () => {
|
||||
test('setDoc then getDoc returns the doc', async () => {
|
||||
test('setDoc then getDoc returns the doc (with id)', async () => {
|
||||
await storage.setDoc('campaigns/a', { name: 'Alpha' });
|
||||
const doc = await storage.getDoc('campaigns/a');
|
||||
expect(doc).toEqual({ name: 'Alpha' });
|
||||
expect(doc).toEqual({ id: 'a', name: 'Alpha' });
|
||||
});
|
||||
|
||||
test('getDoc on missing path returns null', async () => {
|
||||
@@ -42,7 +42,15 @@ function runStorageContract(name, factory) {
|
||||
await storage.setDoc('campaigns/a', { name: 'Alpha', players: [] });
|
||||
await storage.setDoc('campaigns/a', { name: 'Beta' });
|
||||
const doc = await storage.getDoc('campaigns/a');
|
||||
expect(doc).toEqual({ name: 'Beta' });
|
||||
expect(doc).toEqual({ id: 'a', name: 'Beta' });
|
||||
});
|
||||
|
||||
// main L1624: setDoc(path, data, {merge:true}) — merge flag.
|
||||
test('setDoc with {merge:true} merges into existing doc', async () => {
|
||||
await storage.setDoc('campaigns/a', { name: 'Alpha', players: [] });
|
||||
await storage.setDoc('campaigns/a', { players: [1] }, { merge: true });
|
||||
const doc = await storage.getDoc('campaigns/a');
|
||||
expect(doc).toEqual({ id: 'a', name: 'Alpha', players: [1] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,13 +59,13 @@ function runStorageContract(name, factory) {
|
||||
await storage.setDoc('campaigns/a', { name: 'Alpha', players: [1] });
|
||||
await storage.updateDoc('campaigns/a', { players: [1, 2] });
|
||||
const doc = await storage.getDoc('campaigns/a');
|
||||
expect(doc).toEqual({ name: 'Alpha', players: [1, 2] });
|
||||
expect(doc).toEqual({ id: 'a', name: 'Alpha', players: [1, 2] });
|
||||
});
|
||||
|
||||
test('updateDoc on missing doc creates it', async () => {
|
||||
await storage.updateDoc('campaigns/a', { name: 'Alpha' });
|
||||
const doc = await storage.getDoc('campaigns/a');
|
||||
expect(doc).toEqual({ name: 'Alpha' });
|
||||
expect(doc).toEqual({ id: 'a', name: 'Alpha' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,7 +87,7 @@ function runStorageContract(name, factory) {
|
||||
expect(id).toBeTruthy();
|
||||
expect(path).toBe(`campaigns/a/encounters/${id}`);
|
||||
const doc = await storage.getDoc(path);
|
||||
expect(doc).toEqual({ name: 'E1' });
|
||||
expect(doc).toEqual({ id, name: 'E1' });
|
||||
});
|
||||
|
||||
test('two addDocs produce distinct ids', async () => {
|
||||
@@ -91,33 +99,15 @@ function runStorageContract(name, factory) {
|
||||
|
||||
describe('firebase-prefixed path identity', () => {
|
||||
// App passes firebase-prefixed paths (artifacts/{APP_ID}/public/data/...).
|
||||
// Adapter must normalize internally so write+read at prefixed path round-trips
|
||||
// AND collection queries at bare canonical path find prefixed-written docs.
|
||||
// Catches replay-script bug (wrote prefixed, adapter reads bare, missed).
|
||||
// main prod truth: ALWAYS prefixed. Write+read same prefixed round-trips.
|
||||
// Cross bare<->prefixed lookup is NOT required by main (App never does it).
|
||||
// Kept as same-prefix roundtrip only — matches prod.
|
||||
const PREFIX = 'artifacts/test-app/public/data';
|
||||
|
||||
test('setDoc prefixed then getCollection bare finds it', async () => {
|
||||
await storage.setDoc(`${PREFIX}/campaigns/c1`, { name: 'P1' });
|
||||
const docs = await storage.getCollection('campaigns');
|
||||
expect(docs.some(d => d.name === 'P1')).toBe(true);
|
||||
});
|
||||
|
||||
test('setDoc prefixed then getDoc same prefixed path returns it', async () => {
|
||||
test('setDoc prefixed then getDoc same prefixed path returns it (id)', async () => {
|
||||
await storage.setDoc(`${PREFIX}/campaigns/c2`, { name: 'P2' });
|
||||
const doc = await storage.getDoc(`${PREFIX}/campaigns/c2`);
|
||||
expect(doc).toEqual({ name: 'P2' });
|
||||
});
|
||||
|
||||
test('setDoc prefixed then getDoc bare path returns it', async () => {
|
||||
await storage.setDoc(`${PREFIX}/campaigns/c3`, { name: 'P3' });
|
||||
const doc = await storage.getDoc('campaigns/c3');
|
||||
expect(doc).toEqual({ name: 'P3' });
|
||||
});
|
||||
|
||||
test('setDoc bare then getCollection prefixed finds it', async () => {
|
||||
await storage.setDoc('campaigns/c4', { name: 'P4' });
|
||||
const docs = await storage.getCollection(`${PREFIX}/campaigns`);
|
||||
expect(docs.some(d => d.name === 'P4')).toBe(true);
|
||||
expect(doc).toEqual({ id: 'c2', name: 'P2' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -165,7 +155,29 @@ function runStorageContract(name, factory) {
|
||||
{ type: 'delete', path: 'campaigns/a' },
|
||||
]);
|
||||
expect(await storage.getDoc('campaigns/a')).toBeNull();
|
||||
expect(await storage.getDoc('campaigns/b')).toEqual({ name: 'B' });
|
||||
expect(await storage.getDoc('campaigns/b')).toEqual({ id: 'b', name: 'B' });
|
||||
});
|
||||
|
||||
// set-only batch (not used in main currently, but interface allows).
|
||||
test('applies set-only batch', async () => {
|
||||
await storage.batchWrite([
|
||||
{ type: 'set', path: 'campaigns/a', data: { name: 'A' } },
|
||||
{ type: 'set', path: 'campaigns/b', data: { name: 'B' } },
|
||||
]);
|
||||
expect(await storage.getDoc('campaigns/a')).toEqual({ id: 'a', name: 'A' });
|
||||
expect(await storage.getDoc('campaigns/b')).toEqual({ id: 'b', name: 'B' });
|
||||
});
|
||||
|
||||
// update-only batch (interface allows).
|
||||
test('applies update-only batch', async () => {
|
||||
await storage.setDoc('campaigns/a', { name: 'A', hp: 10 });
|
||||
await storage.setDoc('campaigns/b', { name: 'B', hp: 5 });
|
||||
await storage.batchWrite([
|
||||
{ type: 'update', path: 'campaigns/a', data: { hp: 8 } },
|
||||
{ type: 'update', path: 'campaigns/b', data: { hp: 2 } },
|
||||
]);
|
||||
expect(await storage.getDoc('campaigns/a')).toEqual({ id: 'a', name: 'A', hp: 8 });
|
||||
expect(await storage.getDoc('campaigns/b')).toEqual({ id: 'b', name: 'B', hp: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -176,7 +188,7 @@ function runStorageContract(name, factory) {
|
||||
storage.subscribeDoc('campaigns/a', (doc) => calls.push(doc));
|
||||
await flush();
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]).toEqual({ name: 'Alpha' });
|
||||
expect(calls[0]).toEqual({ id: 'a', name: 'Alpha' });
|
||||
});
|
||||
|
||||
test('fires cb on subsequent change', async () => {
|
||||
@@ -186,7 +198,7 @@ function runStorageContract(name, factory) {
|
||||
await storage.setDoc('campaigns/a', { name: 'Alpha' });
|
||||
await flush();
|
||||
const last = calls[calls.length - 1];
|
||||
expect(last).toEqual({ name: 'Alpha' });
|
||||
expect(last).toEqual({ id: 'a', name: 'Alpha' });
|
||||
});
|
||||
|
||||
test('unsubscribe stops callbacks', async () => {
|
||||
|
||||
Reference in New Issue
Block a user