diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/KermitMqttLogger.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/KermitMqttLogger.kt index 7aec5eb00a..3eb5037593 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/KermitMqttLogger.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/KermitMqttLogger.kt @@ -49,12 +49,11 @@ class KermitMqttLogger( MqttLogLevel.WARN -> logger.w(throwable) { message } - // The library reports a broker that closed the socket, an unreachable host, or Wi-Fi dropping mid-read at - // ERROR ("Read loop error: Not enough data available"), and the client then reconnects on its own. Those - // are expected for a mobile client and dominated our error tracking, so log them at WARN — diagnosable in - // a support session, but not counted as application errors. + // The library logs its connection lifecycle at ERROR — broker socket close, unreachable host, Wi-Fi drop — + // and reconnects itself; broker rejections arrive without a throwable and are user config, surfaced via + // MqttProbeStatus. Neither is an app defect, so both log at WARN. Real faults carry a throwable. MqttLogLevel.ERROR -> - if (throwable?.isExpectedConnectionFailure() == true) { + if (throwable == null || throwable.isExpectedConnectionFailure()) { logger.w(throwable) { message } } else { logger.e(throwable) { message } diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/KermitMqttLoggerTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/KermitMqttLoggerTest.kt index d053c1ca1e..500c2e6b16 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/KermitMqttLoggerTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/KermitMqttLoggerTest.kt @@ -67,10 +67,36 @@ class KermitMqttLoggerTest { } @Test - fun `an error with no throwable stays an error`() { + fun `an error with no throwable is downgraded to WARN`() { + // A bare message carries no stack or type to triage, so it must not count as an application error. mqttLogger.log(level = MqttLogLevel.ERROR, tag = "MqttClient", message = "boom", throwable = null) - assertEquals(Severity.Error, writer.entries.single().first) + assertEquals(Severity.Warn, writer.entries.single().first) + } + + @Test + fun `a connection teardown log is not reported as an application error`() { + mqttLogger.log( + level = MqttLogLevel.ERROR, + tag = "MqttConnection", + message = "Fatal error — tearing down connection", + throwable = null, + ) + + assertEquals(Severity.Warn, writer.entries.single().first) + } + + @Test + fun `a broker rejection is a user configuration problem rather than an application error`() { + // Surfaced to the user through MqttProbeStatus, so it is not an app defect. + mqttLogger.log( + level = MqttLogLevel.ERROR, + tag = "MqttConnection", + message = "Connection refused: BAD_USER_NAME_OR_PASSWORD", + throwable = null, + ) + + assertEquals(Severity.Warn, writer.entries.single().first) } @Test