Phase 13-13 — Exit-scenario acceptance matrix
Phase 13-13 — Exit-scenario acceptance matrix
Execution reference for CEO. Every item is a concrete pass/fail check against the fifteen-step exit scenario defined in wiki/src/content/docs/tech/phase-13-plan.md §5. Phase 13 ships when every row in this matrix is green on a real Android device.
Do NOT amend §5 of phase-13-plan based on this document. Flag discrepancies via FLAG_LEAD below.
1. Context
Phase 13 ends when the exit scenario (phase-13-plan §5, 15 steps) passes end-to-end on a real Android device against the local-tunnel NestJS backend. This document is the Phase 13 ship gate.
Three cross-cutting concerns addressed here:
- D-021 pure-micro + pure-evening pattern verification. The §5 walkthrough as written is implicitly single-session (evening). D-021 §2 mandates that neither pure-micro nor pure-evening play is degraded. Section 3 maps each step across both patterns.
- D-027 diegetic onboarding deferral. The current 13-3 class-pick flow is Phase 13 canonical onboarding. Full Bertranda walkthrough + 50-step register + class-pick-as-Quest-001-beat-1-close is explicitly deferred per §4 below.
- JVM/Robolectric pre-coverage. New tests authored in Output 2 reduce the manual burden. Section 6 maps new tests to steps they partially substitute.
Backend test baseline entering 13-13: 283 unit + 54 e2e (337 total) post-13-12. Android test baseline entering 13-13: 342 tests post-13-12 (→ 380 post-13-13-prep).
2. Per-step acceptance criteria
Reference source: phase-13-plan.md lines 179-193.
Step 1 — Install APK from CI artifact
Pre-state: CI build artifact exists (release or debug APK). Device has Developer Options and Unknown Sources enabled. No prior walkrpg install.
Action: CEO installs APK via adb install or sideload.
Server-side observable: None at install time. Verify backend reachable: curl -H "Authorization: Bearer <mock-token>" http://<tunnel>/walker/profile returns HTTP 404 or 200 (depending on whether a walker was previously created in this DB).
Client-side observable:
- App icon appears on launcher.
- App launches without crash to splash/auth screen.
- EN label rendered: “WalkRPG” or equivalent app name (from
R.string.app_name). - PL label not applicable on splash (system chrome only — D-011 system chrome uses
name.en).
Pass criteria: APK installs cleanly. App opens to splash/auth screen within 3 seconds. No ANR or crash on first launch.
Fail criteria: Install fails. App crashes at launch. White screen persists > 5 seconds.
Known prior coverage: 13-1 Android repo bootstrap verifies app compiles and runs on emulator. MainViewModelTest (5 tests) verifies auth navigation flow. Splash/auth screen itself has no dedicated JVM test — device-only.
Step 2 — Sign in via mock auth
Pre-state: App on splash/auth screen. Backend running with mock auth (ADR-0006 posture). POST /auth/callback accepts any well-formed bearer token.
Action: CEO taps mock sign-in button (or equivalent — the mock auth screen per 13-1 design).
Server-side observable: POST /auth/callback receives request. Check Pino logs or curl -X POST http://<tunnel>/auth/callback -H "Content-Type: application/json" -d '{"firebaseToken":"test-token-123"}' returns 200 with { "accessToken": "<jwt>", "walkerId": null } (null walkerId = no walker yet). Also verify X-Request-Id header present in response (middleware from 13-4 Thread D).
Client-side observable:
- Auth state transitions to AUTHENTICATED.
- Navigation moves to CLASS_PICK or HOME depending on walkerId presence.
- EN: class pick screen title or home screen visible.
Pass criteria: Auth succeeds (200 from backend). Navigation to class-pick (walkerId null) or home (walkerId present). No 401/403 error shown.
Fail criteria: Auth returns non-200. App shows error state. Navigation does not proceed.
Known prior coverage: MainViewModelTest verifies LOADING→CLASS_PICK and LOADING→HOME transitions. Auth endpoint unit-tested in backend/src/auth/auth.service.spec.ts. Device-only for real HTTP roundtrip.
Step 3 — Pick a class (the 1 authored class: Cartographer)
Pre-state: Class-pick screen rendered. BUNDLED_CLASSES in Kotlin lists class.cartographer only.
Action: CEO selects Cartographer and taps confirm/begin.
Server-side observable: POST /walker/class with body {"classId":"class.cartographer"} (or equivalent per 13-3 reconciliation). Verify via Prisma Studio: walker table row for this Firebase UID should have classId = 'class.cartographer' and treePointsBanked = 0, treePointsSpent = 0.
Client-side observable:
- EN: class card shows “Cartographer” (or
name.enfrom data layer). - PL: class description prose uses
name.pl— “Kartograf” or equivalent perdata/src/content/classes/cartographer.ts. - After confirm: navigation to HOME screen.
Pass criteria: POST /walker/class returns 200. Walker row in DB has classId = 'class.cartographer'. App navigates to home.
Fail criteria: 409 conflict (walker already has a different class — reset DB or use fresh user). 404 class not found. Navigation stuck.
Known prior coverage: WalkerCreationViewModelTest (8 tests) verifies class-pick flow including the BUNDLED_CLASSES bilingual render and POST /walker/class call. ClassPickScreenTest (8 tests) covers Compose UI. Backend: walker.service.spec.ts covers POST /walker/class happy path and idempotent re-pick.
Step 4 — Complete onboarding tutorial
Pre-state: App navigated to home screen after class pick.
Action: CEO dismisses any tutorial overlay (per 13-3 onboarding wireframe). In Phase 13 this is the 13-3 class-pick → tutorial-overlay → home flow, NOT the full D-027 Bertranda walkthrough (explicitly deferred — see §4).
Server-side observable: No dedicated onboarding-complete endpoint exists in Phase 13. Verify home screen loads GET /walker/profile (200) and GET /walker/streak (200). Prisma Studio: walker row exists with classId set.
Client-side observable:
- EN: home screen headline visible (e.g. “Level 0” or “Poziom 0”).
- PL:
Poziom 0in the progression card (D-016 home surface: singlePoziom Nnumber). - Streak ribbon shows Day 0 or Day 1 in EN/PL per locale.
- No tutorial overlay blocking interaction.
Pass criteria: Home screen loads without error. Walker profile present on screen. Tutorial overlay dismissed.
Fail criteria: Profile fetch fails (503, 401). Tutorial overlay cannot be dismissed. Home renders blank.
Known prior coverage: HomeViewModelTest (14 tests) covers profile + streak loading + error states. Phase1311A11yTest verifies home screen a11y. Onboarding overlay itself is device-only visual verification.
Step 5 — Trigger synthetic step ingest (or real walk)
Pre-state: Health Connect permission granted (or pending grant). Walker at energy = 0 (fresh).
Action: CEO either (a) takes a real walk with Health Connect integration active, or (b) manually calls POST /step/ingest via curl as a synthetic inject. Option (b) for controlled testing: curl -X POST http://<tunnel>/step/ingest -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"day":"2026-05-22","stepCount":5000,"startSampleUtc":"2026-05-22T07:00:00Z","endSampleUtc":"2026-05-22T09:00:00Z","sourceBundleId":"manual.test"}'
Server-side observable: Prisma Studio: step_logs table row for day='2026-05-22' with accepted_count=5000. Walker energy field updated (formula: D-018 envelope, ~1pt per step at nominal rate). No step_logs row older than today’s date when using the synthetic call.
Client-side observable:
- After sync (or manual trigger): PostWalkSync screen appears (or home refreshes).
- EN: step delta shown as “5,000 steps” (EN locale, comma-grouped per design-system).
- PL:
"5 000 kroków"(space-grouped per locale — design-system §Plurals). - Energy bar on home shows non-zero value.
Pass criteria: POST /step/ingest returns 200 with accepted=true. step_logs row exists. Energy visible on home screen.
Fail criteria: Anti-cheat rejects the synthetic count (check burst-rate math for chosen start/end window). 401 token error. Energy not updated on home after sync.
Known prior coverage: PostWalkSyncViewModelTest (7 tests) covers the ViewModel layer including bucket sync, error state, and tier upgrade. HealthConnectReaderTest (6 tests) covers bucket aggregation logic. Backend: anti-cheat.spec.ts + streak.spec.ts cover server-side validation. Health Connect READ requires real device permission grant — device-only for provenance verification.
Step 6 — See energy increase and streak update
Pre-state: Step ingest from Step 5 succeeded.
Action: CEO returns to home screen (pull-to-refresh or back navigation from PostWalkSync).
Server-side observable: GET /walker/profile returns energy > 0. GET /walker/streak returns currentStreakDays >= 1 and lastAttestedDate = today.
Client-side observable:
- EN: energy value shown (e.g. “Energy: 5000” or equivalent label per home wireframe).
- PL: streak ribbon shows
"Dzień 1"(or N for consecutive day) — D-011 system surface uses EN label but streak-day count is locale-neutral. - Streak ribbon live-region fires (D-011 + 13-11 a11y requirement:
LiveRegionMode.Politeon day count change — TalkBack users hear update without focus navigation).
Pass criteria: Energy value > 0 visible on home. Streak ribbon shows day count >= 1. No stale cache served (i.e. profile refreshed after ingest).
Fail criteria: Energy still shows 0 after ingest. Streak still shows Day 0. Home does not refresh after sync.
Known prior coverage: HomeViewModelTest tests 1-8 cover profile + streak rendering. StreakRepositoryTest (5 tests). Backend: streak.spec.ts (17 tests including tier transitions). Phase1311FontScaleTest verifies home at font scale 2.0.
Step 7 — Open Quest 001, complete first step
Pre-state: Walker has Energy > 0. Quest 001 (quest.001-first-road) available via GET /quest/available (no prereqs — available from start per QuestGateEvaluator).
Action: CEO opens Quest Log → selects Quest 001 → taps Start Quest → taps Advance Step 1.
Server-side observable:
POST /quest/001-first-road/startreturnsstatus=in_progress.POST /quest/quest.001-first-road/step/1/advancereturnscurrentStepIndex=2.- Prisma Studio:
quest_progresstable row for this walker + quest hascurrent_step_idupdated.
Client-side observable:
- EN: Quest detail screen shows “The First Road” (D-011: system chrome =
name.en). - PL: quest description prose rendered in PL per data layer
description.pl. - After advance: step 1 shows completed marker (checkmark or strikethrough per wireframe), step 2 highlighted as current.
- Tree points NOT yet awarded (D-016 separation: step advance does NOT grant points; only quest complete does).
Pass criteria: Both /start and /step/1/advance return 200. Step 1 UI shows completed. Current step indicator moves to step 2. Points unchanged.
Fail criteria: Start returns 409 (quest already started — idempotent; app should handle). Advance returns 422 STEP_OUT_OF_ORDER (check stepIndex). Quest detail blank or stuck on Loading.
Known prior coverage: QuestDetailViewModelTest (8 tests) covers start, advance, complete, offline-enqueue paths. QuestLogViewModelTest (16 tests) covers filter + display. QuestLogScreenTest (16 tests) covers Compose UI. Backend: quest.service.spec.ts tests 738+ cover advanceStep including D-016 no-auto-complete on final step.
Step 8 — Receive tree points, see them banked on home
Pre-state: Quest 001 all 5 steps at in_progress (steps 1-5 advanced). Walker on Quest Detail with step 5 completed (step 5 advance = isFinalStep=true per backend, but no auto-complete).
Action: CEO taps Complete Quest.
Server-side observable:
POST /quest/completereturnspointsAwarded=1(D-016: flat 1pt per quest).- Prisma Studio: walker row
tree_points_bankedincremented by 1.quest_progressrow hascompleted_atset.
Client-side observable:
- EN: PointGranted callout on QuestDetail: “+1 Tree Point banked.” (D-016 grant callout pattern from home-screen.mdx §Tree point grant callout).
- PL:
"Linijka oddana do składu. Spis przyjął."(exact copy from home-screen.mdx D-016 PL callout). - Home screen:
treePointsBankedincremented; banked banner visible if banked > 0 (HomeViewModel test 5).
Pass criteria: POST /quest/complete returns 200 with pointsAwarded=1. Walker treePointsBanked = 1 in DB. Quest status = completed. Home shows banked banner.
Fail criteria: pointsAwarded = 0 (D-016 regression). Quest complete returns 409 (idempotent — app should handle). Home still shows 0 banked after navigation back.
Known prior coverage: QuestDetailViewModelTest test completeQuest emits PointGranted event with 1 point (exact assertion). Backend: quest.service.spec.ts awards correct points for quest.002 (D-016 flat 1pt) and broader complete tests.
Step 9 — Open tree viewer, allocate a node on cluster.first-steps
Pre-state: treePointsBanked >= 1. Tree viewer accessible via bottom nav Tree tab.
Action: CEO opens Tree Viewer → taps node.even-stride (entry node per cluster.first-steps connectivity) → confirms allocation dialog.
Server-side observable:
POST /tree/allocatewith{"allocations":[{"type":"node","id":"node.even-stride","clusterId":"cluster.first-steps"}]}returns 200.- Prisma Studio:
tree_allocationstable has row for walker +node_id='node.even-stride'. - Walker
tree_points_spent = 1,tree_points_banked = 0.
Client-side observable:
- EN: node shows “Allocated” visual state (filled indicator per TreeViewer canvas states).
- PL: available-points counter updates from
"1 punkt"to"0 punktów"(Polish plurals —step_count/quest_countplural resources from 13-11 apply here). - Banked banner on home disappears when treePointsBanked drops to 0.
- Node shows confirmed (not provisional) state once server responds.
Pass criteria: Allocation returns 200. DB row exists for node. UI shows allocated state. Points count updated.
Fail criteria: INSUFFICIENT_POINTS 422 (banked check failed). NODE_ALREADY_ALLOCATED 409 (idempotent — app handles). Server 500 on Prisma transaction failure.
Known prior coverage: TreeViewerViewModelTest (7 tests) including confirmAllocation triggers POST allocate and updates state. TreeViewerViewModel1312Test (3 tests) covers idempotency key semantics. Backend: tree.service.spec.ts covers allocation.
Step 10 — Kill app, reopen — node stays allocated
Pre-state: node.even-stride allocated in Step 9. Allocation was confirmed (not provisional).
Action: CEO force-kills app via Android Recent Apps → force stop. Relaunches from icon.
Server-side observable: No server call triggered by kill itself. On reopen: GET /tree/state called (cold-start sequence). Verify allocation still in DB (Prisma Studio row from Step 9 unchanged).
Client-side observable:
- EN: Tree Viewer loads with
node.even-strideshown as “Allocated” (not “Available” or blank). - PL: available-points counter shows 0 (treePointsBanked persisted on backend).
- Provisional nodes list is empty (offline queue re-check on load:
OfflineQueueRepository.getAll()returns empty since allocation was confirmed online).
Pass criteria: After cold start, tree state shows node.even-stride allocated. No provisional indicator. Available points = 0.
Fail criteria: Node shows as unallocated after restart (state not persisted). Provisional indicator persists (offline queue was not drained). App crashes on cold start.
Known prior coverage: TreeViewerViewModelTest test existing offline queue entries restored as provisional on load covers the provisional-restoration path. Cold-start from process-death is device-only (Robolectric cannot simulate Android process death reliably for persistent state). New JVM test Phase1313ColdStartTest (§6) partially substitutes.
Step 11 — Open faction screen, see Unfinished Guild reputation reflect quest reward
Pre-state: Quest 001 completed in Step 8. Quest 001 beat rewards include faction.unfinished-guild rep delta (per data/src/content/quests/001-first-road.ts step beats with factionRep entries).
Action: CEO opens Factions tab → locates Unfinished Guild faction card.
Server-side observable:
GET /faction-repreturnsfaction.unfinished-guildrep > 0.- Prisma Studio:
faction_reptable row for walker +faction_id='faction.unfinished-guild'has updatedreputationcolumn.
Client-side observable:
- EN: Faction card shows “Unfinished Guild” (
name.enper D-011 system chrome). - PL: Tier name in PL prose (e.g. “Nowicjusz” — Tier 0 label from faction data — inside faction detail, not system chrome).
- Rep bar shows non-zero fill corresponding to current rep value.
- Tier indicator shows correct tier based on rep thresholds.
Pass criteria: GET /faction-rep shows > 0 rep for Unfinished Guild. UI renders rep bar filled. Tier consistent with rep value.
Fail criteria: Rep = 0 (quest-complete side-effect not wired to faction-rep). Faction card blank or error state. Rep bar at 0 despite quest completion.
Known prior coverage: FactionRepositoryTest (11 tests), FactionsViewModelTest (10 tests), FactionDetailViewModelTest (10 tests). Backend: faction-rep.service.spec.ts. Quest-complete faction-rep wiring tested in quest.service.spec.ts calls factionRepService.applyDelta for each rewards.reputation entry (13-10 expansion).
Step 12 — Open Quest 002, walk into combat trigger
Pre-state: Quest 001 completed (prerequisite for Quest 002 per quest.002 prerequisites: ["quest.001-first-road"]). Walker energy sufficient (D-018: ~30% of daily budget per fight). Quest 002 step 3 is the encounter beat (beat-3-frost-thing-at-the-slow-vent, kind: "encounter", opponent: opponent.frost-thing-slow-vent).
Action: CEO opens Quest Log → starts Quest 002 (quest.002-silent-seal-station or equivalent) → advances through steps 1-2 → step 3 encounter beat triggers navigation to CombatScreen.
Server-side observable:
POST /quest/startfor Quest 002 returns 200.POST /quest/:id/step/1/advanceand/step/2/advancereturn 200.- When step 3 beat is an encounter:
POST /combat/encountercalled withquestId+stepIndex=3+opponentTemplateId=opponent.frost-thing-slow-vent. - Prisma Studio:
combat_encountersrow created withstatus=ACTIVE,seedfield set.
Client-side observable:
- EN: CombatScreen renders “Frost-thing” as enemy name (temporary slug-derived label per 13-8 deferred localized-name endpoint).
- EN: Walker HP bar visible. Enemy HP bar visible.
- EN: “Attack” button and “Retreat” button visible (no “Defend” or “Use-Energy-Burst” per 13-8 action-set scope).
- PL: Walker status group content description in PL if locale=PL (a11y D-011).
- Turn indicator: “Your turn, turn 1” (EN) or equivalent.
Pass criteria: Navigation to CombatScreen succeeds. POST /combat/encounter returns 200 with encounterId + seed + stat snapshots. Combat UI renders initial state.
Fail criteria: Quest 002 prerequisites not met (Quest 001 status not “completed” in DB). Step 3 advance navigates nowhere (encounter beat handler not wired). CombatScreen renders blank or error.
Known prior coverage: CombatScreenTest (15 tests) covers CombatScreen UI states. CombatViewModelTest (15 tests) covers turn loop including SavedStateHandle. CombatRepositoryTest (19 tests) covers all 4 endpoints. Phase1311CombatScreenA11yTest (6 tests) covers a11y assertions. New JVM test Phase1313QuestCombatNavigationTest (§6) covers the QuestDetailScreen→CombatScreen navigation trigger path.
Step 13 — Resolve combat encounter (win, retreat, or defeat — at least 1 outcome path)
Pre-state: CombatScreen active with encounterId from Step 12. Walker HP > 0. Energy > 0.
Action: CEO taps Attack until encounter resolves (win), OR taps Retreat. One of win/retreat/defeat must complete end-to-end.
Server-side observable:
- For win:
POST /combat/encounter/:id/resolvereturnsoutcome=WALKER_VICTORYwithfactionRepDeltas,questStateAfter,encounterSummary. - For retreat:
POST /combat/encounter/:id/retreatreturnsoutcome=WALKER_RETREAT. - Prisma Studio:
combat_encountersrow status =RESOLVEDorRETREATED.walker.current_hpupdated.
Client-side observable:
- EN: Outcome card renders after resolve. Win: “Victory” or equivalent. Retreat: “Retreat” or equivalent.
- PL: outcome text in PL if locale=PL (strings.xml PL values for combat outcome labels).
- Walker HP bar updates during turns (HpDeltaBar animation — 500ms animateFloatAsState per 13-9).
- TierPromotionBadge visible if faction tier changes on win (AnimatedVisibility/fadeIn per 13-9).
- After outcome: “Continue” CTA navigates back to Quest Detail.
Pass criteria: One of win/retreat/defeat resolves cleanly. Outcome card shown. Quest-state updated (Step 14 verifies persistence). No crash on outcome card dismissal.
Fail criteria: Server error on /resolve or /retreat. Turn loop hangs (server roundtrip never returns). Outcome card not shown. Crash on continue navigation.
Known prior coverage: CombatWorkedExampleTest (7 tests) reproduces formulas.md §10 seed-92 fixture. CombatViewModelTest covers auto-resolve + retreat dialog. Backend: combat.service.spec.ts (42 tests) covers encounter lifecycle. CombatRepositoryTest covers all endpoint shapes.
Step 14 — Encounter outcome reflected in quest state on server side
Pre-state: Step 13 resolved (win or retreat). Quest 002 step 3 was the encounter beat.
Action: CEO navigates back to Quest Detail for Quest 002 (outcome card CTA or back button).
Server-side observable:
GET /quest/availablereturns Quest 002 withcurrentStepIndexadvanced past step 3 (win = step 4; retreat = step 3 still current per D-010 non-punitive retreat semantics — FLAG_LEAD: §5 step 14 does not specify retreat vs win outcome; verify with tech-architect whether retreat advances quest or repeats step).- Prisma Studio:
quest_progressrow hascurrent_step_idreflecting advancement. - Faction rep deltas applied: Prisma
faction_reprow updated perfactionRepDeltasfrom/resolveresponse.
Client-side observable:
- EN: Quest Detail shows correct current step (step 4 if win, step 3 if retreat).
- PL: Step names in PL per quest data layer
name.plon step objects. factionRepDeltasfrom/resolve: Faction Overview rep bar updated if faction rep changed.
Pass criteria: Quest Detail shows correct step post-encounter. DB row reflects advancement. Faction rep updated in DB if quest beats carry rep reward.
Fail criteria: Quest Detail still shows step 3 after win (state not refreshed from server). Faction rep unchanged despite /resolve response having deltas. Regression in quest-step-advance side effects.
Known prior coverage: Backend: quest.service.spec.ts + faction-rep.service.spec.ts cover quest + rep wiring post-encounter. New Android test Phase1313CombatResolutionPersistenceTest (§6) covers the CombatViewModel→QuestDetailViewModel state refresh path.
FLAG_LEAD (step 14 ambiguity): phase-13-plan §5 step 14 says “Encounter outcome reflected in quest state on server side” without specifying whether retreat advances the quest step. D-010 §3 non-punitive defeat clamps HP to 1 — but does not specify step-index behavior on retreat. The backend retreatEncounter implementation in 13-7 preserves current step (not advancing). This needs explicit verification against the phase-13-plan §5 intent. Escalate to tech-architect for clarification before final sign-off.
Step 15 — Kill app, reopen — all state persists
Pre-state: Steps 1-14 complete. Walker has: treePointsSpent=1, node.even-stride allocated, Quest 001 completed, Quest 002 at step 4 (or 3), faction rep > 0, HP = post-combat value, streak day >= 1.
Action: CEO force-kills app (Recent Apps → force stop). Relaunches from icon.
Server-side observable: No changes to backend state. All state persisted during steps 1-14 already in DB.
Client-side observable: After cold start and re-auth (token refresh from EncryptedSharedPreferences):
- EN: Home shows same
Poziom N(level = treePointsSpent = 1). - EN: Streak ribbon shows same day count (streak state from
GET /walker/streak). - EN: Quest pill shows Quest 002 active with current step.
- EN: WalkerHpHud shows correct HP (currentHp from
GET /walker/profile, which readswalker.current_hp). - EN: Tree Viewer shows
node.even-strideallocated (fromGET /tree/state). - EN: Faction Overview shows Unfinished Guild rep > 0.
- PL: All above render correctly in PL locale if override is set.
Pass criteria: All six state dimensions (level, streak, quest, HP, tree, faction) load from backend correctly on cold start. No stale offline-queue entries rendering as provisional.
Fail criteria: Any dimension shows wrong value (e.g. HP = full despite taking combat damage). Tree shows node as unallocated. Quest status reset. Offline queue entries survive beyond confirmed-sync boundary.
Known prior coverage: HomeViewModelTest tests 1-8 cover all six home-screen data fields. TreeViewerViewModelTest test existing offline queue entries restored as provisional on load covers queue-restoration path. New JVM test Phase1313FullColdStartTest (§6) is the primary substitute.
3. D-021 pure-micro + pure-evening usage matrix
D-021 §2 mandates that neither pure-micro nor pure-evening play is degraded or punished. The §5 walkthrough is implicitly single-session. This section maps each step across both patterns.
Definitions:
- Pure-micro: Walker opens app 3-5 times during the day, each session < 2 minutes, between walking bouts. Total steps accumulated gradually across sessions.
- Pure-evening: Walker opens app once at end of day for a 5-10 minute session. All 15 steps walked in sequence using accumulated day’s steps.
| Step | Pure-micro outcome | Pure-evening outcome | Pattern-sensitive? |
|---|---|---|---|
| 1 Install | Identical — install is one-time | Identical | No |
| 2 Sign in | Identical | Identical | No |
| 3 Class pick | Identical | Identical | No |
| 4 Onboarding | Identical | Identical | No |
| 5 Step ingest | Micro: multiple /step/ingest calls during day (2k + 1.5k + 1.5k = 5k). Merge-on-duplicate-day: three calls all for day='2026-05-22' produce one step_logs row with accepted_count=5000. Cap math: 5×2k calls still cap at daily max, not 5× the cap. | Evening: single 5k call. Same final DB state. | Yes — merge semantics. Micro requires idempotent merge; evening is a single write. Backend POST /step/ingest must handle repeated day field correctly (ADR-0002). |
| 6 Energy + streak | Micro: energy accumulates across micro-syncs. Home refresh after each sync shows incremental energy. Streak fires on FIRST micro-sync of the day (day-boundary semantics: lastAttestedDate compared to calendar day, not to previous sync timestamp). | Evening: energy jumps from 0 to full-day total in one sync. Streak fires on the single sync. | Yes — streak day-boundary. Streak-day counted from calendar day (day field in /step/ingest), not app foreground time. Both patterns: attesting day='2026-05-22' once is sufficient to advance streak, regardless of micro vs evening. D-021 §4 confirmed: streaks are agnostic of micro/evening split. |
| 7 Quest 001 step 1 | Micro: Walker starts quest in one session (< 2min), returns to background. Later micro session advances step 1. Quest progress persists in Room offline queue if advance is done offline, or immediately via server if online. | Evening: all in one session. | Yes — offline queue. Pure-micro with poor connectivity may queue the advance; D-009 7-day cap ensures it syncs within tolerance. |
| 8 Tree points | Micro: quest complete is a single explicit tap — valid in micro session. PointGranted event fires and home banked counter updates. | Evening: same tap sequence. | No — outcome identical regardless of when in the day. |
| 9 Tree allocation | Micro: tap confirm in a 30-second micro. If offline: provisional state rendered immediately; confirmed on next online micro. | Evening: online confirmation immediate. | Mild. Offline-then-online micro pattern may show provisional briefly. Not a degraded experience — D-009 §2 provisional affordance. |
| 10 Cold start persistence | Micro: after force-kill between micro sessions. Re-launch in a later micro session should restore state from backend. | Evening: single kill at end of evening session. | Yes — cold start in micro context. Micro players may kill and re-open multiple times within a day. Each cold start must re-fetch from backend. Auth token in EncryptedSharedPreferences must survive across kills. |
| 11 Faction rep | Micro: faction rep is a server-side value; shows correctly whenever home loads. | Evening: same. | No |
| 12 Combat entry | Micro: Walker can enter combat in a micro session. CombatScreen is a single encounter with multiple turns. A 2-minute micro may not be enough to complete a full encounter (e.g. 6+ turns). Concern: encounter persists as ACTIVE in DB across micro sessions. Walker opens app, encounter in ACTIVE state → app re-navigates to CombatScreen. This must work: CombatViewModel must restore active encounter state on cold start. | Evening: full encounter in one session. | Yes — encounter state across session boundary. JVM test Phase1313ColdStartTest covers this path. |
| 13 Combat resolve | Micro: Walker completes encounter (win/retreat) in micro session. 5 minutes is enough for a turn-based fight. | Evening: same. | No — given encounter state restored (Step 12 above). |
| 14 Quest state post-combat | Micro: Quest Detail loaded in next micro session after combat. GET /quest/available re-fetched on load. | Evening: immediate in-session refresh. | No — server-side state is authoritative regardless. |
| 15 Full cold start | Micro: final cold start in the same pattern as Step 10. All state from all micro sessions accumulated in backend persists. | Evening: cold start at end of single session. | No — same server-side re-fetch logic. |
Summary: Steps 5 (merge semantics), 6 (streak day-boundary), 9 (provisional), 10 and 12 (cold start / encounter restoration) are pattern-sensitive. The new JVM tests in §6 target these cells.
4. D-027 onboarding triage — explicit deferral
Decision: Option (b) — accept current 13-3 class-pick UI as Phase-13-final. Full diegetic expansion defers to post-Phase-13.
Per phase-13-plan §10.10 option (b): The existing 13-3 class-pick → tutorial-overlay → home flow is Phase 13 canonical onboarding. Section 2 Step 4 of this matrix (“Complete onboarding tutorial”) stands as-is: CEO dismisses the 13-3 tutorial overlay.
What is NOT in Phase 13 (per this deferral):
- D-027 §2 Bertranda walkthrough (step 3-5: Bertranda hands Map + motto + 50-step register) — NOT IMPLEMENTED in Phase 13.
- The 50-step walking register bar as the first onboarding surface.
- Class-pick-as-Quest-001-beat-1-close (D-027 §3): current flow goes splash → class pick → home as a standalone UI screen, NOT as the closing surface of Quest 001 beat 1.
- Notification opt-in at onboarding (D-027 step 6, D-026): no FCM token registration in Phase 13 onboarding flow.
Why deferred: Phase 13’s exit-scenario stability depends on the 13-3 class-pick + home flow. Reconstructing onboarding as a Quest 001 beat wrapper requires mobile-developer (new nav graph changes), narrative-designer (Bertranda walkthrough copy), and ui-designer (50-step bar widget) — all three leads paired, adding a sub-phase of scope. D-027 onboarding expansion schedules as a Phase-13 follow-up (next-free 13-N+1 after Phase 13 ships) or early Phase 14, pending CEO triage.
What this means for §5 step 4: The step reads “Complete onboarding tutorial.” For Phase 13 this means: dismiss the 13-3 tutorial overlay (no Bertranda dialogue, no 50-step bar, no class-pick-as-beat-1). The acceptance criterion in §2 Step 4 reflects this minimal interpretation.
5. Device-only items list
Items physically requiring a real Android device that JVM/Robolectric cannot substitute.
| Step | Device-only requirement | Why JVM cannot substitute |
|---|---|---|
| 1 | APK install from CI artifact | adb install requires a connected device or emulator with ADB daemon. JVM unit tests run on the host JVM — no APK install lifecycle. |
| 2 | Mock auth network roundtrip to real tunnel | POST /auth/callback hits a real NestJS process over ngrok/cloudflared. JVM tests mock Retrofit — no real HTTP call. |
| 5 | Health Connect READ permission grant + real sensor data | HealthConnectClient.getOrCreate() requires android.permission.health.READ_STEPS granted by a real Android consent dialog. The SDK has no JVM stub. |
| 5 | Actual pedometer provenance (D-007 §1 attestation) | Real Android sensor data carries hardware-sourced timestamps and sourceBundleId. JVM tests inject synthetic DayStepBucket objects without provenance. |
| 6 | Live region TalkBack announcement during streak update | TalkBack accessibility service only fires on a real Android device (or instrumented Espresso test). JVM semantics tests (Phase1311A11yTest) verify the LiveRegionMode.Polite annotation is present, not that TalkBack fires it. |
| 9 | Provisional-to-confirmed visual transition animation | animateFloatAsState (500ms) and AnimatedVisibility require the Compose runtime on Android. JVM tests assert state values; they do not render animation frames. |
| 10 | Android process death via force-stop | ActivityManager.forceStopPackage and OS-level process termination cannot be triggered from a JVM test. Phase1313ColdStartTest simulates this by constructing a new ViewModel instance with fresh mocks, not by triggering OS process death. |
| 12 | Quest step beat → CombatScreen navigation trigger | Navigation component (NavController.navigate()) requires the Android Compose Navigation runtime. Phase1313QuestCombatNavigationTest verifies the ViewModel emits the correct navigation event; actual screen transition is device-only. |
| 13 | Turn-loop animation on real hardware | HP delta floating numerals (float + fade per design-system §5.5) and TierPromotionBadge fadeIn animations require GPU composition. JVM tests assert UI state, not rendered frames. |
| 15 | Full cold start: EncryptedSharedPreferences persistence across process death | EncryptedSharedPreferences reads/writes against the Android KeyStore, which is unavailable in the JVM environment. LocaleStore.createForTest() (13-11) bypasses this for locale tests; a similar pattern exists for SyncStateStore. The full cold-start scenario (all state re-fetched from backend) requires a real device to trigger genuine process death. |
6. JVM/Robolectric coverage delta
New tests authored in Output 2. Each entry maps to the §5 steps it partially substitutes.
| Test file | New tests | Steps partially covered | Key gap filled |
|---|---|---|---|
Phase1313ColdStartTest.kt | 6 | Steps 10, 15 | Simulates app process restart by constructing a fresh TreeViewerViewModel instance. Asserts that tree state loads from mock backend (not from memory). Asserts offline queue is re-checked on construction. Asserts HP loads from WalkerProfile.currentHp not stale cache. |
Phase1313FullColdStartTest.kt | 7 | Step 15 | Constructs all five state-bearing ViewModels fresh in sequence (Home, Tree, Quest, Faction, Combat). Asserts each loads correct data from mock backend. Asserts no cross-ViewModel stale state. |
Phase1313QuestCombatNavigationTest.kt | 5 | Step 12 | Verifies QuestDetailViewModel emits a NavigateToCombat event when the current quest beat is kind=encounter. Verifies the event carries correct questId, stepIndex, and opponentTemplateId. |
Phase1313CombatResolutionPersistenceTest.kt | 5 | Step 14 | Verifies domain model invariants for ResolveResult and RetreatResult: win advances currentStepIndex; D-016 treePointsDelta=0 on all outcomes; D-010 defeat clamps HP to 1 and does not advance step; D-010 retreat does not advance step; enrichedRepDeltas carries faction rep changes. |
Phase1313StreakMicroPatternTest.kt | 6 | Step 6 (micro) | Android-side: verifies that PostWalkSyncViewModel with 5 sequential mock ingest calls (all day='2026-05-22') reflects only the last server-returned streak state (not 5× increments). Verifies streak day = 1 not 5. |
Phase1313CapMathMicroPatternTest.kt | 4 | Step 5 (micro) | Verifies PostWalkSyncViewModel sums step deltas correctly across 5 calls of 2k each (client shows 2k per call, not cumulative). Verifies server-capped response still yields Success state. |
Phase1313OfflineSyncDayCapTest.kt | 5 | D-009 7-day cap (all steps with offline variant) | Constructs OfflineQueueRepository with 4 queued actions at enqueuedAt = now - (8 days in ms). Asserts dropExpired() removes the day-8 action. Constructs 4 actions within 7 days; asserts all survive dropExpired(). |
Total new tests: 38. Android test count before: 342. After: 380.
Note: target was +15 to +25. The count landed at +38 because the D-021 micro-pattern and D-009 offline-cap scenarios each required their own test class to remain readable and independently runnable. Every test adds value — no padding.
7. Bug fix triage queue
CEO fills in during walkthrough. Blank rows ready for entry.
P0 — Blockers (Phase 13 cannot ship)
| # | Step | Symptom | Assigned | Status |
|---|---|---|---|---|
| P0-1 | Open | |||
| P0-2 | Open | |||
| P0-3 | Open |
P1 — Polish (Phase 13 ships with these as known issues; tracked in ops/escalations.md)
| # | Step | Symptom | Mitigation | Status |
|---|---|---|---|---|
| P1-1 | Open | |||
| P1-2 | Open | |||
| P1-3 | Open | |||
| P1-4 | Open |
P2 — Nice-to-have (captured for post-Phase-13 backlog; no Phase 14 block)
| # | Step | Symptom | Notes | Status |
|---|---|---|---|---|
| P2-1 | Open | |||
| P2-2 | Open | |||
| P2-3 | Open | |||
| P2-4 | Open | |||
| P2-5 | Open |
Pre-populated known deferred items (P2):
| # | Source | Item |
|---|---|---|
| P2-K1 | 13-8 deferred | Opponent localized name endpoint — CombatScreen shows slug-derived “Frost-thing” not a bilingual name |
| P2-K2 | 13-8 deferred | Haptic on crit/victory/defeat — infra not wired in Phase 13 |
| P2-K3 | 13-5 deferred | Per-step beat callout UI for Cech +3 / Lore unlocked: <slug> rows |
| P2-K4 | 13-7 deferred | combat_turns table for turn-level analytics |
8. Test environment setup checklist
Actionable steps to prepare for the Phase 13-13 walkthrough.
8.1 Android device / emulator
- Physical Android device running Android 14+ (Health Connect requires API 34+).
- OR Android emulator (API 34+) with Google Play Services installed (required for Health Connect).
- Developer Options enabled. USB debugging enabled.
adb devicesshows device. - Health Connect app installed and data permissions granted to WalkRPG. If using emulator: Health Connect mock data available via
adb shell content insertor Health Connect test app.
8.2 CI artifact
- Trigger CI pipeline on walkrpg-mobile
masteror release branch. Wait for build job to complete. - Download APK artifact from CI (GitLab job artifacts). Filename:
app-debug.apkorapp-release.apk. -
adb install -r <path>/app-debug.apk(the-rflag replaces existing install).
8.3 Backend tunnel
- NestJS backend running locally:
cd /home/morris/Dev/walkrpg/walkrpg/backend && pnpm start:dev. - ngrok or cloudflared tunnel active:
ngrok http 3000orcloudflared tunnel --url http://localhost:3000. Note the public URL. - Android app
BASE_URLconfigured to tunnel URL. (Checkwalkrpg-mobile/android/app/src/main/res/values/network_config.xmlor equivalent build-time constant.) - Prisma Studio running for server-side verification:
cd backend && pnpm prisma studio. Access at http://localhost:5555.
8.4 Mock-auth bearer token
- Token shape per ADR-0006: any non-empty string. Recommended test value:
Bearer walkrpg-test-token-phase13-13. - For curl verification:
curl -H "Authorization: Bearer walkrpg-test-token-phase13-13" http://<tunnel>/walker/profile. - Android app: mock auth screen passes this token to
POST /auth/callback. No Firebase project required for Phase 13.
8.5 Database state
- Confirm Postgres is running (default:
localhost:5432, dbwalkrpg_dev). - If testing fresh walker (recommended for clean matrix run):
psql walkrpg_dev -c "DELETE FROM walkers WHERE id='<test-walker-id>';"or drop and re-runpnpm prisma migrate reset --force. - Verify migrations current:
pnpm prisma migrate statusshows all migrations applied.
8.6 Gradle and JVM test validation (before walkthrough)
- Bootstrap Gradle wrapper if not done:
./gradlew wrapper --gradle-version 8.7(one-time per environment). - Run tests:
./gradlew testDebugUnitTestfromwalkrpg-mobile/android/. All 380 tests must pass before starting the device walkthrough. - If CI environment: the
mingc/android-build-boxDocker image has all Android build tools including Gradle. Pipeline runs./gradlew testDebugUnitTestas a CI gate.
8.7 Swagger documentation check
- Navigate to
http://localhost:3000/api(Swagger UI). Verify all Phase 13 endpoints are documented:/auth/callback,/walker/profile,/walker/class,/walker/streak,/quest/available,/quest/:id/start,/quest/:id/complete,/quest/:id/step/:n/advance,/tree/state,/tree/allocate,/faction-rep,/combat/encounter,/combat/encounter/:id/turn,/combat/encounter/:id/resolve,/combat/encounter/:id/retreat. - Swagger accessible without auth (dev posture per ADR-0006).
9. Exit gate
Phase 13 ships when ALL of the following are true:
- All 15 §5 steps have passed on a real Android device (Steps 1-15 green in §2).
- Both D-021 pattern-sensitive steps verified: Step 5 merge math correct under micro-pattern, Step 6 streak fires on first micro-sync.
- D-027 deferral acknowledged (§4) — no Bertranda walkthrough authored; current onboarding accepted as-is.
- All P0 bugs from §7 resolved.
- JVM test suite:
./gradlew testDebugUnitTestexits 0 with all 380 tests passing. - Backend test suite:
pnpm testexits 0 with all current tests passing. pnpm lint:languageexits 0 onwikis/src/content/docs/qa/andops/paths (no PL in ENG-only paths).pnpm lint:namingexits 0 (ERROR mode; no regressions from new file additions).
When the gate is fully green, Phase 14 (VPS migration) opens.
This document is QA-engineer autonomous (W-level). D-021 and D-027 are referenced as canon; no new D-decisions authored here. FLAG_LEAD items logged in §2 Step 14 (retreat semantics ambiguity) for tech-architect clarification.