android / Build + lint + test (push) Successful in 4m12s
Two changes so a network handoff stops making the app refuse to play music. ## Correction first: half of what I proposed already existed I recommended "require corroboration before ServerDown, since Unstable is non-gating." ReachabilityMachine has done exactly that since it was written — onProbeFailure takes Reachable → Unstable, and escalates only on corroboration or the 120s backstop. There is even a test named `single probe failure is unstable not down`. I proposed building a thing that shipped months ago. Reading the machine properly turned up the real gap, which is narrower and more specific. ## 1. Probe when the app returns to the foreground The genuine missing piece, and #1209's own note had it backwards: it listed this as "already happens via link probe." It doesn't. `recheck()` had exactly two callers — a button in VersionTooOldBanner and pull-to-refresh — and nothing observed ProcessLifecycleOwner. The link probe fires on a connectivity *change*, so an app backgrounded on stable Wi-Fi gets none. That made a stale ServerDown outlive its cause: the poll loop's delay() is throttled while screen-off/doze, so recovery waited for whenever the OS next let the loop run. June's capture recovering at "EXACTLY 22:31:10 app_foreground" was the throttled delay resuming, not a deliberate probe — same timestamp, different mechanism, and that difference is the whole bug. NetworkStatusController now implements DefaultLifecycleObserver and calls the existing recheck() on ON_START. force = true, so it also bypasses ARBITRATE_MIN_GAP_MS: a user opening the app is exactly when a stale banner and a refused track are most visible, and it's once per foreground. ## 2. A burst of op failures no longer corroborates itself The actual defect in the escalation path. Corroboration required 2 op failures within 30s — but a link handoff fails every in-flight request at once, so a burst is ONE event producing N failures, not N independent observations that the server is gone. Two simultaneous failures walked straight to Unreachable. onOpFailure now drops a failure landing within CORROBORATION_MIN_SPACING_MS (3s) of the last recorded one. Above the sub-second window a handoff occupies, low enough that a real outage still corroborates within seconds once anything retries. ## Why this matters more than the task implied #1209 called the follow-ups "cosmetic in the diagnostics". They aren't. OfflineGatedDataSource.gateOnHealth() throws OfflineException on ServerDown BEFORE touching the network, and TrackRow disables rows. So a spurious ServerDown means the app declines to play uncached tracks that would play fine — for a blip that already resolved. The note's "captured skips advanced fine" was timing luck, not evidence the gate is harmless. ## Tests `two op failures plus a failed probe escalate immediately` used timestamps 500ms apart, which the new rule treats as a burst — so I re-spaced it and renamed it `two SPACED op failures...`. That's a deliberate reversal of an encoded expectation, not a broken test being patched. Also re-spaced `stale op failures do not corroborate` (used 0 and 1_000): left alone it would still have passed, but for the wrong reason — burst-dropping rather than staleness — and a test that can't fail for its stated reason is worse than no test. Added: a burst of four failures plus a failed probe stays Unstable, and a burst that never recovers still escalates via the sustained backstop, so dropping duplicates can't make a real outage undetectable. The foreground hook itself is unverifiable in a JVM test (ProcessLifecycleOwner needs the framework, and there's no instrumentation lane). Checked instead that nothing constructs NetworkStatusController outside Hilt, so init's ProcessLifecycleOwner.get() only runs on the main thread during Application.onCreate — the same pattern LiveEventsDispatcher already uses.