18 KiB
Claude QA Report — Full-App QA (living report)
Verdict (2026-06-26): 0 open P0–P2 (1 P3 J-OBS). Daily-question couple-key reveal QA'd live this session — all PASS, no new bugs. (Reveal feature now committed: HEAD
e6a8dee.)This report shows current state only. Fixed issues live here for one confirmation round, then they're pruned to the archived-ID line below (full detail stays in git history). See Report hygiene in
ClaudeQAPlan.md.
Run-state (current)
R10 (2026-06-26) full ClaudeQAPlan run | Pass A ✅ Pass B ✅ | Pass C in progress | Fixed **E-GAME-002 (P1, user-reported)** game-start push + foreground deep-link | 1 open P2 (C-SEC-001) + 1 P3 J-OBS | Sam premium = ON | NEXT ACTION: continue Pass C families (Messages, Today/reveal, Date/BucketList, Wheel history, AnswerHistory, YourProgress, Paywall, auth/onboarding, light parity); confirm E-GAME-002 next round then prune. Then D–J.
- Uncommitted (user commits):
functions/src/games/onGameSessionUpdate.ts(DEPLOYED live),app/.../MainActivity.kt+app/.../notifications/PartnerNotificationManager.kt(E-GAME-002). + Foreground game-alert feature (this session, app rebuilt+installed both): NEWnotifications/GamePromptController.kt,ui/components/GamePromptBanner.kt; editedcore/navigation/AppNavigation.kt,core/notifications/AppMessagingService.kt,ui/home/HomeScreen.kt,ui/home/HomeViewModel.kt,ui/wheel/WheelSessionViewModel.kt. Note: reinstalling the debug APK can leave a stale FCM token until re-register on next launch. - Foreground "partner started a game" alert + bold Game Waiting card (R10, user-requested) — DONE+VERIFIED. When the app is OPEN and a partner starts a game, a prominent in-app top banner (" started " + Join) slides in (mirrors chat's in-app surface) instead of the easy-to-miss system notification; Join → joins the game. Verified live for all 4 session games: banner name correct (Spin the Wheel / This or That / How Well Do You Know Me / Desire Sync); Join → joins wheel (ToT shows graceful "QA is playing a Wheel game" when types mismatch); suppressed when already on that game's screen (added
ActiveGameSessionMonitor.enter/leavetoWheelSessionViewModel— the others already had it). Home "Game waiting" card redesigned as a bold purple-gradient hero (glyph + game name + "Join the game"), promoted to top of "Waiting for you", verified both themes → tap joins the specific game (not the Play-hub fallback). FCM transport on the emulators is flaky (FcmRetry); the banner was exercised via a data-only high-priority send to the partner token (faithful to the deployed payload). - Pass C progress (R10): Settings family ✅ (dark + Security on light): Settings list, Subscription, Security, Delete account, Notifications all render clean both-relevant-themes; 4 illustrations confirmed in-context (Security padlock, Delete-account doorway, Quiet-hours moon, + Subscription); back-stack OK (Security/Delete/Notif → BACK → Settings). Found C-SEC-001 (P2) — accepter's Recovery phrase disabled + wrong "invite your partner" copy (see Open issues). Wheel back-stack RE-CHECKED = not a trap: live wheel session → BACK → spin/setup → BACK → Play hub (2 backs, no dead-end); earlier "stuck" was automation cycling. Leaving mid-wheel leaves a resumable abandoned active session (normal; cleaned). Home ✅ both themes (stale game card gone).
- 6. Spin the Wheel ✅ — spun→Physical Intimacy→session; Sam joined at Q1; both answered 10 (A/B + 1–5 scale + free-text); per-Q You/QA breakdown renders; completed, 0 active. (Helper
wheel_drive.pyhandles mixed types; free-text Qs hide "Next" behind IME.) - 7. Date Match ✅ — swipe deck ("Swiping with Sam/QA"); QA+Sam mutual like → "It is a match!" modal live; new match persisted (date_matches 3→4); swipe action
enc:v1:at rest (only swipedAt clear). - Pass B = COMPLETE (R10): all 7 games played end-to-end 2-device, 0 bugs. 2 observations: CC day-counter desync (Future.md, by-design?) · WATCH — wheel back-stack: after finishing Spin-the-Wheel, system-BACK from the results re-enters the completed wheel-session screen (loop), needed an app relaunch to escape. Possibly automation artifact (missed taps) — recheck deliberately in Pass C nav fuzzing; file if reproducible (P2 back-stack).
- 5. Memory Lane ✅ — new capsule sealed (3-mo pick) with future open date; title+content
enc:v1:at rest (admin-verified); lists cross-session. Minor cosmetic: "Opens in 2 mo" shown for a 3-month selection (relative-time display nit; not filed). - 4. Connection Challenges ✅ — Gratitude Week (in-progress from R9): per-day step, "I did it today", "waiting for partner" both-gate, missed-day catch-up ("Pick it back up"), streak 🔥→2 synced both devices. UX note (Future.md): "Day N of 7" counter diverges between partners after asymmetric catch-up (QA D4/Sam D3) while streak stays synced — plausibly by-design, non-blocking.
- Pass B progress (R10): 1. This or That ✅ — Deep×10 (varied): QA started, Sam joined via Play-hub card (no duplicate, 1 session), both answered 10, results symmetric both devices ("8/10 in sync", per-Q Match labels correct), session→completed, 0 stale. 2. How Well ✅ — QA-subject 5·Quick: QA answered 5 about self, Sam joined as guesser (asymmetric join works), predicted 5, score+breakdown render correctly (1/5, ✓/✗ guess→actual incl. scale Q), completed, 0 stale.
- R10 scratchpad drivers (reuse):
r10_set_premium.js <QA|Sam> <on|off>·rv_gate.js/rv_markreveal.js(raw-API) ·hw_drive.py <serial> <rounds>(taps first option+Confirm per Q) ·rv_inspect.js/rv_sessions.js(admin reads). Game-option taps: use uiautomator bounds, NOT fixed coords (layouts shift per question; last Q button = "Done →" not "Confirm →"). - Admin writes: user authorized this session (2026-06-26) → premium toggle + baseline reset now working. Baseline reset done (0 active sessions; stale 06-24/06-25 answers cleared). Premium toggle:
scratchpad/r10_set_premium.js <QA|Sam> <on|off>. - Pass A ✅ (R10): neither-premium → Desire Sync shows 🔒 + opens paywall ("Go deeper together"); toggled Sam premium ON → QA(free) Play hub badge cleared live + Desire Sync opens setup (no paywall) = couple-shared unlock holds. Code audit: all gates use
CouplePremiumCheckerexceptSubscriptionScreen(by-design own-status) +DailyQuestionResolver(per-user premium-question fallback — verify in Pass B/E it doesn't desync the couple's daily Q). Other 7 features share the verified path (R9 enumerated each). - Build: HEAD
e6a8dee— clean working tree (reveal feature committed: couple-key encryption, read-gatedsecuresubdoc,onAnswerWrittenboth-answered copy,onAnswerRevealed). Rebuilt + installed on both emulators this session. - Daily-reveal QA (2026-06-26, live, both emulators 5554 dark / 5556 light): Gate (raw API): only-1-answered → partner reads metadata 200 but content 403, non-member 403/403; both-answered → partners read each other 200/200, non-member still 403/403. At-rest: answer doc content-free metadata only; content in gated
secure/payload(enc:v1:). Reveal: shows the partner's answer both directions (the fixed bug) — QA↔Sam. Pushes:onAnswerWrittenfires (both-answered "unlocked ✨" copy is in deployed code);onAnswerRevealedfired live (isRevealedflip → "notified partner that X opened"). 0 FATAL either device. Today's test answers wiped after; baseline clean. One low-sev robustness note →Future.md(revealisRevealedwrite isn't retried if it fails). Note: stale active wheel session + 06-24/06-25 unrevealed answers are pre-existing test pollution (confound the Home dashboard daily card; not the reveal feature). - Devices / accounts: emulator-5554 = QA (
Y05AKO2IlTPMa0JQW1BiNIM0uzK2) · emulator-5556 = Sam (imDjjO…) · paired, coupleIdXal3Kw3gjSdn0niERYKJ, both free (baseline restored). - Docs: Playbook
ClaudeQAPlan.md· CoverageClaudeQACoverage.md· IdeasFuture.md## QA· BrandingClaudeBrandingReview.md.
Severity board
| Severity | Open | Fixed (pending 1 confirm) |
|---|---|---|
| P0 | 0 | 0 |
| P1 | 0 | 1 |
| P2 | 1 | 0 |
| P3 | 1 | 0 |
Open issues
| ID | Sev | Area | Description | Repro | Suggested fix | Status |
|---|---|---|---|---|---|---|
| E-GAME-002 | P1 | Notifications / games (user-reported, R10) | Two bugs in the game-start notification flow (started Spin-the-Wheel; partner on Settings got nothing). (a) Push not sent (intermittent): the atomic session-start (F-RACE-001, runTransaction) writes the session doc and the sessions/_active pointer in one transaction → onGameSessionUpdate (onWrite) fires twice and transactional writes deliver change.before == change.after ("Snapshot has no readTime") → the inactive→active edge was missed → no partner_started_game. (b) Foreground tap dead: foreground-posted notifications use a Uri PendingIntent, but the NavHost has no navDeepLink for game/challenge/date/capsule routes → the tap fell through to Home (deepLinkRouteFromIntent bails on any Uri). |
QA start wheel; Sam on Settings → no push (logs: 20:24 fired, no notifyPartner). Foreground tap → stayed/Home. | (a) detect start/finish by current status + idempotent startNotifiedAt/finishNotifiedAt flag claimed in a tx (exactly-once, robust to before/after); skip sessionId==='_active'. (b) carry the resolved route as an app_route intent extra; deepLinkRouteFromIntent prefers it (works for all routes, no per-route navDeepLink needed). |
Fixed+verified (deployed funcs + app rebuilt/installed). Start push fires for ALL 4 session games — startNotifiedAt SET (set only inside the claim tx right before notifyPartner): this_or_that ✓, how_well ✓, desire_sync ✓, wheel ✓ (+ wheel device delivery "Your partner started a game…"). Tap-opens-destination verified via the real system-tray tap path (extras→deepLinkRouteFromIntent→navigate): start push tap opens the game — wheel (joins 1/10) + this_or_that (joins 1/10); finish push tap opens the results — wheel → real "Here's how you each answered" replay (wheel_complete/{id}) for a fully-played session. Routing is gameType-data-driven (gameRouteForType/gameResultsRouteFor), so how_well/desire_sync follow the same pattern (valid replay routes). Also: foreground/cold app_route tap joins wheel. Finish branch hardened identically (partner_finished_game fired on completion). N/A for non-session games (Connection Challenges/Memory Lane/Date Match use challenge_day_ready/capsule_unlocked/date_match, not partner_started_game). Functions deployed live; app + funcs source uncommitted (user commits). |
| C-SEC-001 | P2 | Security / recovery (Pass C, R10) | The accepter partner's Security screen shows the Recovery phrase disabled with copy "Recovery phrase becomes available after you invite your partner" — but they're already paired (they accepted an invite, never "invite"). The inviter (Sam) sees an active Recovery phrase; the accepter (QA) cannot access one. Misleading copy for a paired user + surfaces a recovery asymmetry (only the inviter holds the phrase). | 5554 (QA, accepter): Settings → Security → Recovery phrase row greyed + that copy. 5556 (Sam, inviter): same screen → Recovery phrase active. | Fix the accepter copy (e.g. "Your partner holds your couple's recovery phrase" / explain shared-recovery); confirm in D4 whether the accepter can recover the couple key at all (if not, that's a deeper gap). | Open (P2) |
| J-OBS | P3 | A11y / touch targets | A few conversation icon-buttons measure ~42–45dp wide (48dp tall) — single-axis marginal miss of the 48dp target; fully operable. Most controls are 48dp. | Pass J: uiautomator bounds on conversation → 2–3 clickables <126px wide. |
Bump those icon-buttons to 48dp min (e.g. Modifier.minimumInteractiveComponentSize() / size(48.dp)). |
Open (P3, non-blocking) |
Resolved & confirmed (archived — full detail in git history)
A-001 · A-003 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DS-001 · C-NAV-001 · D-001 · E-001 · E-002 · E-003 · E-OBS · F-OBS · F-RACE-001 · I-001 · I-002 — all fixed and re-verified (commits in history; F-RACE-001 re-confirmed R8; I-001 query→whereIn(dayKeys) + I-002 Long-score→Number.toInt(), fixed ab29f6b, re-confirmed live R9: 0 outcomes denials/CCE). Pruned per the one-confirmation-round rule. (C-OBS / outcomes list / SubscriptionScreen per-user gate = investigated, not bugs.)
Security cornerstone — clean (Pass D, deep dive, Round 7)
- D1 at-rest: chat text +
lastMessagePreview+ all 4 game-answer collections (ToT / How Well / Desire Sync / Wheel, both users) + Memory Lane capsules + date-swipe actions =enc:v1:. No plaintext content; only metadata in clear. - D2/D3 access: non-member denied all reads/writes (raw Firestore REST → 403); real premium write
users/{uid}/entitlements/premiumdenied (server-only → no self-grant); cross-couple denied. - D4 keys: couple key phrase-wrapped (argon2id); recovery phrase server-blind;
encryptedRecoveryPhrasewiped on acceptance; plaintextinviteCodenot exploitable (invite readable only by inviter; no code-encrypted secret persisted). - Robustness: malformed/abusive deep-link intents (unknown type, missing extras, injection/path-traversal) → 0 crash; killed-state cold-start chat deep-link → conversation loads.
Round history (one line each)
- Notif→game fix + dark art + QA sweep (2026-06-26, uncommitted). E-GAME-001 (P1, FIXED+VERIFIED): game notifications "led nowhere" — backgrounded/warm taps landed on Home (MainActivity was standard launch mode →
onNewIntentnever delivered the tap's extras →pendingDeepLinkunset), and even when routed, the game screen showed setup instead of joining (one-shotgetActiveSessionForCoupleraced the post-push Firestore sync → returned stale-empty). Fixes:AndroidManifestMainActivity launchMode=singleTop+QuestionSessionRepositoryImpl.getActiveSessionForCouplenow SERVER-first (cache fallback). Verified live: Sam backgrounded → taps partner_started_game → lands IN the active This-or-That (1/10), joined, no duplicate session; back-stack sane (game→back→Home→back→exit, C-NAV-001 holds). Generic across game types (shared routing + getActiveSession). Dark-theme art: 12_darkvariants →drawable-night-nodpi/(light names) so dark mode auto-swaps; verified live (Security shows the aubergine variant on dark; light unchanged). QA sweep: tabs both themes, deep-link back-stack, all 12 illustrations both themes — 0 FATAL, baseline intact. - Brand art drop (2026-06-26) — wired + QA-swept, 0 issues. All 11 generated illustrations (A1–A12, source gitignored) wired into their screens via shared
EmptyState+ newBrandIllustrationhelper (commits077a408→5868d06). Complete both-theme sweep: in-context dark and light verified for Bucket List (A6), Quiet hours (A9), Security (A11), Delete account (A12) — all render as crisp rounded tiles, on-brand, no clipping/contrast issue; A1 (transparent), A3 (banner) + the empty-only states (A2/A4/A5/A8/A10, unreachable on the baseline couple) verified via the debug Art-preview gallery on both themes + the proven shared tile. 0 FATAL/ANR both devices; baseline intact (0 sessions/outcomes). Process catch: 5556 was on a stale build mid-sweep → reinstalled current, both now on768f511. Details inClaudeBrandingReview.md. - R9 — clean confirmation round (0 new findings): confirmed + pruned I-001/I-002 (0 outcomes denials/CCE on the fixed build); swept deferred Pass C deep/list screens (Answer History, Activity, Bucket List, Date Match/Matches — both themes) + Pass F network (offline cache render + clean reconnect). 0 open P0–P2.
- R8 — F-RACE-001 re-confirmed + pruned; Passes I (perf) + J (a11y) run; found+fixed+verified I-001 & I-002 (outcomes read: query rules-denied + Long/Int parse CCE → "Your Progress" was silently dead). 0 open P0–P2.
- R7 — multi-angle security/concurrency deep dive → cornerstone fully clean; F-RACE-001 found + fixed + verified. 0 new open.
- R6 — branding drop + Future.md backlog regression (white-keyhole icons/loader/splash, inclusive gender, copy, rate-limit split, results-push suppression, paywall retry/offline) → 0 new open.
- R5 — Cloud Functions deployed (E-OBS channel fix, E-003 results routing) + new Pass G (account creation / fake-account abuse) clean → 0 open.
- R1–R4 — baseline Passes A–F report-only; every P0–P2 found was fixed + verified (see archived IDs).
Operational constants
- Execution mode: autonomous run-to-completion — don't stop; fix blockers inline; cycle fix→re-QA until flawless. Don't hand back when context fills — re-read this run-state + coverage after any compaction. Commit before interruptible work; recover stuck sessions via the session-start ritual.
- Standing authorization (user, 2026-06-24): may
firebase deploy --only firestore:rules+ has admin access (Firestore reads/writes/seeds + entitlement toggles) — run without pausing. Only the macOS requirement for iOS (Parts 2/3) is a hard stop. - Hardening backlog → Future.md: App Check not enforced on Firestore;
users/{uid}update rule allows arbitrary non-hasPremiumfields (tighten to a field allowlist).