docs: R25 coverage entries, instrumented test landmine warning, tokenized snapshot URL note

This commit is contained in:
null 2026-06-30 23:33:54 -05:00
parent 88f2350c9c
commit 38eae8f915
4 changed files with 19 additions and 1 deletions

View File

@ -1,6 +1,9 @@
# Claude QA Coverage Matrix # Claude QA Coverage Matrix
> **Resume anchor — current status only.** Statuses: `pass | fail→id | todo | n/a | not implemented→Future.md | blocked→id`. > **Resume anchor — current status only.** Statuses: `pass | fail→id | todo | n/a | not implemented→Future.md | blocked→id`.
> **R25 (2026-06-30) — full fresh run on the new R24 E2EE backup/restore surface + cornerstone regression; 0 new defects.** Cheap gates green (unit **244** · fn **38** · theme-scan CRIT **1=false-pos** [HomeScreen:829 brand count pill] · painter-xml **0** · wiring 🔴**0** · cold-start smoke **6/6 both** · render smoke **4/4**). **Pass D CLEAN (deep on backup/restore):** at-rest manifest=pointers-only, Storage snapshot=`enc:v1:` (16KB, server-blind), restore_requests=0; rules member-scoped + keybox bound to other member + immutable pubkey; **D3 live negative all-denied** (backup manifest/chunks/restore_requests + create/write = 403/400; original couple/self-grant = 403); cross-user restore via **tokenized capability URL** verified (plain GET→200+`enc:v1:`); **R24 storage.rules deploy gap RESOLVED**. **Pass E:** `restore_requested` partner push **deployed + firing** (Sam queue: 3 today) → RESTORE_CONSENT; **R24-b functions NOT deployed** (`onRestoreFulfilled` absent, no `lastRestoreSelfAlertAt`, 0 `restore_self_alert`) → self-alert + completion alert = `blocked→deploy`. **Pass M:** new Security entries live-verified this session (recovery reveal on no-lock, Copy+`IS_SENSITIVE` mask, Help-my-partner-restore + back). **Cornerstone regression (Sam 5556):** A paywall gate ✓, B Play-hub cards+badges ✓, L inbox+thread fully decrypted no-leak ✓, N daily-Q decrypted+reveal ✓, 0 FATAL. **⚠️ PROCESS LANDMINE (I caused): `connectedDebugAndroidTest` UNINSTALLS+WIPES the app-under-test → wiped QA(5554) data → QA at fresh onboarding (O-ONBOARD-001 stays fixed). NEVER run instrumented tests on 5554/5556 fixtures — use throwaway 5558.** QA fixture recovery = `blocked→user` (password/re-auth needed). **User actions to close: (1) `firebase deploy --only functions` ✅ DONE (functions deployed by user; both self-alerts live-validated R25-c). (2) restore QA fixture ✅ DONE R25-b.**
> **R25-b (2026-06-30) — QA(5554) fixture RECOVERED; live 2-device partner-assisted restore verified end-to-end, 0 defects.** Password reset via admin (user-authorized) → QA signed in → **NEEDS_RECOVERY** → "Start restore" published request (code 592847) → **deployed `onRestoreRequested` fired LIVE** (Sam got "Help your partner restore 💜" push, id 40038) → Sam's **Change-1 consent live-verified** (email anchor + name **QA** decrypted locally + confirm checkbox; Approve gated on code(6) **AND** confirm) → approve → **QA auto-restored**: paired Home + "Sam/Revealed" + **full chat history decrypts**; fresh `restore_ok_R25` from restored QA **decrypts on Sam** = bidirectional round-trip = full R24 restore regression. **Deferred obs (not a defect):** warm-start restore-push tap opened Play hub not RESTORE_CONSENT (likely collapsed-notif-group artifact; cold-start routing smoke-green).
> **R25-c (2026-06-30) — LIVE-FIRE of deployed owner-alerts (Change 3): both restore self-alerts observed on QA's OWN device; last user-gate CLOSED; 0 defects.** User-authorized re-wipe QA(5554) → sign in → NEEDS_RECOVERY → Start restore (code 565429). **(1) Request self-alert fired LIVE** — QA **shade** (`closer.app` id 67945, `partner_activity`, imp 4) + durable `users/{QA}/notification_queue` (`restore_self_alert`, 23:17:38 “New device is restoring your history”) + partner push to Sam (“Help your partner restore 💜”) — all from one `onRestoreRequested`. **R25-b routing obs CLOSED:** tapping Sams *single* restore notif → RESTORE_CONSENT (not Play hub) ⇒ earlier artifact was the collapsed 2-item group header. Consent gate re-verified (code(6) alone Approve-disabled → +confirm enabled). Sam approve → **`onRestoreFulfilled` fired (status ok, 1319ms) on REQUESTED→READY** → **(2) completion self-alert** queued to QA (`restore_self_alert`, 23:19:50 “Your history was just restored”) — not on shade only because QA was foregrounded (auto-restored); push still reached live tokens. 132s apart (>~60s dedupe → no suppression). **Robustness live:** 1 stale token (`registration-token-not-registered`) failed but `Promise.allSettled` → function ok. QA auto-restored to paired Home + content decrypts, 0 FATAL → **fixture healthy**. **Minor follow-on (not defect):** prune `not-registered` FCM tokens.
> **R21 (2026-06-29) — brand-voice + Home-bubble polish, then full QA re-run; 0 new defects, 0 FATAL.** Copy: `prompt→question` (~26 strings) + clinical→Closer voice (Outcome/check-in feature, "Your Progress"→"Growing together", "Private sync"→"Just for two", Home eyebrow "Your daily question", paywall "…and growth"). Home partner bubble upgraded (Coil `SubcomposeAsyncImage` + gradient ring + a11y; partner photo verified live). Cheap gates all green (210 unit · 24 fn · theme-scan CRIT 0 · painter-xml 0 · smoke 6/6 both). **Reveal-when-answered verified LIVE end-to-end** (both answer → Home "Reveal is ready / Reveal together" → AnswerReveal shows both picks). Multi-angle nav verified (daily Q via Today+Home, reveal via Home card, Settings→Growing together, Play→Question Packs "250 questions"). Cornerstones A/B/D carry from R20 (no rules/crypto/games-logic change — diff is copy + Home-bubble UI); E re-verified (smoke). Also landed this session (uncommitted): recovery-UX partner-as-backup copy + change-phrase desync guard, `SECURITY.md`, first instrumented test `FirstRunRenderSmokeTest`. See `ClaudeReport.md` R21. > **R21 (2026-06-29) — brand-voice + Home-bubble polish, then full QA re-run; 0 new defects, 0 FATAL.** Copy: `prompt→question` (~26 strings) + clinical→Closer voice (Outcome/check-in feature, "Your Progress"→"Growing together", "Private sync"→"Just for two", Home eyebrow "Your daily question", paywall "…and growth"). Home partner bubble upgraded (Coil `SubcomposeAsyncImage` + gradient ring + a11y; partner photo verified live). Cheap gates all green (210 unit · 24 fn · theme-scan CRIT 0 · painter-xml 0 · smoke 6/6 both). **Reveal-when-answered verified LIVE end-to-end** (both answer → Home "Reveal is ready / Reveal together" → AnswerReveal shows both picks). Multi-angle nav verified (daily Q via Today+Home, reveal via Home card, Settings→Growing together, Play→Question Packs "250 questions"). Cornerstones A/B/D carry from R20 (no rules/crypto/games-logic change — diff is copy + Home-bubble UI); E re-verified (smoke). Also landed this session (uncommitted): recovery-UX partner-as-backup copy + change-phrase desync guard, `SECURITY.md`, first instrumented test `FirstRunRenderSmokeTest`. See `ClaudeReport.md` R21.
> **R20 (2026-06-29) — fresh full ClaudeQAPlan run; found + FIXED 2 escaped bugs.** Build HEAD `62696a6` + R20 fixes (uncommitted: `QuestionSessionRepositoryImpl.kt`, `GameSessionManager.kt`, `HomeViewModel.kt`). Cheap gates all green (unit **210** · fn **24** · theme-scan CRIT **0** · painter-xml **0** · wiring 🔴**0** · smoke **6/6 both**). Cornerstones A/B/D/E live-clean, 0 FATAL. **B-ABANDON-001 (P2)** — Quit/abandon on any game silently `PERMISSION_DENIED` (full `saveSession` set drops server-only flags → rule rejects removed `affectedKeys`) → stranded session; **fixed** (targeted `update(status,completedAt)`), verified live (quit→active=0→new game starts). **B-COPY-001 (P3)** — Home GAME_WAITING hero falsely claimed "partner already played their part"; **fixed** (neutral partner-named copy), verified live both devices. Both pending 1 confirm. See `ClaudeReport.md` R20 run-state. > **R20 (2026-06-29) — fresh full ClaudeQAPlan run; found + FIXED 2 escaped bugs.** Build HEAD `62696a6` + R20 fixes (uncommitted: `QuestionSessionRepositoryImpl.kt`, `GameSessionManager.kt`, `HomeViewModel.kt`). Cheap gates all green (unit **210** · fn **24** · theme-scan CRIT **0** · painter-xml **0** · wiring 🔴**0** · smoke **6/6 both**). Cornerstones A/B/D/E live-clean, 0 FATAL. **B-ABANDON-001 (P2)** — Quit/abandon on any game silently `PERMISSION_DENIED` (full `saveSession` set drops server-only flags → rule rejects removed `affectedKeys`) → stranded session; **fixed** (targeted `update(status,completedAt)`), verified live (quit→active=0→new game starts). **B-COPY-001 (P3)** — Home GAME_WAITING hero falsely claimed "partner already played their part"; **fixed** (neutral partner-named copy), verified live both devices. Both pending 1 confirm. See `ClaudeReport.md` R20 run-state.
> Build = **R18b working tree** (uncommitted: Wheel finish-gate + `partner_joined_game`/banner-standardization client+functions+rules + portrait lock + docs — full file list in `ClaudeReport.md` run-state); **210 unit + 24 functions tests green**; debug APK rebuilt+installed both emulators. **Deploy status:** `functions/` + `firestore.rules` **DEPLOYED by user** (join push live; Tier-2 self-constraint **verified live** — member own-uid add 200, foreign-uid/removal 403). No remaining deploy gates. Position + verdict: see `ClaudeReport.md` R18b run-state. **R18b polish/hardening round (latest):** fixed **E1 (P2)** Wheel silently-swallowed submit failure → retryable error (no false reveal); modern banner/bubble feel (haptics, spring, JOINED presence dot, tap+swipe, a11y, persistent-not-clobbered); predictive back (`enableOnBackInvokedCallback`); Wheel "Quit game" abandon; Tier-2 rules self-constraint. Pass-E smoke 6/6 both. **Verdict: R18 — fixed the last open visual P2 C-DARKART-002** (uiMode-sync in `MainActivity` via `AppCompatDelegate.setDefaultNightMode` so ALL art follows the in-app theme; verified live across all 4 theme/art states) + flaky **TEST-002** (capsule determinism — injected clock) + content **P-GRAMMAR-001** (13 stress-Q subject-verb agreement errors → asset data fix). Live passes this round: **A** (premium gate, Desire Sync + premium pack → Paywall; free content reachable), **B** (Wheel playthrough end-to-end), **L** (chat E2E send/receive-decrypt/at-rest/receipt/no-leak), **E** (backgrounded FCM delivery, privacy-safe, deep-links to chat), **M-001 confirmed** (client mirror intact). **R18b (Future.md review): found+fixed a P0 — O-ONBOARD-001** onboarding/auth crash on EVERY fresh install (`painterResource` on the `<bitmap>` `ic_launcher_foreground`, a regression from icon-redesign commit 334cb07; invisible to logged-in QA emulators). Verified live before/after on fresh 5558 (API 34); fixed both `OnboardingScreen.CtaSlide` + `AuthVisuals.AuthLogoMark` → raster `closer_launcher_foreground`; added `scripts/painter-xml-scan.sh` guard (proven). Also closed BucketList FAB hardcoded color. **Board: 0 open P0/P1 · 1 open P2 (O-AGE-001 pre-ship age gate) · 1 open P3 (BRAND-DARK-COVERAGE); the full confirmed backlog was pruned this round** (O-ONBOARD-001, C-DARKART-002, 6× C-THEME, M-001, TEST-001/002, P-GRAMMAR-001, BucketList-FAB, BRAND-ICON-CUSTOM, and **C-ORIENT-001 → RESOLVED: portrait-locked in the manifest, verified live**). 0 FATAL. **R18b feature work (uncommitted; tests 209 unit + 24 functions green):** (1) **Game finish-gate** — no game can finish with unanswered questions (Wheel: skip allowed but Finish bounces to the first blank; the other 3 already require a pick); verified live end-to-end (full 2-player Wheel + This-or-That). (2) **"Partner joined your game" push + standardized durable in-app game banner** — new `partner_joined_game` (joiner's avatar) + all foreground game pushes routed through the themed banner (started/joined transient, your-turn/results persistent); verified live + Pass-E smoke 6/6 both emulators. **⚠ The join push needs `functions/` + `firestore.rules` deployed by the user to fire live.** > Build = **R18b working tree** (uncommitted: Wheel finish-gate + `partner_joined_game`/banner-standardization client+functions+rules + portrait lock + docs — full file list in `ClaudeReport.md` run-state); **210 unit + 24 functions tests green**; debug APK rebuilt+installed both emulators. **Deploy status:** `functions/` + `firestore.rules` **DEPLOYED by user** (join push live; Tier-2 self-constraint **verified live** — member own-uid add 200, foreign-uid/removal 403). No remaining deploy gates. Position + verdict: see `ClaudeReport.md` R18b run-state. **R18b polish/hardening round (latest):** fixed **E1 (P2)** Wheel silently-swallowed submit failure → retryable error (no false reveal); modern banner/bubble feel (haptics, spring, JOINED presence dot, tap+swipe, a11y, persistent-not-clobbered); predictive back (`enableOnBackInvokedCallback`); Wheel "Quit game" abandon; Tier-2 rules self-constraint. Pass-E smoke 6/6 both. **Verdict: R18 — fixed the last open visual P2 C-DARKART-002** (uiMode-sync in `MainActivity` via `AppCompatDelegate.setDefaultNightMode` so ALL art follows the in-app theme; verified live across all 4 theme/art states) + flaky **TEST-002** (capsule determinism — injected clock) + content **P-GRAMMAR-001** (13 stress-Q subject-verb agreement errors → asset data fix). Live passes this round: **A** (premium gate, Desire Sync + premium pack → Paywall; free content reachable), **B** (Wheel playthrough end-to-end), **L** (chat E2E send/receive-decrypt/at-rest/receipt/no-leak), **E** (backgrounded FCM delivery, privacy-safe, deep-links to chat), **M-001 confirmed** (client mirror intact). **R18b (Future.md review): found+fixed a P0 — O-ONBOARD-001** onboarding/auth crash on EVERY fresh install (`painterResource` on the `<bitmap>` `ic_launcher_foreground`, a regression from icon-redesign commit 334cb07; invisible to logged-in QA emulators). Verified live before/after on fresh 5558 (API 34); fixed both `OnboardingScreen.CtaSlide` + `AuthVisuals.AuthLogoMark` → raster `closer_launcher_foreground`; added `scripts/painter-xml-scan.sh` guard (proven). Also closed BucketList FAB hardcoded color. **Board: 0 open P0/P1 · 1 open P2 (O-AGE-001 pre-ship age gate) · 1 open P3 (BRAND-DARK-COVERAGE); the full confirmed backlog was pruned this round** (O-ONBOARD-001, C-DARKART-002, 6× C-THEME, M-001, TEST-001/002, P-GRAMMAR-001, BucketList-FAB, BRAND-ICON-CUSTOM, and **C-ORIENT-001 → RESOLVED: portrait-locked in the manifest, verified live**). 0 FATAL. **R18b feature work (uncommitted; tests 209 unit + 24 functions green):** (1) **Game finish-gate** — no game can finish with unanswered questions (Wheel: skip allowed but Finish bounces to the first blank; the other 3 already require a pick); verified live end-to-end (full 2-player Wheel + This-or-That). (2) **"Partner joined your game" push + standardized durable in-app game banner** — new `partner_joined_game` (joiner's avatar) + all foreground game pushes routed through the themed banner (started/joined transient, your-turn/results persistent); verified live + Pass-E smoke 6/6 both emulators. **⚠ The join push needs `functions/` + `firestore.rules` deployed by the user to fire live.**

View File

@ -36,7 +36,10 @@ process rule, make sure it doesn't contradict the Guardrails.
`scripts/painter-xml-scan.sh` (crash guard — `painterResource()` on a non-`<vector>` XML drawable throws on render; `scripts/painter-xml-scan.sh` (crash guard — `painterResource()` on a non-`<vector>` XML drawable throws on render;
caught O-ONBOARD-001 class — exit≠0 is a P0 gate); (c) the **instrumented render smoke** (when an emulator is attached) caught O-ONBOARD-001 class — exit≠0 is a P0 gate); (c) the **instrumented render smoke** (when an emulator is attached)
`./gradlew :app:connectedDebugAndroidTest` runs `FirstRunRenderSmokeTest` (first-run composables paint in light+dark; `./gradlew :app:connectedDebugAndroidTest` runs `FirstRunRenderSmokeTest` (first-run composables paint in light+dark;
the on-device net for the "composes fine, crashes on first paint" class — a red run is a P0 gate); (d) optional the on-device net for the "composes fine, crashes on first paint" class — a red run is a P0 gate). **⛔ RUN THIS ON A
THROWAWAY (5558), NEVER on the 5554/5556 fixtures — `connectedDebugAndroidTest` UNINSTALLS the app-under-test on
completion, which WIPES its data (auth + couple keys + App-Check debug token), same effect as the forbidden `pm clear`;
R25 wiped QA(5554) this way and had to surface a user-gated re-auth to recover it.** (d) optional
**monkey fuzz** `adb shell monkey -p app.closer --throttle 300 --pct-touch 90 -v 5000` (any crash = bug). File 🔴/🟠 to **monkey fuzz** `adb shell monkey -p app.closer --throttle 300 --pct-touch 90 -v 5000` (any crash = bug). File 🔴/🟠 to
`ClaudeReport.md`; record counts in coverage. `ClaudeReport.md`; record counts in coverage.
4. **Run the passes report-only**, sub-batched to one context window each — recurring set **AN + P** (K money-path + 4. **Run the passes report-only**, sub-batched to one context window each — recurring set **AN + P** (K money-path +
@ -445,6 +448,9 @@ Context-cost tips: prefer **code/admin-read audits** (cheap) before live UI swee
state lives in those files (they survive compaction), not in a commit you make. Never stage, commit, push, branch, or state lives in those files (they survive compaction), not in a commit you make. Never stage, commit, push, branch, or
amend. amend.
- **Never `pm clear` / wipe app data** — breaks the App Check debug token. Pre-pairing QA: sign-out → fresh sign-up. - **Never `pm clear` / wipe app data** — breaks the App Check debug token. Pre-pairing QA: sign-out → fresh sign-up.
- **Never run `connectedDebugAndroidTest` (or any instrumented test) on the 5554/5556 fixtures** — AGP UNINSTALLS the
app-under-test after the run, wiping its data (auth + couple keys + App-Check token) exactly like `pm clear`. Run the
render smoke on a **throwaway (5558)**. (R25 wiped QA/5554 this way → needed a user-gated re-auth to recover.)
- **Never run `seed/build_db.py`.** Admin seeds/writes, entitlement toggles, and any deploys are **user-authorized per occurrence**. - **Never run `seed/build_db.py`.** Admin seeds/writes, entitlement toggles, and any deploys are **user-authorized per occurrence**.
- **By-design vs bug:** if a finding may be intended behavior, **log it and keep going** (don't stop to ask; don't unilaterally rewrite deliberate design — the log captures it). - **By-design vs bug:** if a finding may be intended behavior, **log it and keep going** (don't stop to ask; don't unilaterally rewrite deliberate design — the log captures it).
- **Pass C parallelism:** set **5554 = Dark, 5556 = Light** to capture both themes at once. - **Pass C parallelism:** set **5554 = Dark, 5556 = Light** to capture both themes at once.

File diff suppressed because one or more lines are too long

View File

@ -1188,6 +1188,12 @@ OOB code = `truncate6(SHA-256(pubkey ‖ nonce))`.
- **Backup blobs live under `users/{uid}/backups/`** (uploader-scoped write, so Storage rules can authorize it + - **Backup blobs live under `users/{uid}/backups/`** (uploader-scoped write, so Storage rules can authorize it +
the existing `onUserDelete` `users/{uid}/` cleanup covers them). Do NOT move them under `couples/` without adding the existing `onUserDelete` `users/{uid}/` cleanup covers them). Do NOT move them under `couples/` without adding
membership-checked write auth (Storage rules can't read Firestore) + a `couples/` delete cleanup. membership-checked write auth (Storage rules can't read Firestore) + a `couples/` delete cleanup.
- **The PARTNER reads that uploader-scoped snapshot during restore via the tokenized `?token=` download URL** stored
in the couple-gated manifest (`downloadBytes` does a plain HTTP GET on the URL — the token is the capability), NOT
the uid-scoped Storage read rule (which only lets the owner read their own). The blob is `enc:v1:` so even a leaked
URL yields ciphertext. Re-introduction hazard: if you switch restore to the authenticated SDK download or drop the
manifest's tokenized URL, cross-user (partner-assisted) content restore silently breaks once chunks compact into the
snapshot. Verified R25: plain GET on the manifest `snapshotUrl``200` + `enc:v1:` for a non-owner.
- **Partner-assist security is the OOB code.** B must **type** the 6-digit code A reads aloud before wrapping the - **Partner-assist security is the OOB code.** B must **type** the 6-digit code A reads aloud before wrapping the
couple key; the code is a fingerprint of the exact pubkey in the request doc (defeats a server/MITM pubkey swap + couple key; the code is a fingerprint of the exact pubkey in the request doc (defeats a server/MITM pubkey swap +
account-takeover). Never reduce it to a tap-to-approve. Consume (delete) the request after unwrap so no wrapped account-takeover). Never reduce it to a tap-to-approve. Consume (delete) the request after unwrap so no wrapped