From ed05e92809a91be7504902ca8a5a6fff36283066 Mon Sep 17 00:00:00 2001 From: "Konstantin Duczek (Claude Opus 5.5)" Date: Fri, 23 Oct 2026 18:48:26 +0200 Subject: [PATCH] android: drop health warnings when backend reports none When the backend is fully healthy it sends Health.State with Warnings = null (a nil Go map), not an empty map. HealthNotifier only called notifyHealthUpdated through a ?.let chain, so a null Warnings skipped the update entirely and a previously shown warning was never removed. In practice a short dns-forward-failing blip (e.g. switching networks) left "DNS unavailable" on the main screen for hours after the backend had logged "Health updated: null". Treat null Warnings as an empty set. The existing tests passed only because they used emptyMap(); add one with Warnings = null. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EXBrJu3voaKptscn5jq7vt Signed-off-by: Konstantin Duczek (Claude Opus 5.5) --- .../ipn/ui/notifier/HealthNotifier.kt | 6 +++++- .../ipn/ui/notifier/HealthNotifierTest.kt | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt b/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt index 1e2bfc20a0..21eb18a157 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifier.kt @@ -74,7 +74,11 @@ class HealthNotifier( } .collect { health -> TSLog.d(TAG, "Health updated: ${health?.Warnings?.keys?.sorted()}") - health?.Warnings?.values?.filterNotNull()?.toTypedArray()?.let(::notifyHealthUpdated) + // A healthy backend sends Warnings = null; treat it as empty so that + // previously shown warnings are dropped. + notifyHealthUpdated( + health?.Warnings?.values?.filterNotNull()?.toTypedArray() ?: emptyArray() + ) } } } diff --git a/android/src/test/kotlin/com/tailscale/ipn/ui/notifier/HealthNotifierTest.kt b/android/src/test/kotlin/com/tailscale/ipn/ui/notifier/HealthNotifierTest.kt index b14b719fd5..be382b2a3c 100644 --- a/android/src/test/kotlin/com/tailscale/ipn/ui/notifier/HealthNotifierTest.kt +++ b/android/src/test/kotlin/com/tailscale/ipn/ui/notifier/HealthNotifierTest.kt @@ -102,6 +102,24 @@ class HealthNotifierTest { assertTrue(notifier.currentWarnings.value.isEmpty()) } + /** + * The backend reports a fully healthy state as Warnings = null (a nil Go map), not as an empty + * map. The warning must still be dropped. + */ + @Test + fun warningClearsWhenBackendSendsNullWarnings() = runTest { + val (healthFlow, _, notifier) = createRunningNotifier() + + healthFlow.value = healthState(derpWarning()) + settle() + assertEquals(1, notifier.currentWarnings.value.size) + + healthFlow.value = Health.State(Warnings = null) + settle() + assertTrue(notifier.currentWarnings.value.isEmpty()) + assertNull(notifier.currentIcon.value) + } + @Test fun warningsDroppedWhenStateLeavesRunning() = runTest { val (healthFlow, ipnFlow, notifier) = createRunningNotifier()