# T-2.10 Review

## Verdict
[x] NEEDS CHANGES (blockers listed)

## Test runs
- piRemoteTests/SessionConnectionLifecycleTests: 10/10 passed
- piRemoteTests/AppStateLifecycleTests: 6/6 passed
- piRemoteUITests/BackgroundLifecycleUITests: 4/4 passed
- piRemoteUITests (full): 12/12 passed (regression check)

All tests pass independently confirmed — impl agent's numbers are accurate.

## Findings

### Blockers

#### B-1: `ResumeCursor.update()` is never called — delta-pull is dead in production

**Location:** `Sources/Core/Sessions/SessionConnection.swift`, `incomingBinary` sink (~line 139)

```swift
ws.incomingBinary
    .sink { [weak self] frame in
        guard let self else { return }
        self.isStreamFrozen = false         // T-2.10: first delta clears freeze
        self.scrollback.append(frame.data)
        self.stream.send(frame.data)
        // ❌ MISSING: ResumeCursor().update(sessionId: id, seq: frame.seq)
    }
```

`BinaryFrame` carries a `seq: UInt64` from the server. It is parsed correctly (`FrameCodec.swift:162`), but never passed to `ResumeCursor.update()`. As a result:

- `ResumeCursor().lastSeq(for: conn.id)` always returns `nil` (UserDefaults key never set).
- On background: the `scrollback.sizeBytes > 0` condition can be true, but `ResumeCursor().lastSeq()` → `nil` → `lastCapturedSeq = nil`.
- On foreground: `conn.resume(from: nil)` → fresh attach, not delta-pull.
- `isStreamFrozen` is always `false` in production.
- The "Reconnecting…" status text is never shown in production.

**Why tests pass anyway:**
- Unit tests call `resume(from: 100)` / `resume(from: 42)` directly with a hardcoded non-nil seq — they bypass cursor tracking entirely.
- UI tests use `lastCapturedSeq = 1` (hardcoded sentinel in the `isUITestStub` branch) — also bypass cursor tracking.

**Fix required:** Call `ResumeCursor().update(sessionId: id, seq: frame.seq)` in the `incomingBinary` sink after appending to scrollback.

---

### Nits

#### N-1: `stubMode` and launch-arg hooks not gated by `#if DEBUG`

`SessionConnection.stubMode` is `internal` (no `#if DEBUG`). `AppState.init()` processes `--uitest-with-stub-connection`, `--reset-state`, `--enable-faceid`, `--force-lock` unconditionally. `MainTerminalView.isUITestStub` and the `bootstrapStubConnection()` path are also in production code.

iOS sandbox prevents arbitrary launch args from external callers, so the risk is low. Recommend wrapping with `#if DEBUG` for hygiene, particularly `stubMode` which bypasses WebSocket entirely.

#### N-2: Post-Face-ID-auth reconnect gap

`appWillForeground()` emits `false` on `lifecycleTransitions` **before** the Face-ID gate executes:

```swift
func appWillForeground() async {
    _lifecycleTransitions.send(false)   // ← fires before isLocked is set
    let elapsed = Date().timeIntervalSince(lastForegroundedAt)
    guard elapsed > 60 else { return }
    isLocked = true
    let ok = await FaceIDGate.authenticate()
    isLocked = !ok                      // ← cleared here, but no reconnect signal
}
```

The timing is **safe** (the `.onReceive` `Task { @MainActor }` is enqueued but doesn't run until the main actor yields at `await FaceIDGate.authenticate()`, by which point `isLocked = true` is already set, so `guard !appState.isLocked` correctly blocks the resume). Security is intact.

However, when elapsed > 60s and Face-ID **succeeds** (`isLocked` becomes `false`), no reconnect signal is ever sent. The terminal stays suspended (status: "Disconnected") until the user triggers another foreground cycle. Not a regression from prior behavior, but worth tracking.

#### N-3: `nonisolated(unsafe) var cancellables` in test classes

`@MainActor`-isolated test classes use `nonisolated(unsafe)` to store `cancellables` because XCTest doesn't guarantee main-actor delivery for `tearDown`. The pattern is consistent across both test files and works correctly, but consider using `@MainActor func tearDown() async` to avoid the `nonisolated(unsafe)` escape.

---

### Coverage gaps

**CG-1 (follows from B-1):** No test exercises the full path: binary frame received → `ResumeCursor.update()` → background → `lastCapturedSeq` non-nil → foreground → `resume(from: nonNil)` → `isStreamFrozen = true`. All tests that check `isStreamFrozen = true` inject the seq directly or use a stub sentinel. This path must be covered once B-1 is fixed.

**CG-2:** Post-Face-ID-auth reconnect (elapsed > 60s, auth succeeds, terminal should reconnect). See N-2.

**CG-3:** `isStreamFrozen` actually gates frame delivery — only the boolean state is unit-tested. There is no test that checks frames queued/dropped while `isStreamFrozen = true` (N/A for the current design since freeze is informational, not a gate, but should be noted if the spec intends it as a real gate).

**CG-4:** Multiple reconnect cycles with seq progression (seq₀ → suspend → resume(from: seq₀) → seq₁ → suspend → resume(from: seq₁) → …) are not exercised.

---

## Additional checks (from review checklist)

- **`connection?.lastSeq` captured before suspend:** ✓ `lastCapturedSeq` is assigned from `ResumeCursor().lastSeq()` before `await conn.suspend()` in the `.onReceive` handler.
- **Face-ID bypass:** ✓ Not bypassed — timing analysis above confirms the `Task { @MainActor }` sees `isLocked = true` before running.
- **StatusBar transitions:** ✓ `isStreamFrozen` is set before `connectionState = .connecting` fires in both stub and real paths, so the sink observes the correct flag value.
- **No stuck "Reconnecting…":** ✓ Stub drives `.connected` after 800 ms; real path clears `isStreamFrozen` on first binary frame or snapshot.
- **Idempotent suspend/resume:** ✓ Tested and passes.
- **Latency <200 ms:** ✓ Passes (no blocking operations in the suspend/resume path).
- **Face-ID LockView regression:** ✓ `LockScreenUITests.testLockScreenAppearance` passes.
- **No existing UI test regressions:** ✓ All 12 UI tests pass.

## Branch SHA reviewed: 419ad2fec1ccd5f70997b7bb405e0a10ec1633a7
