v0.314.0: a restore puts the setup gate back as it was (R-1057); a test build moves with the floor (R-1053); Prowlarr links itself (R-1058); the box claims Plex by address (R-1060)
gates / gates (push) Successful in 47s

MinAgent 0.155.0 (unchanged). Tests red-proved by mutation (A: 10, B: 7, C: 1, C2: 2).

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-15 14:34:33 +02:00
parent e209a8d724
commit 17863fe8bc
22 changed files with 762 additions and 21 deletions
@@ -1,6 +1,8 @@
package selfupdate
import (
"log"
"strings"
"testing"
)
@@ -38,6 +40,7 @@ func TestMaybeAutoUpdate_AtFloor_NoAction(t *testing.T) {
u.SetFloor("0.87.0") // current == floor
u.MaybeAutoUpdate()
waitDone(t, u) // v0.314.0: the update runs in the background; read only after it could have run
if pulled {
t.Error("must NOT pull when at/above floor (latest>current is the customer's button, not the floor)")
@@ -57,6 +60,7 @@ func TestMaybeAutoUpdate_NoFloor_Inert(t *testing.T) {
u.SetFloor("") // explicitly none
u.MaybeAutoUpdate()
waitDone(t, u) // v0.314.0: the update runs in the background; read only after it could have run
if pulled || len(agent.swapCalls()) != 0 {
t.Errorf("no floor must be inert; pulled=%v swaps=%d", pulled, len(agent.swapCalls()))
@@ -74,6 +78,7 @@ func TestMaybeAutoUpdate_FloorAboveLatest_NoChase(t *testing.T) {
u.SetFloor("0.88.0") // floor > latest available
u.MaybeAutoUpdate()
waitDone(t, u) // v0.314.0: the update runs in the background; read only after it could have run
if pulled {
t.Error("must NOT pull when the floor exceeds the latest available tag")
@@ -143,6 +148,7 @@ func TestMaybeAutoUpdate_DevCurrent_NoAction(t *testing.T) {
u.SetFloor("0.87.0")
u.MaybeAutoUpdate()
waitDone(t, u) // v0.314.0: the update runs in the background; read only after it could have run
if pulled || len(agent.swapCalls()) != 0 {
t.Errorf("dev build must not auto-update; pulled=%v swaps=%d", pulled, len(agent.swapCalls()))
@@ -158,8 +164,74 @@ func TestMaybeAutoUpdate_NoAgent_NoAction(t *testing.T) {
u.SetFloor("0.87.0")
u.MaybeAutoUpdate()
waitDone(t, u) // v0.314.0: the update runs in the background; read only after it could have run
if pulled {
t.Error("must not pull when no agent is wired")
}
}
// R-1053 (v0.314.0) — a box on a TEST BUILD of the floor's version is below the floor and is moved to it.
// COMPANION RED-PROOF: parse the current version with ParseVersion again in MaybeAutoUpdate → nothing is pulled.
func TestR1053_ATestBuildIsMovedToTheFloor(t *testing.T) {
agent := &fakeAgent{}
u := newTestUpdater(t, "0.313.0-rc.4", agent)
u.queryFn = func() (string, error) { return "0.313.0", nil }
var pulled string
u.pullFn = func(img string) error { pulled = img; return nil }
u.SetFloor("0.313.0")
u.MaybeAutoUpdate()
waitDone(t, u)
if want := imageBase + ":0.313.0"; pulled != want || len(agent.swapCalls()) != 1 {
t.Fatalf("a box on rc.4 must be moved to the floor 0.313.0: pulled %q, swaps %v", pulled, agent.swapCalls())
}
}
// A test build of a NEWER version is above the floor: nothing happens (the floor never moves a box down).
// COMPANION RED-PROOF: skip the at/above check for any pre-release in MaybeAutoUpdate → it is pulled and this fails.
func TestR1053_ATestBuildAboveTheFloorStays(t *testing.T) {
agent := &fakeAgent{}
u := newTestUpdater(t, "0.314.0-rc.1", agent)
u.queryFn = func() (string, error) { return "0.313.0", nil }
pulled := false
u.pullFn = func(string) error { pulled = true; return nil }
u.SetFloor("0.313.0")
u.MaybeAutoUpdate()
waitDone(t, u) // the update runs in the background — read the result only after it could have run
if pulled || len(agent.swapCalls()) != 0 {
t.Fatal("0.314.0-rc.1 is above the floor 0.313.0 — it must stay")
}
}
// A version the floor cannot compare is said at WARN, once per (version, floor).
// COMPANION RED-PROOF: log the line at DEBUG again (or on every call) → the count is 0 (or 2).
func TestR1053_AnUnreadableVersionWarnsOncePerChange(t *testing.T) {
var buf strings.Builder
u := newTestUpdater(t, "dev", &fakeAgent{})
u.logger = log.New(&buf, "", 0)
u.queryFn = func() (string, error) { return "0.313.0", nil }
u.SetFloor("0.313.0")
u.MaybeAutoUpdate()
waitDone(t, u) // v0.314.0: the update runs in the background; read only after it could have run
u.MaybeAutoUpdate()
waitDone(t, u) // v0.314.0: the update runs in the background; read only after it could have run
if n := strings.Count(buf.String(), "[WARN] [selfupdate] auto-update: this box runs \"dev\""); n != 1 {
t.Fatalf("want exactly one WARN for the same version and floor, got %d:\n%s", n, buf.String())
}
u.SetFloor("0.314.0")
u.MaybeAutoUpdate()
waitDone(t, u) // v0.314.0: the update runs in the background; read only after it could have run
if n := strings.Count(buf.String(), "[WARN]"); n != 2 {
t.Fatalf("a new floor must warn again, got %d WARN lines", n)
}
}
// The household's button never offers a test build: the highest tag picked from the registry skips every pre-release.
// COMPANION RED-PROOF: parse tags with ParseVersionPre in decodeHighestTag → 0.314.0-rc.1 is offered and this fails.
func TestR1053_TheButtonNeverOffersATestBuild(t *testing.T) {
u := newTestUpdater(t, "0.312.0", &fakeAgent{})
got, err := u.decodeHighestTag(strings.NewReader(`{"name":"x","tags":["0.312.0","0.313.0","0.314.0-rc.1","latest"]}`))
if err != nil || got != "0.313.0" {
t.Fatalf("offered %q (%v), want 0.313.0", got, err)
}
}
+12 -2
View File
@@ -70,6 +70,8 @@ type Updater struct {
// persisted UpdateState guards across restarts). Auto-target is ALWAYS the floor, never latest.
floor string
lastAutoFloorAttempt string
// lastUnparseableWarn (v0.314.0, R-1053): the (version|floor) last warned about, so the WARN is once per change.
lastUnparseableWarn string
// Seams (default to the real implementations; overridden in tests to avoid network/docker).
queryFn func() (string, error) // resolve latest registry tag (default u.queryRegistry)
@@ -624,10 +626,18 @@ func (u *Updater) MaybeAutoUpdate() {
u.dbg("maybeAutoUpdate: an update is already running — skip")
return
}
curVer, err := ParseVersion(u.currentVer)
// R-1053 (v0.314.0): a test build (0.313.0-rc.4) is BELOW its release, so the floor moves it. A version that still
// cannot be read is said at WARN once per (version, floor) — it used to be a DEBUG line nobody saw while Tester 1
// sat on rc.4 below the floor.
curVer, err := util.ParseVersionPre(u.currentVer)
if err != nil {
key := u.currentVer + "|" + floor
warn := u.lastUnparseableWarn != key
u.lastUnparseableWarn = key
u.mu.Unlock()
u.dbg("maybeAutoUpdate: current %q not parseable (dev?) — skip", u.currentVer)
if warn {
u.logger.Printf("[WARN] [selfupdate] auto-update: this box runs %q, which is not a version the floor can compare — the floor %s will NOT move it", u.currentVer, floor)
}
return
}
floorVer, err := ParseVersion(floor)