Closer/ClaudeReport.md

18 KiB
Raw Blame History

Claude QA Report — Full-App QA (living report)

Verdict (2026-06-26): 0 open P0P2 (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 DJ.

  • 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): NEW notifications/GamePromptController.kt, ui/components/GamePromptBanner.kt; edited core/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/leave to WheelSessionViewModel — 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 + 15 scale + free-text); per-Q You/QA breakdown renders; completed, 0 active. (Helper wheel_drive.py handles 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 CouplePremiumChecker except SubscriptionScreen (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-gated secure subdoc, onAnswerWritten both-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: onAnswerWritten fires (both-answered "unlocked " copy is in deployed code); onAnswerRevealed fired live (isRevealed flip → "notified partner that X opened"). 0 FATAL either device. Today's test answers wiped after; baseline clean. One low-sev robustness note → Future.md (reveal isRevealed write 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, coupleId Xal3Kw3gjSdn0niERYKJ, both free (baseline restored).
  • Docs: Playbook ClaudeQAPlan.md · Coverage ClaudeQACoverage.md · Ideas Future.md ## QA · Branding ClaudeBrandingReview.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 ~4245dp wide (48dp tall) — single-axis marginal miss of the 48dp target; fully operable. Most controls are 48dp. Pass J: uiautomator bounds on conversation → 23 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/premium denied (server-only → no self-grant); cross-couple denied.
  • D4 keys: couple key phrase-wrapped (argon2id); recovery phrase server-blind; encryptedRecoveryPhrase wiped on acceptance; plaintext inviteCode not 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 → onNewIntent never delivered the tap's extras → pendingDeepLink unset), and even when routed, the game screen showed setup instead of joining (one-shot getActiveSessionForCouple raced the post-push Firestore sync → returned stale-empty). Fixes: AndroidManifest MainActivity launchMode=singleTop + QuestionSessionRepositoryImpl.getActiveSessionForCouple now 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 _dark variants → 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 (A1A12, source gitignored) wired into their screens via shared EmptyState + new BrandIllustration helper (commits 077a4085868d06). 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 on 768f511. Details in ClaudeBrandingReview.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 P0P2.
  • 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 P0P2.
  • 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.
  • R1R4 — baseline Passes AF report-only; every P0P2 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-hasPremium fields (tighten to a field allowlist).