From 9d6572bc3347926d6333c8da2b3d6041f1623104 Mon Sep 17 00:00:00 2001 From: mrambossek Date: Sat, 1 Aug 2026 11:38:45 +0200 Subject: [PATCH] compat: fix the too-new message's grammar, add a live gate test The generated refusal read "point at a app within range". Also adds LiveCompatTest, which checks the half a unit test cannot reach: that two independently-built artifacts agree on the window, that the profile stays readable for a version the server refuses, and that both bounds are enforced. Co-Authored-By: Claude Fable 5 --- .../app/echo_lot/engine/LiveCompatTest.kt | 71 +++++++++++++++++++ server/internal/compat/compat.go | 2 +- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 echolot-app/core-engine/src/test/kotlin/app/echo_lot/engine/LiveCompatTest.kt diff --git a/echolot-app/core-engine/src/test/kotlin/app/echo_lot/engine/LiveCompatTest.kt b/echolot-app/core-engine/src/test/kotlin/app/echo_lot/engine/LiveCompatTest.kt new file mode 100644 index 0000000..dd253d6 --- /dev/null +++ b/echolot-app/core-engine/src/test/kotlin/app/echo_lot/engine/LiveCompatTest.kt @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2026 Echolot contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +package app.echo_lot.engine + +import app.echo_lot.protocol.Compat +import app.echo_lot.protocol.ControlClient +import app.echo_lot.protocol.VersionRefused +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail + +/** + * Checks the version gate against a LIVE server — the half that unit tests cannot reach, because + * the whole point is that two independently-built artifacts agree. Self-skips without + * ECHOLOT_LIVE_*. + */ +class LiveCompatTest { + + private val url = System.getenv("ECHOLOT_LIVE_URL") + private val pin = System.getenv("ECHOLOT_LIVE_PIN") + private val cred = System.getenv("ECHOLOT_LIVE_CRED") + + private fun clientAs(version: String) = ControlClient(url!!, setOf(pin!!), version) + + @Test + fun theServerAdvertisesAndEnforcesItsWindow() { + if (url == null || pin == null || cred == null) { + println("LiveCompatTest skipped (no ECHOLOT_LIVE_* env)"); return + } + + // The profile must state the window — without it the app cannot pre-empt a refusal. + val profile = clientAs("0.2.0").profile(cred) + println("server ${profile.serverVersion} protocol=${profile.compat.protocolVersion} " + + "accepts app [${profile.compat.appMin}, ${profile.compat.appMax})") + assertTrue(profile.compat.protocolVersion.isNotBlank(), "profile omits protocol_version") + assertTrue(profile.compat.appMin.isNotBlank(), "profile omits app_min") + + // This build must be inside it, or every other live test here is meaningless. + val verdict = Compat.check(profile, "0.2.0") + assertEquals(Compat.Verdict.OK, verdict.verdict, verdict.message ?: "") + + // The profile stays reachable for a version the server would otherwise refuse: that is + // how a refused client discovers what it needs. + val ancient = clientAs("0.1.0") + val stillReadable = ancient.profile(cred) + assertEquals(profile.serverVersion, stillReadable.serverVersion, + "the profile endpoint must never be gated on app version") + + // And a gated endpoint refuses it, with a message naming the window. + try { + ancient.createSession(cred, System.getenv("ECHOLOT_LIVE_TARGET") ?: "fmr") + fail("server accepted a session from an out-of-window app") + } catch (e: VersionRefused) { + val msg = assertNotNull(e.message) + println("refused as expected: $msg") + assertTrue(msg.contains("0.1.0"), "refusal should name the offending version: $msg") + assertTrue(msg.contains(profile.compat.appMin), "refusal should name the window: $msg") + } + + // Too new is refused the same way — the window is a range, not a floor. + try { + clientAs("99.0.0").createSession(cred, System.getenv("ECHOLOT_LIVE_TARGET") ?: "fmr") + fail("server accepted a session from an app above its window") + } catch (e: VersionRefused) { + println("too-new refused as expected: ${e.message}") + } + } +} diff --git a/server/internal/compat/compat.go b/server/internal/compat/compat.go index 947dbfa..6d19c16 100644 --- a/server/internal/compat/compat.go +++ b/server/internal/compat/compat.go @@ -183,7 +183,7 @@ func Check(peer string, r Range, peerName string) (Verdict, string) { return TooOld, fmt.Sprintf("%s %s is older than this build supports (needs %s). Update the %s.", peerName, v, r, peerName) case r.HasMax && !v.Less(r.Max): - return TooNew, fmt.Sprintf("%s %s is newer than this build supports (accepts %s). Update this side, or point at a %s within range.", + return TooNew, fmt.Sprintf("%s %s is newer than this build supports (accepts %s). Update this side, or use a version of the %s within that range.", peerName, v, r, peerName) } return OK, ""