5.7 KiB
Claude QA Report — Full-App QA (living report)
Verdict (2026-06-25, R8): 0 open P0–P2 (1 P3 J-OBS, non-blocking). I-001 + I-002 (outcomes read) fixed + verified live. Security cornerstone clean. At the flawless bar.
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)
Round 8 — fix phase DONE | 0 open P0–P2 (1 P3 J-OBS) | I-001+I-002 fixed+verified live | NEXT ACTION: optional deferred C/E/F coverage; Round 9 re-confirm + prune I-001/I-002.
- Build: client HEAD
23dd6a7, Cloud Functions deployed. - 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 | 2 (I-001, I-002) |
| P2 | 0 | 0 |
| P3 | 1 | 0 |
Open issues
| ID | Sev | Area | Description | Repro | Suggested fix | Status |
|---|---|---|---|---|---|---|
| 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) |
Fixed this round — pending one confirmation round (then prune)
| ID | Sev | Area | Fix | Status |
|---|---|---|---|---|
| I-001 | P1 | Outcomes read — query rules-denied | getOutcomes() now scopes the query with .whereIn(FieldPath.documentId(), OUTCOME_DAY_KEYS) (the 4 allowed dayKeys) so it satisfies firestore.rules:658 instead of issuing a denied bare-list. |
Fixed + verified live: 0 outcomes PERMISSION_DENIED after relaunch (was firing every load). |
| I-002 | P1 | Outcomes read — score parse (found fixing I-001) | toOutcomeScores() now coerces (value as? Number)?.toInt() instead of casting to Map<String,Int> — Firestore returns integer fields as Long on Android → the hard Int cast threw CCE → scores swallowed to emptyList(). Same shape submitOutcomeCallable writes, so it broke the real path too. |
Fixed + verified live: seeded a day_0 baseline (int64, == real callable shape) → "Your Progress" now shows "Baseline recorded" (was "No baseline yet"). Seed removed, baseline restored. |
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 — all fixed and re-verified (commits in history; F-RACE-001 fixed 23dd6a7, re-confirmed live R8: race → 1 session, loser joins same set). 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)
- 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).