Closer/ClaudeReport.md

14 KiB
Raw Blame History

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_swipes security rules were self-defeating — required swipedAt is timestamp (client writes a number) and actions.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 with is number + a diff().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,functions runs, 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 and getAnswer() 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. The users rule 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 the users rule 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, so releaseOwnKey fails 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 sealed releaseKeys/devices paths all returned PERMISSION_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/devices permission denials are gone. The "doesn't show the partner's face" bug is resolved by the users rule + 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.saveSession generated the session doc id but returned Result<Unit>, so GameSessionManager.startGame returned a session with empty idobserveAnswers built an invalid path → crash. Affected This or That, Desire Sync, How Well (all use startGame). Fixed: saveSession now returns the doc id; GameSessionManager uses 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." releaseOwnKey returns false because the partner's ECIES public key isn't retrievable, and there's a persistent denial where the client observes its own releaseKeys (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:

  1. Sender couldn't write its release key. writeReleaseKey did an existence-check ref.get() first, but the releaseKeys read rule is recipient-only → the sender's get() threw PERMISSION_DENIEDreleaseOwnKey threw → "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)
  2. Reveal crashed reading the partner's answer. markAnswerKeyReleased wrote updatedAt as a Firestore serverTimestamp(), but toLocalAnswer read it with getLong()RuntimeException: Field 'updatedAt' is not a java.lang.Number → hard crash when the partner opened the reveal. Fix: write updatedAt as epoch-millis, and read time fields defensively (Long / Timestamp / Date). (FirestoreAnswerDataSource.kt)
  3. 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_matches onCreate.
  • Daily question made deterministic-per-day; shared DailyQuestionResolver unifies 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.