12 KiB
Claude QA Report — Full-App QA (living report)
RUN-STATE: Round 2 | Pass B in progress (1 game/chunk) | ✅ This or That, ✅ How Well (both full playthroughs, results match, no crash). NEXT ACTION: play Desire Sync → Connection Challenges → Memory Lane → Spin the Wheel → Date Match (Desire Sync + Memory Lane need a Sam-premium toggle; remember to exit games via Back to Play so the session closes). Then Pass C deep/stateful + nav/back; live notification matrix; D3 non-member; Pass F (incl. F-OBS). Pass-B note: a finished game keeps its session active until a player exits the results (Back to Play); leaving both on results blocks the next game until "End their game". Exit cleanly between games. R2-1 DONE: A-001 couple-shared re-verified live (Desire Sync/Memory Lane/Wheel enter when partner premium; free→paywall). D-001 (P1) FIXED+DEPLOYED (capsules/challenges rules; Memory Lane + Connection Challenges now load). Sam reverted to free (baseline). Round 1 complete (all 5 passes run report-only; P0–P2 found were fixed in-line). Fixes: A-001 (
e8892a9), E-001 (ce12abb). Open P3: A-003, B-001, E-002. EXECUTION MODE: autonomous run-to-completion — do NOT stop; fix blockers inline; keep cycling fix→re-QA until flawless. Do NOT hand back when context fills — the harness auto-compacts and you continue from THIS run-state (re-read it + coverage after any summary). Commit before interruptible work; recover stuck sessions via the session-start ritual. STANDING AUTHORIZATION (user, 2026-06-24): mayfirebase deploy --only firestore:rules+ has admin access (Firestore reads/writes/seeds + entitlement toggles) — run these without pausing. Only the macOS requirement for iOS (Parts 2/3) remains a hard stop. Playbook:ClaudeQAPlan.md. Coverage matrix:ClaudeQACoverage.md. Report-only during passes (no fixes until the fix phase). Devices: emulator-5554 (QA=Y05AKO) + emulator-5556 (Sam=imDjjO), paired (coupleIdXal3Kw3gjSdn0niERYKJ). Build == HEAD64f0a7e.
(Prior games/notifications QA from 2026-06-24 was completed + verified; superseded by this full-app effort.)
Severity summary (Round 1)
| Severity | Open | Fixed |
|---|---|---|
| P0 | 0 | 0 |
| P1 | 0 | 2 |
| P2 | 0 | 1 |
| P3 | 4 | 0 |
Round 1: all P0–P2 found were FIXED (A-001 premium P1, E-001 notif-routing P2). Open = P3 cosmetics only (A-003 badge, B-001 stale-session guard, E-002 informational notif routing). Deferred for a clean "flawless" certification: exhaustive deep/stateful screens (Pass C), full live notification matrix + D3 live non-member test.
Pass A — Couple-shared premium ✅ pass complete
Target: if either partner is premium, all premium features unlock for both. Result: only chat is couple-shared. Every other feature gate is per-user → a free user whose partner paid stays locked.
| ID | Area | Screen/Route | Severity | Description | Repro | Status |
|---|---|---|---|---|---|---|
| A-001 | Premium gating | PlayHubViewModel, DesireSyncScreen, MemoryLaneScreen, ConnectionChallengesScreen, QuestionPackLibraryViewModel, wheel CategoryPicker/SpinWheel/WheelHistory(VMs) | P1 | These gated on per-user EntitlementChecker.isPremium() instead of couple-shared. A free partner of a premium user stayed locked. |
Set Sam premium, QA free → QA Play hub still showed 🔒 on Desire Sync + Memory Lane. | FIXED e8892a9 — routed all gates through CouplePremiumChecker (now exposes isPremium/hasPremium resolving partner internally). Verified: Sam premium → QA enters Desire Sync; both free → QA → paywall. |
| A-003 | Premium UI (cosmetic) | PlayHubScreen (Desire Sync + Memory Lane cards) | P3 | The "🔒 Premium" badge on these two cards is static (rendered in separate card composables that don't receive hasPremium), so it still shows a lock even when the couple has premium access. Feature IS accessible (gate fixed in A-001) — only the badge is misleading. |
With couple premium, QA's Play hub still shows 🔒 Premium on Desire Sync/Memory Lane though tapping enters the game. | Open (deferred — thread hasPremium into the card composables) |
| A-002 | Premium (control) | ConversationViewModel (chat) | — | Working correctly (couple-shared) — kept as the reference pattern for the A-001 fix. | Verified prior round: partner-premium unlocks chat media/reactions for the free partner. | OK |
Note (by-design, not a bug): SubscriptionScreen uses per-user isPremium() — correct, it reflects the user's own subscription/account state, not a feature gate.
Pass B — Games lifecycle (launch/crash sweep done; full two-device lifecycle partial)
| ID | Area | Screen/Route | Severity | Description | Repro | Status |
|---|---|---|---|---|---|---|
| B-001 | Games / sessions | couples/{id}/sessions | P3 (observe) | A stale active wheel session (startedBy QA, createdAt missing/undefined) blocked all games ("Waiting for Sam"). In-app "End their game" recovery works (session→completed, games unblocked). Likely a prior-test artifact, but: sessions appearing without a timestamp + one stale session blocking every game is worth a guard. |
Open This or That → "Waiting for Sam… Sam is playing a Wheel game"; tap End their game → unblocked. | Open |
Launch/crash sweep (QA, free): This or That ✅ (mood/length select), How Well Do You Know Me ✅ (intro), Connection Challenges ✅, Spin the Wheel ✅ — all render, no FATAL. Desire Sync + Memory Lane are premium-gated (covered in Pass A; gameplay needs premium toggle). Date Match: todo. Full two-device start→finish + results not exhaustively re-run this round (the prior round verified onGameSessionUpdate start/finish end-to-end).
Pass C — Visual (light + dark) (main screens verified; deep/stateful screens pending)
Method: 5554=Dark, 5556=Light; readable dark|light pair montages + a code scan for non-adapting colors.
Verified clean (both themes, readable, no clipping): Home, Today, Play, Messages inbox, Settings. closerBackgroundBrush() is theme-aware (adapts). No FATAL on these.
| ID | Area | Screen | Severity | Description | Status |
|---|---|---|---|---|---|
| C-OBS | Theming | ~20 screens (AnswerRevealScreen 15, WheelSessionScreen 14, DateMatchScreen 10, PaywallScreen 9, BucketListScreen 9, SettingsScreen 7, HomeScreen 5, …) | observe | Use hardcoded Color(0x…) literals (195 total) that don't adapt to theme — a dark-mode contrast risk to verify per-screen. Main screens checked look fine; deep/stateful screens (reveal, wheel session, dates, bucket list) still need visual verification in both themes. |
Open (verify in continuation) |
Deep/stateful screens (answer reveal, wheel session/complete, date match/builder/matches, bucket list, memory capsule, history, paywall, auth/onboarding/pairing) need their states set up — pending next chunk.
Pass B requirement (updated): each game must be played one complete time through on both devices (start → every step → finish/reveal/results), not just launched. Round 1 did launch-only → full playthroughs owed in Round 2 for all 7 (premium games need a premium toggle). A launch-only result = partial, not pass.
Pass C requirement (added): navigation from every entry point (each screen reached from all its links — e.g. conversation from inbox/Discuss/notification; game from Play/notification; paywall from each gate) + back-stack / "double-back" (system back AND in-app back return to the right place from each entry; no dead-ends, no exit-app surprise, no screen needing two backs/duplicate stack entries; deep-link/notification entries land with a sane back stack). Owed in Round 2. Wrong/double back or dead-end = P2 (P1 if it traps the user).
Pass D — Security & Encryption ✅ clean (no P0/P1 found)
- D1 at-rest: all private content is ciphertext — message
text+lastMessagePreview+ thread messages =enc:v1:; daily answersencryptedPayload=sealed:v1:. Metadata (dates, types, commitmentHash, ids) plaintext as expected. Chat media bytes = Tink ciphertext (verified prior round + unchanged code path). No plaintext content leak. - D2 rules: no catch-all
match /{document=**}, no blanketif true;hasPremiumserver-only (client create/update blocked, rules L172/174); entitlementswrite:false; conversations/messages/typing/reactions + entitlement partner-read scoped to members. - D4 key exchange: pairing uses a wrapped couple key (
wrappedCoupleKey+kdfSalt/kdfParams+encryptedRecoveryPhrase); invite code is the KDF seed, never stored raw; strict E2EE (invites without a wrapped key rejected) — confirmed inacceptInviteCallable. - D5 App Check/secrets: App Check enforced (
SecurityModule,PlayIntegrityChecker,FirebaseInitializer); both service-account JSONs gitignored and untracked;allowBackup=false. - D6 leak vectors: analytics events carry only metadata (no message/answer content);
allowBackup=false.
Follow-ups (not blockers): live non-member negative test (D3) needs a fresh 3rd account (rule logic verified member-scoped); a fresh Storage-bytes spot-check of chat media.
| ID | Area | Severity | Description | Status |
|---|---|---|---|---|
| D-001 | Rules — missing subcollection rules | P1 | couples/{id}/capsules and couples/{id}/challenges had no match block → default-deny → Memory Lane hung on its loading heart and Connection Challenges couldn't load (live PERMISSION_DENIED confirmed). Two premium features broken. |
Sam premium, QA opens Memory Lane → stuck loading heart; logcat Listen for Query(.../capsules) failed: PERMISSION_DENIED. |
| F-OBS | Resilience (UI) | P3 | MemoryLaneScreen (and likely others) hangs on the loading indicator forever when a Firestore query fails, instead of showing an error/empty state. Masked the D-001 root cause. Add load-failure handling. | Was visible before D-001 fix (stuck heart). |
| (outcomes) | Rules | — | The Round-1 outcomes list PERMISSION_DENIED is by-design — the rule restricts reads to specific dayKeys (day_0/30/60/90); a bare list query is correctly denied. Not a bug. |
— |
Pass E — Notifications
- Copy carries no private content: all function notification bodies are generic ("Tap to read and reply", "Answer together before it expires", etc.);
${title}refers to public question/game titles, not user answers. ✓ (ties to D6) - Routing: centralized in
PartnerNotificationType(fromRemoteType→routeFor); chat opens the exact conversation, reveal→answerReveal(questionId), games→Play, capsule→Memory Lane, etc. - Foundations (prior round, code present): FCM token registration on sign-in, POST_NOTIFICATIONS, channels.
| ID | Area | Severity | Description | Status |
|---|---|---|---|---|
| E-001 | Notification routing | P2 | Type-string mismatch: functions send daily_question + challenge_day_ready, but client mapped only daily_question_reminder + challenge_waiting → tapping those did NOT deep-link to Today / Connection Challenges. |
FIXED <pending-commit> — added daily_question/challenge_day_ready to fromRemoteType (build green; live tap-verify deferred). |
| E-002 | Notification routing | P3 | partner_left, partner_deleted_account, invite_created, spki are unmapped → tap lands on default (no deep-link). Informational types; acceptable but ideally routed. |
Open |
Full live delivery matrix (17 types × foreground/background/killed) deferred — key types verified prior round; routing now code-correct.