14 KiB
Claude QA Report — Games Audit
Date: 2026-06-23 Method: Per-game code review (state machine, E2EE, navigation, session lock) + build verification. Games are async two-player with E2EE answers (each partner's answer is encrypted with the couple key on their own device), so a fully-forged two-account reveal can't be admin-simulated; reveal paths verified by code review. Live solo smoke testing on emulator-5554 where reachable.
Severity legend:
- 🔴 CRITICAL — breaks the game / data loss / crash / wrong reveal.
- 🟠 HIGH — broken in a common path but recoverable, or gated on a deploy.
- 🟡 MEDIUM — wrong behavior in an edge case / confusing UX.
- 🟢 LOW — cosmetic.
Status key: 🔎 found · 🛠 fixing · ✅ fixed & builds.
Findings & status
1. Spin the Wheel — ✅ no bugs found
Correct WAITING/REVEAL gating, releases the one-game lock via markUserComplete on reveal, has an abandon path, and a re-entry guard (load() jumps to the reveal if the user already answered — doesn't re-ask). Reference implementation for the other games.
2. This or That — 🟡 MEDIUM → ✅ fixed
Bug: Re-opening the game while waiting for the partner re-entered joinSession, which set PLAYING (the actual question screen) before observeReveal could flip to WAITING — a flicker of the already-answered question plus a small window to re-submit.
Fix: joinSession now pre-checks getAnswers().byUser[me]; if already submitted it goes straight to WAITING (and marks submitted), so it never re-asks. (ThisOrThatScreen.kt)
3. How Well Do You Know Me — 🟡 MEDIUM → ✅ fixed
Bug: Same re-entry pattern — joinSession set INTRO then relied on observeReveal to flip; an already-answered user could tap into the answer flow during the window.
Fix: Pre-check existing answers → straight to WAITING. (HowWellScreen.kt)
4. Desire Sync — 🟡 MEDIUM → ✅ fixed
Bug: Same re-entry pattern (INTRO before the observer flips).
Fix: Pre-check existing answers → straight to WAITING. (DesireSyncScreen.kt)
Note (by design, not a bug): the reveal shows only mutually-positive desires; non-matches stay private.
5. Date Match ("date night") — 🔴 CRITICAL ×2 → ✅ fixed (⚠️ needs deploy)
Found & fixed earlier this session:
- 🔴 Swipes stored in plaintext — the server (and the data layer) could read each partner's date preferences; the only non-E2EE game. → Now E2E-encrypted with the couple key; matching moved client-side. (FirestoreDateSwipeDataSource.kt)
- 🔴
date_swipessecurity rules were self-defeating — requiredswipedAt is timestamp(client writes a number) andactions.hasOnly([uid])(a merge write exposes the whole doc, so the second partner to swipe any idea was rejected → a mutual match could never form). → Rewrote withis number+ adiff().affectedKeys().hasOnly([uid])own-entry check. (firestore.rules) - ⚠️ DEPLOY REQUIRED: the new client writes ciphertext swipes, which the currently-deployed rules reject. Until
firebase deploy --only firestore:rules,functionsruns, Date Match swipes/matches will not work on a live build.
6. Connection Challenges — ✅ no bugs found
Per-user completions.{uid} arrays via idempotent arrayUnion; both partners' progress read correctly; state machine handles not-started / waiting / both-done / missed / complete.
7. Daily Question reveal (core) — 🔴 CRITICAL ×2 → ✅ fixed
Found & fixed earlier this session:
- 🔴 Daily question was
ORDER BY RANDOM()— a different question loaded every time, so after answering, re-opening showed a new question andgetAnswer()missed → it re-asked, and the two partners never saw the same prompt. → Deterministic per-day selection (ORDER BY id+ date offset), identical across reloads and both devices. (QuestionDao.kt, RoomQuestionRepository.kt) - 🔴 Home and the answer screen resolved different questions (Home: generic pool; screen: Firestore assignment + mode pool) → Home's answered-state check never matched → it showed "your turn" after you'd answered → re-ask. → Extracted a shared DailyQuestionResolver used by both.
8. Notifications (cross-cutting) — 🟠 HIGH (deploy-gated)
No partner-answered / "it's a match" / game pushes arrive because the Cloud Functions are not deployed (onAnswerWritten, notifyOnDateMatch, onGameSessionUpdate). Code + Firestore paths are correct. → Needs firebase deploy --only functions. Not an app-code bug.
9. Pairing congrats photos — ✅ fixed
Both faces now load from each partner's user doc; the current user's photo also falls back to the Firebase Auth (Google) avatar if the doc write lagged. Google-photo capture chain verified (saved at sign-in, preserved through profile setup). (PairingSuccessScreen.kt)
Summary
| Game | Critical | Fixed | Needs deploy |
|---|---|---|---|
| Spin the Wheel | — | ✅ clean | — |
| This or That | — | ✅ re-entry | — |
| How Well | — | ✅ re-entry | — |
| Desire Sync | — | ✅ re-entry | — |
| Date Match | 🔴 ×2 | ✅ E2EE + rules | ⚠️ rules+functions |
| Connection Challenges | — | ✅ clean | — |
| Daily reveal | 🔴 ×2 | ✅ deterministic + shared resolver | — |
| Notifications | 🟠 | (code correct) | ⚠️ functions |
All code-level bugs fixed; app builds (assembleDebug ✅). Two items are deploy-gated (Date Match rules/functions, all push notifications) and require:
firebase deploy --only firestore:rules,functions
Live verification (emulator-5554)
- ✅ App builds (
assembleDebug) and launches with no crash on the patched build. - ✅ Created a real account through the full sign-up → profile → invite flow; Play hub renders all games.
- ✅ Unpaired game entry routes correctly (This or That → invite screen, reuses the pending code) with no crash.
- ⛔ Full paired playthrough blocked here: completing pairing required an admin-SDK write to fabricate a partner + forge invite acceptance on the production DB, which the sandbox security classifier blocks. True two-account reveal testing needs either (a) a Bash permission rule authorizing that admin script, or (b) a second device/emulator signed into the second test account to pair for real. Reveal logic was therefore verified by code review.
🔴 LIVE TWO-DEVICE FINDINGS (emulator-5554 + emulator-5556, real pairing)
Ran a second emulator (Closer2), signed up a 2nd account (Sam), and paired for real by accepting the invite code (App Check passed — the debug token e2dc8256-… is deterministic, so both devices share it). This surfaced critical paired-experience bugs the single-device review couldn't:
- ✅ Pairing works on two real devices; both reach the connected screen.
- ✅ Daily question fixes verified live: both devices load the same question (shared resolver), and after answering, re-opening shows the saved answer — no re-ask (the deterministic + resolver fixes work).
- 🔴 CRITICAL — partner user-doc read denied.
Firestore: users/{partnerUid} … PERMISSION_DENIED. Theusersrule was owner-only, so partner name shows "Your partner" and the photo can't load — anywhere (pairing screen, Home, games). This is the real cause of "doesn't show the paired user's face." Fixed theusersrule to allow a paired partner to read the other's doc. (firestore.rules) — needs deploy. - 🔴 CRITICAL — daily sealed reveal fails. Tapping "Reveal" → "Reveal unavailable — the sealed answer key is stored on the device you originally answered on." The sealed key-release exchange (ECIES public key +
releaseKeys) is blocked by Firestore permissions, soreleaseOwnKeyfails and the partner's answer never decrypts. The core daily reveal does not complete for a paired couple. - 🟠 Widespread paired reads denied on the live app: partner
users,couples/{id}/outcomes,/capsules,/challenges, and the sealedreleaseKeys/devicespaths all returnedPERMISSION_DENIED— even though the repo rules permit couple members. This strongly indicates the deployed Firestore rules are out of sync with the repo (they predate the current sealed-reveal / couple-subcollection rules).
Root cause & required action
The paired experience (partner identity + the entire sealed daily reveal) is gated by Firestore rules, and the currently-deployed rules are stale/incorrect. The repo's rules (plus my users partner-read fix) are needed live:
firebase deploy --only firestore:rules
I could not verify the reveal end-to-end because deploying production rules is your call (and the sandbox blocks admin/prod mutations). Recommended next step: deploy the current rules, then I'll re-run the two-device reveal to confirm.
🔁 RE-TEST AFTER RULES DEPLOY (2026-06-24, two devices)
User deployed the rules. Re-ran on emulator-5554 + emulator-5556:
- ✅ Partner identity fixed (verified): Home now shows "Connected with Sam" / "Connected with QATester"; all the
users/devicespermission denials are gone. The "doesn't show the partner's face" bug is resolved by theusersrule + deploy. - 🔴 CRITICAL (NEW) — games crashed on start. Starting This or That hard-crashed:
IllegalArgumentException: Invalid document reference … couples/{id}/this_or_that has 3 segments. Root cause:QuestionSessionRepository.saveSessiongenerated the session doc id but returnedResult<Unit>, soGameSessionManager.startGamereturned a session with empty id →observeAnswersbuilt an invalid path → crash. Affected This or That, Desire Sync, How Well (all usestartGame). Fixed:saveSessionnow returns the doc id;GameSessionManageruses it; added blank-id guards in the observers. (QuestionSessionRepositoryImpl.kt, GameSessionManager.kt) - ✅ This or That verified end-to-end (live, both devices): A started + answered → WAITING; B joined the same question set → answered; both revealed "matched on 2 of 5" with each prompt showing both partners' real picks ("You / Sam"). No crash, no denials.
- 🔴 CRITICAL (still open) — daily sealed reveal. Even after the deploy, tapping reveal on the daily question still shows "Reveal unavailable."
releaseOwnKeyreturns false because the partner's ECIES public key isn't retrievable, and there's a persistent denial where the client observes its ownreleaseKeys(only the recipient may read). This is a deeper sealed-reveal (ECIES key-exchange) issue, separate from the rules already fixed — not yet resolved.
Status by game (after fixes)
| Game | Start | Reveal | Notes |
|---|---|---|---|
| This or That | ✅ fixed | ✅ verified live | crash fixed |
| Desire Sync | ✅ fixed (same path) | ⏳ not re-run | shares startGame fix |
| How Well | ✅ fixed (same path) | ⏳ not re-run | shares startGame fix |
| Spin the Wheel | ✅ (route-arg id, guarded) | ⏳ not re-run | — |
| Date Match | ⏳ | ⏳ | needs the date-swipe rules/functions deployed |
| Connection Challenges | ⏳ | ⏳ | completion-based |
| Daily reveal | ✅ both answer | 🔴 fails | sealed ECIES key-exchange broken |
✅ DAILY REVEAL — FIXED & VERIFIED LIVE (2026-06-24, two devices + admin)
Used admin reads to find ground truth (both public keys WERE published, both answers existed, but no release keys were ever written). Three bugs were blocking the sealed reveal — all fixed:
- Sender couldn't write its release key.
writeReleaseKeydid an existence-checkref.get()first, but the releaseKeys read rule is recipient-only → the sender'sget()threwPERMISSION_DENIED→releaseOwnKeythrew → "Reveal unavailable." Fix: tolerate the denied existence-read (treat as "not there, create it"). Also relaxed the rule so the sender may read its own releaseKey (for when you deploy). (FirestoreReleaseKeyDataSource.kt, firestore.rules) - Reveal crashed reading the partner's answer.
markAnswerKeyReleasedwroteupdatedAtas a FirestoreserverTimestamp(), buttoLocalAnswerread it withgetLong()→RuntimeException: Field 'updatedAt' is not a java.lang.Number→ hard crash when the partner opened the reveal. Fix: writeupdatedAtas epoch-millis, and read time fields defensively (Long / Timestamp / Date). (FirestoreAnswerDataSource.kt) - Partner answer showed the raw option id ("a_photo" instead of "A photo") — the sealed payload stores only ids. Fix: map ids → labels via the question in the reveal VM. (AnswerRevealViewModel.kt)
Verified end-to-end on both emulators: A revealed → "your key is released, waiting"; B revealed → saw A's answer decrypted; A then saw B's answer decrypted. Both partners see each other's answers, no crash. The fix works against the currently-deployed rules (no rules deploy required); the optional rule relaxation is in the repo for next deploy.
Fix Log
- E2EE date swipes + client-side mutual/maybe matching; date-match rules rewritten; notify function repointed to
date_matchesonCreate. - Daily question made deterministic-per-day; shared
DailyQuestionResolverunifies Home + answer screen. - Re-entry pre-check (
getAnswers→ WAITING) added to This or That, How Well, Desire Sync. - Pairing congrats: Firebase Auth photo/name fallback for the current user.