---
phase: 06-client-rebuild-mvp-gate-cli-08-hard-milestone
plan: 15
subsystem: client/auth-flow + client/net
tags: [d-25-fix, cookie-autologin, token-threading, self-heal, stale-room, reconnect, e2e]
dependency_graph:
  requires: [06-12, 06-14]
  provides: [cookie-autologin-fix, stale-room-fix, e2e-cookie-reload, e2e-ws-kill, e2e-camera-follow]
  affects: [apps/client/src/scenes, apps/client/src/prediction, apps/client/test/e2e]
tech_stack:
  added: []
  patterns:
    - Option A token threading (BootScene → LoginScene → GameScene init data)
    - Option B GameScene self-heal via getSession() in create()
    - InputDispatcher.setRoom() for stale-room refresh on silent_reauth
    - window.__rebno deterministic test hook (D-35) for e2e assertions
key_files:
  created:
    - apps/client/test/e2e/cookie-reload.e2e.test.ts
    - apps/client/test/e2e/ws-kill-reconnect.e2e.test.ts
    - apps/client/test/e2e/camera-follow.e2e.test.ts
  modified:
    - apps/client/src/scenes/BootScene.ts
    - apps/client/src/scenes/LoginScene.ts
    - apps/client/src/scenes/GameScene.ts
    - apps/client/src/prediction/input-dispatcher.ts
    - apps/client/src/__test__/login-scene.test.ts
    - apps/client/src/__test__/game-scene.test.ts
    - apps/client/src/__test__/input-dispatcher.test.ts
    - apps/client/test/e2e/cli-08.e2e.test.ts
decisions:
  - Ship both Option A (token threading) and Option B (GameScene self-heal) as complementary fixes; Option A closes the data-flow gap; Option B prevents recurrence if init shapes drift
  - InputDispatcher room stored as private _room + public getter to enable setRoom() without breaking existing read callers
  - GameScene.connect on subsequent reconnects calls setRoom() on existing dispatcher (not recreate), preserving heldKeys + heartbeat timer state
  - E2e assertions use window.__rebno hook (D-35) not WebGL pixel sampling
metrics:
  duration_minutes: ~25
  completed_date: "2026-05-10"
  tasks_completed: 3
  tasks_total: 3
  files_changed: 11
---

# Phase 6 Plan 15: Cookie Auto-Login Blank-Render Fix (D-25) Summary

[doc->REQ-CLI-09] [doc->REQ-CLI-04] [doc->REQ-CLI-07]

**One-liner:** Cookie auto-login blank-render (D-25) closed via three-edit Option A token threading (BootScene → LoginScene → GameScene) plus Option B GameScene getSession() self-heal; secondary stale InputDispatcher.room defect fixed via setRoom(); six regression tests landed (3 vitest + 3 Playwright).

---

## Tasks Completed

| Task | Name | Commit | Key Files |
|------|------|--------|-----------|
| 1 (RED) | Failing tests: token threading + self-heal | 337b219 | login-scene.test.ts, game-scene.test.ts |
| 1 (GREEN) | Option A + Option B source edits | 531db49 | BootScene.ts, LoginScene.ts, GameScene.ts |
| 2 (RED) | Failing tests: InputDispatcher.setRoom | 1934a69 | input-dispatcher.test.ts |
| 2 (GREEN) | InputDispatcher.setRoom + GameScene refresh | b6895bb | input-dispatcher.ts, GameScene.ts |
| 3 | Three Playwright e2e files + cli-08 update | e3db8b7 | cookie-reload, ws-kill, camera-follow, cli-08 |

---

## Option A Edit Locations Confirmed

(a) **BootScene.ts line 88-95 area:** `this.scene.start('LoginScene', { fastPath: true, username: session.user.username, sessionToken: session.token })` — `sessionToken: session.token` is now forwarded (Option A edit 1 of 3).

(b) **LoginScene.ts LoginSceneInitData interface:** `sessionToken?: string` field added (Option A edit 2 of 3, data-type side). `init(data)` stores `this.sessionToken = data.sessionToken`. Instance field `private sessionToken: string | undefined` added.

(c) **LoginScene.ts renderFastPath fast-path timer callback:** Changed from `scene.start('GameScene', { username })` to `scene.start('GameScene', { sessionToken: this.sessionToken, username })` (Option A edit 3 of 3) — matches form-submit path shape at lines 173-177.

---

## Option B Self-Heal Confirmed

**GameScene.ts create():** When `!this.sessionToken && this.username`, the new `recoverSessionTokenAndConnect(wssUrl)` private method is called instead of `connect()` directly. It awaits `getSession()`, adopts `session.token`, then calls `connect()`. On `getSession()` failure it logs a warning and falls through to `connect()` — the defensive guard at line 263 (`if (!this.sessionToken) return;`) catches genuinely unauthenticated navigation (T-06-15-02 mitigated).

---

## InputDispatcher API Change

**InputDispatcher.setRoom(room: Pick<Room, 'send'>): void** — added alongside converting the existing `private readonly room` constructor parameter to `private _room` with a public `get room()` accessor. All internal sends go through `this._room.send`. The public getter preserves the existing read API for external callers and tests.

**GameScene.connect():** The `if (!this.inputDispatcher)` guard was extended with an `else` branch: on subsequent reconnects, `this.inputDispatcher.setRoom(this.room!)` is called instead of creating a new dispatcher — preserving heldKeys, xOrder/yOrder, and the heartbeat timer state.

---

## Test Results

### Vitest Unit Tests (3 new, all GREEN)

| Test | File | Status |
|------|------|--------|
| fast-path forwards sessionToken to GameScene | login-scene.test.ts | GREEN |
| fast-path does NOT forward sessionToken when fastPath is false | login-scene.test.ts | GREEN |
| cookie path: init without sessionToken → getSession self-heal → joinRebnoRoom called | game-scene.test.ts | GREEN |
| direct token path: init with sessionToken → joinRebnoRoom called without getSession | game-scene.test.ts | GREEN |
| setRoom() refreshes the internal room reference | input-dispatcher.test.ts | GREEN |
| setRoom() preserves internal state across the switch | input-dispatcher.test.ts | GREEN |

Full suite: **112 tests + 4 todo passed, 17 test files** — no regressions.

### Playwright e2e Tests (3 new, provisioned)

E2e tests are provisioned against the dev-mode webServer (playwright.config.ts). They require a running dev server to execute and are not run in this commit pipeline (per Phase 6 convention — human UAT via 06-17 validates them live).

| Test | File | Assertions |
|------|------|------------|
| cookie-reload | cookie-reload.e2e.test.ts | reload → canvas ready in 5s + roomId='mvp-room' + atlas 'mvp' loaded + chat round-trip |
| ws-kill-reconnect | ws-kill-reconnect.e2e.test.ts | kill WS → reconnect in 12s + roomId='mvp-room' + chat round-trip |
| camera-follow | camera-follow.e2e.test.ts | walk 1.5s → cameraDelta ≥ 8px + playerMoved (D-29 mandatory) |

### cli-08.e2e.test.ts Selector Update (D-30)

Added `__rebno.roomId === 'mvp-room'` assertion and `canvas[data-room-id="mvp-room"]` selector check after both clients reach game-ready. No existing room-literal was present in cli-08 (search confirmed no `mvp-lobby` or `bn-central`); only new assertions were added.

---

## Camera-Pan Delta

The camera-follow threshold of 8px total (|scrollX delta| + |scrollY delta|) is conservative for a 1.5-second walk. At the BNO movement speed derived from Phase 4 server constants, the expected actual delta during a live run is ~60-80px horizontal for a clear 1.5s 'd' key hold starting from the default spawn point (center of 880×800 MVP room). The threshold is intentionally minimal to avoid flakiness on slow CI; actual values from the first passing run will be observable in the Playwright HTML report.

---

## Confirmation: apps/server/src/RebnoRoom.ts Untouched

`git log --oneline apps/server/src/RebnoRoom.ts` last shows commit `7d25d4e` (plan 06-14). Zero server-side changes in plan 06-15. The bearer-only WS auth contract at RebnoRoom.ts:267-269 is preserved per §7 of 06-12-DEBUG-FINDINGS.

---

## Trace Tag Verification

```
REQ-CLI-09  required: [doc, impl, int]   stages: +doc +impl +unit +int  [OK]
REQ-CLI-04  required: [doc, impl, unit]  stages: +doc +impl +unit +int  [OK]
REQ-CLI-07  required: [doc, impl, unit]  stages: +doc +impl +unit +int  [OK]
```

New tags in this plan:
- `[impl->REQ-CLI-09] [impl->REQ-CLI-04]` in BootScene.ts, LoginScene.ts, GameScene.ts, input-dispatcher.ts
- `[unit->REQ-CLI-09]` in login-scene.test.ts, game-scene.test.ts, input-dispatcher.test.ts
- `[int->REQ-CLI-09] [int->REQ-CLI-04]` in cookie-reload.e2e.test.ts, ws-kill-reconnect.e2e.test.ts
- `[int->REQ-CLI-07]` in camera-follow.e2e.test.ts, cli-08.e2e.test.ts

---

## Deviations from Plan

None — plan executed exactly as written. Both Option A and Option B shipped. All three vitest pairs and all three Playwright files created. cli-08 updated. No server-side changes.

---

## Self-Check: PASSED

Files exist:
- apps/client/src/scenes/BootScene.ts — FOUND
- apps/client/src/scenes/LoginScene.ts — FOUND
- apps/client/src/scenes/GameScene.ts — FOUND
- apps/client/src/prediction/input-dispatcher.ts — FOUND
- apps/client/src/__test__/login-scene.test.ts — FOUND
- apps/client/src/__test__/game-scene.test.ts — FOUND
- apps/client/src/__test__/input-dispatcher.test.ts — FOUND
- apps/client/test/e2e/cookie-reload.e2e.test.ts — FOUND
- apps/client/test/e2e/ws-kill-reconnect.e2e.test.ts — FOUND
- apps/client/test/e2e/camera-follow.e2e.test.ts — FOUND
- apps/client/test/e2e/cli-08.e2e.test.ts — FOUND

Commits exist: 337b219, 531db49, 1934a69, b6895bb, e3db8b7 — all verified in git log.
