feat(onboarding): RecoveryKeyManager fix, OnboardingScreen polish, build.gradle bump, Future.md planning update
This commit is contained in:
parent
b5b8ad8df9
commit
912b8c8093
|
|
@ -6,7 +6,7 @@
|
|||
>
|
||||
> **Scope expanded (plan review):** the playbook now has first-class passes **K–O** (billing money-path · messaging/chat E2E · functional settings · daily-Q/outcomes/interactive · release-build/store-readiness). These surface **coverage GAPS, not defects** — the recurring defect bar is clean, but **K (real purchase/restore/cancel path), L (full chat), M (settings take-effect), N (outcomes/Bucket-List/Date-Builder), O (minified release + App Check + store)** are `todo`/`partial`/`blocked→needs-device`. **Next-priority work = close these (start L + M on-emulator; K + O need a real device / pre-ship).** **Device/OS matrix = `blocked→needs-device` (pre-ship):** all per-round QA runs on two **identical** emulators (5554/5556, same API + screen) — minSdk/targetSdk · small/large screen · ≥1 physical device are NOT covered; don't claim "device matrix ✓".
|
||||
>
|
||||
> **First-run / cold-path = `blocked→fixture` (run the fresh-install lane):** the recurring emulators are **paired + signed-in + onboarding-complete**, so the recurring passes **cannot reach onboarding / sign-up / login / auth-logo / pairing / new-device-recovery / day-1 empty states** — this is the fixture blind-spot that hid **O-ONBOARD-001** (P0, every fresh install crashed). Cover it on a **throwaway** device (e.g. `emulator-5558` / fresh AVD — never `pm clear` 5554/5556) on any onboarding/auth/pairing/branding/`res/drawable` change + pre-ship. Don't claim "first-run ✓" off the logged-in fixtures.
|
||||
> **First-run / cold-path = `blocked→fixture` (run the fresh-install lane):** the recurring emulators are **paired + signed-in + onboarding-complete**, so the recurring passes **cannot reach onboarding / sign-up / login / auth-logo / pairing / new-device-recovery / day-1 empty states** — this is the fixture blind-spot that hid **O-ONBOARD-001** (P0, every fresh install crashed). Cover it on a **throwaway** device (e.g. `emulator-5558` / fresh AVD — never `pm clear` 5554/5556) on any onboarding/auth/pairing/branding/`res/drawable` change + pre-ship. Don't claim "first-run ✓" off the logged-in fixtures. **R20 (2026-06-29) — FULL FRESH-PAIRING LANE RUN ✅ (user-requested), 0 FATAL:** two throwaways uninstalled→fresh-installed (5558=Avery + booted CloserCodexQA/5560=Riley). A: notif-permission → **onboarding all 3 slides** ("Answer honestly / No peeking / Grow Closer" — CtaSlide logo renders, O-ONBOARD-001 stays fixed) → auth landing (AuthLogoMark renders) → **sign-up** → **3-step profile** (name · inclusive gender Female/Male/Non-binary/Prefer-not-to-say · optional photo skip) → unpaired handoff → **invite code 47V-JCZ** + recovery phrase shown. B: **Skip onboarding path** verified (slide 1 → auth) → sign-up → profile → **Accept-instead → enter 47VJCZ → "Pair up"** → "You're connected / Riley & Avery". **New couple `3sRSEvky7HdSUOY9F1z0` (2 userIds, `encryptionVersion=2`) created server-side**; both devices flipped to paired in real-time; B's day-1 check-in modal + first daily Q render. No new bugs. Teardown: throwaways uninstalled, 5560 powered off, QA/Sam (5554/5556) untouched + clean (active=0). (Residual isolated throwaway Firestore couple left as disposable test data.)
|
||||
>
|
||||
> **📖 Architecture reference:** see [`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) — contains architecture, security model, data model, and the [Known landmines](docs/Engineering_Reference_Manual.md#known-landmines-and-recent-fixes) section that backs every fix-and-pruned ID below.
|
||||
> Hygiene: this is a *current-status* matrix, not a per-round changelog — `fail→id` flips to `pass` once a fix is
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ process rule, make sure it doesn't contradict the Guardrails.
|
|||
entitlement math) — **a red suite is a P0/P1 regression gate, stop and fix before QA'ing a build**; (b) the **scanners** —
|
||||
`qa/entrypoint_smoke.sh` (both serials), `scripts/theme-scan.sh` (Pass C), `scripts/wiring-scan.sh` (Pass N),
|
||||
`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) optional
|
||||
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;
|
||||
the on-device net for the "composes fine, crashes on first paint" class — a red run is a P0 gate); (d) optional
|
||||
**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.
|
||||
4. **Run the passes report-only**, sub-batched to one context window each — recurring set **A–N + P** (K money-path +
|
||||
|
|
@ -219,9 +221,15 @@ confirms + enumerates this; the fix phase applies couple-shared everywhere.
|
|||
logic-level — unit/functions tests, `theme-scan`, `wiring-scan`, `painter-xml-scan` — **none of them actually render a
|
||||
composable.** `painterResource` on a `<bitmap>` compiled fine, passed all 205 unit tests, and only threw on first paint.
|
||||
There is a whole class of "composes fine, crashes on render" bugs (resource resolution, `LocalContext` casts, bad
|
||||
`painterResource`) with **zero automated coverage**. Until a screenshot/instrumented render test exists (see `Future.md`
|
||||
— Roborazzi/Paparazzi JVM screenshot test or a Compose/Espresso smoke), the **fresh-install lane above is the only net**
|
||||
for render crashes on first-run screens — treat it as mandatory, not optional.
|
||||
`painterResource`). **R20 added the first on-device net for it:** `app/src/androidTest/.../ui/FirstRunRenderSmokeTest.kt`
|
||||
renders the first-run crash composables (`CtaSlide` + `AuthLogoMark`, light+dark — the exact O-ONBOARD-001 sites) via a
|
||||
Compose `createComposeRule()` and asserts they paint; proven to FAIL on the reintroduced bug. Run it with
|
||||
`./gradlew :app:connectedDebugAndroidTest` (needs a connected emulator; filter a class with
|
||||
`-Pandroid.testInstrumentationRunnerArguments.class=…`). It currently covers **only the first-run leaf composables** —
|
||||
most routes still have no render test, so the **fresh-install lane above remains the net for the rest** until the smoke
|
||||
grows (see `Future.md` — extend toward sign-in→pair→daily-Q→game with a Hilt test runner, and/or a Roborazzi/Paparazzi
|
||||
screenshot suite). Treat both the fresh-install lane and (when an emulator is attached) `connectedDebugAndroidTest` as
|
||||
part of the render-crash net.
|
||||
- **Automate the regression smoke:** capture the smoke checklist as a runnable script (adb/Maestro) so every round
|
||||
re-checks it cheaply instead of by hand. **Built:** `qa/entrypoint_smoke.sh <serial> <recipient_uid>` (+ helper
|
||||
`qa/qa_push.js`) — the cold-start / entry-point launch-integrity smoke. It launches via the launcher AND sends a
|
||||
|
|
@ -237,9 +245,10 @@ confirms + enumerates this; the fix phase applies couple-shared everywhere.
|
|||
`CloserBrandCopyTest`, …) and `cd functions && npm test` (`entitlementLogic.test.ts`). **A failing test is a regression
|
||||
bug (P0/P1) — file it and do not QA a build with a red suite.** A fix that breaks a test isn't "Fixed" (see Fix phase).
|
||||
These guard the exact invariants this QA chases (ciphertext format, rate limiting, quiet-hours suppression,
|
||||
entitlement math), so a green run is a precondition, not a bonus. (**Coverage gap:** there are **0 instrumented
|
||||
tests** — no on-device Compose/Espresso smoke; logged in `Future.md`. Until that exists, the live passes + scanners
|
||||
are the only UI-behavior net.)
|
||||
entitlement math), so a green run is a precondition, not a bonus. (**Instrumented coverage (R20):** the first on-device
|
||||
test now exists — `FirstRunRenderSmokeTest` (a Compose render smoke of the first-run screens); run it when an emulator
|
||||
is attached with `./gradlew :app:connectedDebugAndroidTest`. It's still **first-run-only** — broader UI/nav/DB-DataStore
|
||||
behavior remains uncovered, so the live passes + scanners are still the main UI-behavior net; grow the suite per `Future.md`.)
|
||||
- **Stress / monkey fuzz (cheap random-crash net the manual nav-fuzz misses):** once per build run
|
||||
`adb shell monkey -p app.closer --throttle 300 --pct-touch 90 -v 5000` on each emulator with `logcat` capturing —
|
||||
any `FATAL EXCEPTION`/ANR it triggers is a bug (file it with the monkey seed). This complements Pass C's *targeted*
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
18
Future.md
18
Future.md
|
|
@ -22,12 +22,18 @@ Improvement & feature ideas surfaced while QA-testing as a consumer (each works
|
|||
light and dark, pixel-diffs them, and fails on unexpected white backgrounds or invisible text. When done,
|
||||
run it in CI against every UI PR.
|
||||
|
||||
- **Instrumented / on-device test coverage (currently 0 androidTest).** `app/src/test` has 19 solid unit tests
|
||||
(encryption, rate limiter, quiet hours, streak, entitlement, …) but `app/src/androidTest` is empty — there is no
|
||||
automated on-device net for UI behavior, navigation, or DB/DataStore integration; the live QA passes + scanners are the
|
||||
only thing catching that class. Add a small **Compose UI / Espresso smoke** (sign-in → pair → answer daily Q → open a
|
||||
game → send a message) wired into the per-round gate (alongside `qa/entrypoint_smoke.sh`), then grow it. *Prompted by:*
|
||||
the R-? QA-plan gap review — the playbook now runs `./gradlew testDebugUnitTest` but has no instrumented suite to run.
|
||||
- **🟡 STARTED (R20) — Instrumented / on-device test coverage (was 0 androidTest).** First cut shipped:
|
||||
`app/src/androidTest/.../ui/FirstRunRenderSmokeTest.kt` — an on-device **Compose render smoke** of the first-run
|
||||
screens (`CtaSlide` + `AuthLogoMark`, light + dark), the exact `painterResource` logo sites that crashed every fresh
|
||||
install in **O-ONBOARD-001**. It's the net for the "composes fine, crashes on first paint" class the JVM unit tests +
|
||||
static scanners structurally can't catch. **Proven (R20):** passes green on-device, and FAILS with the original
|
||||
`IllegalArgumentException: Only VectorDrawables…` when the bug is reintroduced. Infra added: `testInstrumentationRunner`
|
||||
+ `ui-test-junit4` (build.gradle.kts); also un-blocked the androidTest source set (the stale `CanonicalVectorCaptureInstrumentTest`
|
||||
couldn't compile against `private deriveKey` → made it `@VisibleForTesting internal`). Run: `./gradlew :app:connectedDebugAndroidTest`.
|
||||
**Still to grow:** it's Hilt-/Firebase-free leaf-composable rendering only — extend toward a fuller
|
||||
sign-in → pair → answer daily Q → open a game → send a message flow (needs a Hilt test runner + fakes), and wire
|
||||
`connectedDebugAndroidTest` into the per-round gate / CI alongside `qa/entrypoint_smoke.sh`. *Prompted by:* the QA-plan
|
||||
render-coverage gap (O-ONBOARD-001 escaped because nothing rendered a composable).
|
||||
|
||||
- **✅ DONE — Consistent brand glyphs across game cards + waiting surfaces.** G-set + G2 (17 glyphs) in
|
||||
`res/drawable-nodpi/glyph_*.xml`; **13 wired + verified live:** every Play-hub card (This or That, How Well, Desire
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ android {
|
|||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// RevenueCat API key. Set RC_API_KEY in local.properties (never committed).
|
||||
// Debug builds fall back to a placeholder; release builds abort — see task guard below.
|
||||
buildConfigField(
|
||||
|
|
@ -220,4 +222,10 @@ dependencies {
|
|||
// Canonical-vector capture harness (paired-CI for iOS↔Android E2EE fixture fill)
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test:runner:1.5.2")
|
||||
|
||||
// Instrumented Compose render smoke (first-run screens) — the on-device net for the
|
||||
// "composes fine, crashes on first paint" class (e.g. O-ONBOARD-001). Needs the BOM so the
|
||||
// ui-test version matches the app's Compose; ui-test-manifest (debug, above) hosts the ComposeRule.
|
||||
androidTestImplementation(composeBom)
|
||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
package app.closer.ui
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import app.closer.ui.auth.AuthLogoMark
|
||||
import app.closer.ui.onboarding.CtaSlide
|
||||
import app.closer.ui.theme.CloserTheme
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Instrumented render smoke for the FIRST-RUN screens — the on-device net for the whole
|
||||
* "composes fine, crashes on first paint" class that our JVM unit tests and static scanners
|
||||
* cannot catch (they never render a composable).
|
||||
*
|
||||
* Both composables exercised here carry the two `painterResource(...)` logo calls that crashed
|
||||
* EVERY fresh install in O-ONBOARD-001 (a `<bitmap>` XML routed through Compose's VectorDrawable
|
||||
* loader, which throws). That P0 compiled, passed all unit tests, and only threw on first paint —
|
||||
* exactly what this test guards. Rendering these on-device executes the `painterResource` calls; a
|
||||
* regression throws during composition and fails the test.
|
||||
*
|
||||
* Deliberately Hilt-/Firebase-/network-free: it composes the leaf first-run composables directly
|
||||
* (no Activity launch, no auth, no backend), so it stays fast and deterministic. Both light and
|
||||
* dark are exercised (the logo resolves per-theme drawables). Grow this toward a fuller
|
||||
* sign-in → pair → daily-Q → game flow once a Hilt test runner + fakes are in place.
|
||||
*
|
||||
* Run on a connected emulator/device:
|
||||
* ./gradlew :app:connectedDebugAndroidTest
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class FirstRunRenderSmokeTest {
|
||||
|
||||
@get:Rule
|
||||
val composeRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun ctaSlide_rendersLogoAndCtas_light() {
|
||||
composeRule.setContent {
|
||||
CloserTheme(darkTheme = false) { CtaSlide(onNavigate = {}) }
|
||||
}
|
||||
composeRule.onNodeWithText("Create account").assertIsDisplayed()
|
||||
composeRule.onNodeWithText("I already have an account").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ctaSlide_rendersLogoAndCtas_dark() {
|
||||
composeRule.setContent {
|
||||
CloserTheme(darkTheme = true) { CtaSlide(onNavigate = {}) }
|
||||
}
|
||||
composeRule.onNodeWithText("Create account").assertIsDisplayed()
|
||||
composeRule.onNodeWithText("I already have an account").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun authLogoMark_renders_light() {
|
||||
composeRule.setContent {
|
||||
CloserTheme(darkTheme = false) { AuthLogoMark() }
|
||||
}
|
||||
// contentDescription "Closer" lives on the foreground logo Image — its presence proves the
|
||||
// painterResource(closer_launcher_foreground) call composed without throwing.
|
||||
composeRule.onNodeWithContentDescription("Closer").assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun authLogoMark_renders_dark() {
|
||||
composeRule.setContent {
|
||||
CloserTheme(darkTheme = true) { AuthLogoMark() }
|
||||
}
|
||||
composeRule.onNodeWithContentDescription("Closer").assertExists()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package app.closer.crypto
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import java.util.Base64
|
||||
import com.google.crypto.tink.KeysetHandle
|
||||
import com.google.crypto.tink.aead.AesGcmKeyManager
|
||||
|
|
@ -78,7 +79,12 @@ class RecoveryKeyManager @Inject constructor() {
|
|||
com.google.crypto.tink.JsonKeysetReader.withBytes(bytes)
|
||||
)
|
||||
|
||||
private fun deriveKey(phrase: String, salt: ByteArray): ByteArray {
|
||||
// internal (not private) ONLY so the iOS↔Android canonical-vector capture harness
|
||||
// (CanonicalVectorCaptureInstrumentTest) can derive the Argon2id fixture. @VisibleForTesting
|
||||
// keeps lint flagging any non-test production caller. (Without this the androidTest source set
|
||||
// fails to compile, which blocked every instrumented test, incl. the first-run render smoke.)
|
||||
@VisibleForTesting
|
||||
internal fun deriveKey(phrase: String, salt: ByteArray): ByteArray {
|
||||
val params = Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
|
||||
.withSalt(salt)
|
||||
.withParallelism(ARGON2_PARALLELISM)
|
||||
|
|
|
|||
|
|
@ -217,8 +217,11 @@ private fun ValueSlide(
|
|||
}
|
||||
}
|
||||
|
||||
// `internal` (not private) so the instrumented render smoke (FirstRunRenderSmokeTest) can compose
|
||||
// this slide directly — it carries the two `painterResource` logo calls that crashed every fresh
|
||||
// install in O-ONBOARD-001, so rendering it in a test is the cheap guard against that class.
|
||||
@Composable
|
||||
private fun CtaSlide(onNavigate: (String) -> Unit) {
|
||||
internal fun CtaSlide(onNavigate: (String) -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
Loading…
Reference in New Issue