From 0208a577c42edd5c2bc0ffe970d4f896c6411677 Mon Sep 17 00:00:00 2001 From: QIU Date: Tue, 25 Aug 2026 15:25:32 +0000 Subject: [PATCH] fix(aprs): do not tell a mistyped passcode it is in receive-only mode The previous commit derived "wants receive-only" from AprsPasscode.canTransmit, which is a boolean over four cases. Both a deliberate -1 and a mistyped passcode return false, so fixing the mis-diagnosis in one direction introduced it in the other: measured against the shipped algorithm, three entries - a passcode off by one digit, an arbitrary number, and a non-numeric entry - were all told they were connected in receive-only mode, when what they needed to hear was that the passcode does not match the callsign. classify already distinguishes these; only its ReceiveOnly case counts as a deliberate choice. A test now pins the distinction, including the fact that all four entries are equally unable to transmit - which is exactly why the boolean was not enough. Found by probe before review, not by the suite, which had no test for the reporter's use of this and still does not. --- .../look4sat/core/data/aprs/AprsReporter.kt | 11 +++++--- .../core/domain/aprs/AprsPasscodeTest.kt | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt index 5a2cd0e7..c089630b 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt @@ -127,10 +127,13 @@ class AprsReporter( } val packetLine = (beacon as AprsBeacon.Result.Line).text - // Receive-only is a deliberate choice, not a mistake, and has to be carried through: - // it logs in with -1 exactly as a wrong passcode does, and the server answers - // "unverified" to both. - val wantsReceiveOnly = !AprsPasscode.canTransmit(cfg.callsign, cfg.passcode) + // Receive-only is a deliberate choice and has to be told apart from a typo, because + // both log in with -1 and the server answers "unverified" to each. Classifying rather + // than asking canTransmit: that collapses ReceiveOnly and Mismatch into one boolean, + // so a mistyped passcode would be told it is in receive-only mode - the same + // mis-diagnosis as before, pointing the other way. + val wantsReceiveOnly = + AprsPasscode.classify(cfg.callsign, cfg.passcode) is AprsPasscode.Entry.ReceiveOnly val c = client ?: AprsIsClient( host = cfg.server, port = cfg.port, diff --git a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/aprs/AprsPasscodeTest.kt b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/aprs/AprsPasscodeTest.kt index c32f65ec..f779e47e 100644 --- a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/aprs/AprsPasscodeTest.kt +++ b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/aprs/AprsPasscodeTest.kt @@ -64,6 +64,31 @@ class AprsPasscodeTest { assertEquals(AprsPasscode.RECEIVE_ONLY, AprsPasscode.loginValue(callsign, wrong)) } + /** + * The three-way distinction the reporter depends on, and why it must not use canTransmit for + * it: that collapses a deliberate receive-only choice and a typo into one boolean. Telling an + * operator who mistyped their passcode that they are in receive-only mode is the same + * mis-diagnosis as telling a deliberate receive-only user their passcode is wrong, pointing + * the other way. + */ + @Test + fun `a deliberate receive-only entry is distinguishable from a typo`() { + val deliberate = AprsPasscode.classify(callsign, "-1") + val blank = AprsPasscode.classify(callsign, "") + val typo = AprsPasscode.classify(callsign, (correct + 1).toString()) + val garbage = AprsPasscode.classify(callsign, "abcde") + + assertTrue(deliberate is AprsPasscode.Entry.ReceiveOnly) + assertTrue(blank is AprsPasscode.Entry.ReceiveOnly) + assertFalse("a typo must not read as receive-only", typo is AprsPasscode.Entry.ReceiveOnly) + assertFalse("garbage must not read as receive-only", garbage is AprsPasscode.Entry.ReceiveOnly) + + // All four are equally unable to transmit, which is why the boolean is not enough. + for (entry in listOf("-1", "", (correct + 1).toString(), "abcde")) { + assertFalse(AprsPasscode.canTransmit(callsign, entry)) + } + } + @Test fun `something that is not a number is its own case`() { assertEquals(AprsPasscode.Entry.NotANumber, AprsPasscode.classify(callsign, "abcde"))