From 8f46495426ce3e7b1c4a5f0c6e07321f9ce2f159 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 27 Jul 2026 18:23:50 +0200 Subject: [PATCH] seam sweep: move the TieredBackend witness into production code (no version bump) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A witness in a _test.go file fires on go test and go vet but NOT on go build alone — and a build-only step is exactly how the R-88 Part 2 near-miss would have shipped. Moved beside the type it pins, and added one for AgentVersionReporter. No defect found: quiesceBackend and *Client both satisfy their interfaces today. No version bump, no deploy — compile-time only. --- controller/cmd/controller/seam_witnesses.go | 22 +++++++++++++++++++ .../controller/tieredbackend_assert_test.go | 19 ---------------- controller/internal/agentapi/features.go | 6 +++++ 3 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 controller/cmd/controller/seam_witnesses.go delete mode 100644 controller/cmd/controller/tieredbackend_assert_test.go diff --git a/controller/cmd/controller/seam_witnesses.go b/controller/cmd/controller/seam_witnesses.go new file mode 100644 index 0000000..b1b38cd --- /dev/null +++ b/controller/cmd/controller/seam_witnesses.go @@ -0,0 +1,22 @@ +package main + +import "gitea.dooplex.hu/admin/felhom-controller/internal/quiesce" + +// COMPILE-TIME WITNESSES for OPTIONAL interfaces satisfied by a RUNTIME type assertion. +// +// Moved here from a _test.go file (R-88 Part 2) on purpose: a witness in a test fires on `go test` +// and `go vet`, but NOT on `go build` alone. The failure it guards against — a signature change that +// silently breaks an interface nobody checks at compile time — is exactly the kind that gets pushed +// by a build-only step. +// +// THE INCIDENT THIS PREVENTS, which already happened once: when `TieredBackend.DueFor` gained a +// return value during R-88 Part 2, `quiesceBackend` stopped satisfying the interface and the whole +// repo still BUILT AND VETTED CLEAN, because `resolveDueTiers` only ever asserts it at runtime +// (`l.backend.(TieredBackend)`). A failed assertion silently falls back to the untargeted +// single-tier path — so every box would have quietly lost R-82's multi-tier backups, with no error +// anywhere. It was caught by accident, not by the toolchain. +// +// THIS DOES NOT MAKE THE INTERFACE REQUIRED. Optionality is deliberate: it is what lets a new +// controller meet an old agent, and what `resolveDueTiers` degrades through on purpose. The witness +// pins the IMPLEMENTATION, not the CONTRACT. +var _ quiesce.TieredBackend = quiesceBackend{} diff --git a/controller/cmd/controller/tieredbackend_assert_test.go b/controller/cmd/controller/tieredbackend_assert_test.go deleted file mode 100644 index 28df536..0000000 --- a/controller/cmd/controller/tieredbackend_assert_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "testing" - - "gitea.dooplex.hu/admin/felhom-controller/internal/quiesce" -) - -// R-88 Part 2 — TieredBackend is satisfied by a RUNTIME type assertion in `resolveDueTiers` -// (`l.backend.(TieredBackend)`), NOT at compile time. So when DueFor's signature changed, the whole -// repo still built and vetted clean while `quiesceBackend` silently stopped satisfying the -// interface — which would have degraded every box to the untargeted single-tier path, losing R-82's -// multi-tier backups entirely, with no error anywhere. -// -// This compile-time assertion is the only thing that catches it. It caught it during R-88 Part 2. -// Do not delete it; a runtime-asserted interface needs a compile-time witness. -func TestQuiesceBackendSatisfiesTieredBackend(t *testing.T) { - var _ quiesce.TieredBackend = quiesceBackend{} -} diff --git a/controller/internal/agentapi/features.go b/controller/internal/agentapi/features.go index cfa841f..b296b35 100644 --- a/controller/internal/agentapi/features.go +++ b/controller/internal/agentapi/features.go @@ -230,3 +230,9 @@ func classifySupportErr(err error) SupportState { } return SupportUnknown } + +// AgentVersionReporter witness. Asserted at SupportsWithSource (`p.(AgentVersionReporter)`); a failed +// assertion falls back from the version gate to the live probe. That degrade is benign — both paths +// decide correctly — but *Client is the production prober and losing the version path would silently +// turn every MinAgent floor into a probe round-trip, which is a behaviour change nobody would see. +var _ AgentVersionReporter = (*Client)(nil)