Closer/ClaudeReport.md

65 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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** ("<partner> started <Game>" + 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 `077a408`→`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 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).