Skip to content

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.en from data layer).
  • PL: class description prose uses name.pl — “Kartograf” or equivalent per data/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 0 in the progression card (D-016 home surface: single Poziom N number).
  • 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.Polite on 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/start returns status=in_progress.
  • POST /quest/quest.001-first-road/step/1/advance returns currentStepIndex=2.
  • Prisma Studio: quest_progress table row for this walker + quest has current_step_id updated.

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/complete returns pointsAwarded=1 (D-016: flat 1pt per quest).
  • Prisma Studio: walker row tree_points_banked incremented by 1. quest_progress row has completed_at set.

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: treePointsBanked incremented; 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/allocate with {"allocations":[{"type":"node","id":"node.even-stride","clusterId":"cluster.first-steps"}]} returns 200.
  • Prisma Studio: tree_allocations table 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_count plural 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-stride shown 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-rep returns faction.unfinished-guild rep > 0.
  • Prisma Studio: faction_rep table row for walker + faction_id='faction.unfinished-guild' has updated reputation column.

Client-side observable:

  • EN: Faction card shows “Unfinished Guild” (name.en per 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/start for Quest 002 returns 200.
  • POST /quest/:id/step/1/advance and /step/2/advance return 200.
  • When step 3 beat is an encounter: POST /combat/encounter called with questId + stepIndex=3 + opponentTemplateId=opponent.frost-thing-slow-vent.
  • Prisma Studio: combat_encounters row created with status=ACTIVE, seed field 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/resolve returns outcome=WALKER_VICTORY with factionRepDeltas, questStateAfter, encounterSummary.
  • For retreat: POST /combat/encounter/:id/retreat returns outcome=WALKER_RETREAT.
  • Prisma Studio: combat_encounters row status = RESOLVED or RETREATED. walker.current_hp updated.

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/available returns Quest 002 with currentStepIndex advanced 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_progress row has current_step_id reflecting advancement.
  • Faction rep deltas applied: Prisma faction_rep row updated per factionRepDeltas from /resolve response.

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.pl on step objects.
  • factionRepDeltas from /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 reads walker.current_hp).
  • EN: Tree Viewer shows node.even-stride allocated (from GET /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.
StepPure-micro outcomePure-evening outcomePattern-sensitive?
1 InstallIdentical — install is one-timeIdenticalNo
2 Sign inIdenticalIdenticalNo
3 Class pickIdenticalIdenticalNo
4 OnboardingIdenticalIdenticalNo
5 Step ingestMicro: 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 + streakMicro: 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 1Micro: 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 pointsMicro: 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 allocationMicro: 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 persistenceMicro: 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 repMicro: faction rep is a server-side value; shows correctly whenever home loads.Evening: same.No
12 Combat entryMicro: 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 resolveMicro: 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-combatMicro: 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 startMicro: 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.

StepDevice-only requirementWhy JVM cannot substitute
1APK install from CI artifactadb install requires a connected device or emulator with ADB daemon. JVM unit tests run on the host JVM — no APK install lifecycle.
2Mock auth network roundtrip to real tunnelPOST /auth/callback hits a real NestJS process over ngrok/cloudflared. JVM tests mock Retrofit — no real HTTP call.
5Health Connect READ permission grant + real sensor dataHealthConnectClient.getOrCreate() requires android.permission.health.READ_STEPS granted by a real Android consent dialog. The SDK has no JVM stub.
5Actual pedometer provenance (D-007 §1 attestation)Real Android sensor data carries hardware-sourced timestamps and sourceBundleId. JVM tests inject synthetic DayStepBucket objects without provenance.
6Live region TalkBack announcement during streak updateTalkBack 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.
9Provisional-to-confirmed visual transition animationanimateFloatAsState (500ms) and AnimatedVisibility require the Compose runtime on Android. JVM tests assert state values; they do not render animation frames.
10Android process death via force-stopActivityManager.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.
12Quest step beat → CombatScreen navigation triggerNavigation component (NavController.navigate()) requires the Android Compose Navigation runtime. Phase1313QuestCombatNavigationTest verifies the ViewModel emits the correct navigation event; actual screen transition is device-only.
13Turn-loop animation on real hardwareHP 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.
15Full cold start: EncryptedSharedPreferences persistence across process deathEncryptedSharedPreferences 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 fileNew testsSteps partially coveredKey gap filled
Phase1313ColdStartTest.kt6Steps 10, 15Simulates 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.kt7Step 15Constructs 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.kt5Step 12Verifies QuestDetailViewModel emits a NavigateToCombat event when the current quest beat is kind=encounter. Verifies the event carries correct questId, stepIndex, and opponentTemplateId.
Phase1313CombatResolutionPersistenceTest.kt5Step 14Verifies 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.kt6Step 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.kt4Step 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.kt5D-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)

#StepSymptomAssignedStatus
P0-1Open
P0-2Open
P0-3Open

P1 — Polish (Phase 13 ships with these as known issues; tracked in ops/escalations.md)

#StepSymptomMitigationStatus
P1-1Open
P1-2Open
P1-3Open
P1-4Open

P2 — Nice-to-have (captured for post-Phase-13 backlog; no Phase 14 block)

#StepSymptomNotesStatus
P2-1Open
P2-2Open
P2-3Open
P2-4Open
P2-5Open

Pre-populated known deferred items (P2):

#SourceItem
P2-K113-8 deferredOpponent localized name endpoint — CombatScreen shows slug-derived “Frost-thing” not a bilingual name
P2-K213-8 deferredHaptic on crit/victory/defeat — infra not wired in Phase 13
P2-K313-5 deferredPer-step beat callout UI for Cech +3 / Lore unlocked: <slug> rows
P2-K413-7 deferredcombat_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 devices shows device.
  • Health Connect app installed and data permissions granted to WalkRPG. If using emulator: Health Connect mock data available via adb shell content insert or Health Connect test app.

8.2 CI artifact

  • Trigger CI pipeline on walkrpg-mobile master or release branch. Wait for build job to complete.
  • Download APK artifact from CI (GitLab job artifacts). Filename: app-debug.apk or app-release.apk.
  • adb install -r <path>/app-debug.apk (the -r flag 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 3000 or cloudflared tunnel --url http://localhost:3000. Note the public URL.
  • Android app BASE_URL configured to tunnel URL. (Check walkrpg-mobile/android/app/src/main/res/values/network_config.xml or 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, db walkrpg_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-run pnpm prisma migrate reset --force.
  • Verify migrations current: pnpm prisma migrate status shows 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 testDebugUnitTest from walkrpg-mobile/android/. All 380 tests must pass before starting the device walkthrough.
  • If CI environment: the mingc/android-build-box Docker image has all Android build tools including Gradle. Pipeline runs ./gradlew testDebugUnitTest as 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:

  1. All 15 §5 steps have passed on a real Android device (Steps 1-15 green in §2).
  2. Both D-021 pattern-sensitive steps verified: Step 5 merge math correct under micro-pattern, Step 6 streak fires on first micro-sync.
  3. D-027 deferral acknowledged (§4) — no Bertranda walkthrough authored; current onboarding accepted as-is.
  4. All P0 bugs from §7 resolved.
  5. JVM test suite: ./gradlew testDebugUnitTest exits 0 with all 380 tests passing.
  6. Backend test suite: pnpm test exits 0 with all current tests passing.
  7. pnpm lint:language exits 0 on wikis/src/content/docs/qa/ and ops/ paths (no PL in ENG-only paths).
  8. pnpm lint:naming exits 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.