17863fe8bc
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
238 lines
8.9 KiB
Go
238 lines
8.9 KiB
Go
package selfupdate
|
|
|
|
import (
|
|
"log"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Scenario A — below floor auto-updates to the FLOOR (not latest), reusing the Phase 1 swap.
|
|
// This is the companion RED-PROOF: it must FAIL if MaybeAutoUpdate stops honoring the floor.
|
|
func TestMaybeAutoUpdate_BelowFloor_UpdatesToFloor(t *testing.T) {
|
|
agent := &fakeAgent{}
|
|
u := newTestUpdater(t, "0.86.0", agent)
|
|
// Registry latest is 0.87.0 (so the floor is pullable). Auto-target must be the FLOOR, not latest.
|
|
u.queryFn = func() (string, error) { return "0.87.0", nil }
|
|
var pulled string
|
|
u.pullFn = func(img string) error { pulled = img; return nil }
|
|
|
|
u.SetFloor("0.87.0")
|
|
u.MaybeAutoUpdate()
|
|
waitDone(t, u)
|
|
|
|
want := imageBase + ":0.87.0"
|
|
if pulled != want {
|
|
t.Errorf("pulled %q, want %q (the floor)", pulled, want)
|
|
}
|
|
calls := agent.swapCalls()
|
|
if len(calls) != 1 || calls[0] != want {
|
|
t.Errorf("agent swap calls = %v, want exactly [%q]", calls, want)
|
|
}
|
|
}
|
|
|
|
// Scenario B — at/above floor: no pull, no swap. Must NOT update even though latest > current.
|
|
func TestMaybeAutoUpdate_AtFloor_NoAction(t *testing.T) {
|
|
agent := &fakeAgent{}
|
|
u := newTestUpdater(t, "0.87.0", agent)
|
|
u.queryFn = func() (string, error) { return "0.99.0", nil } // latest far ahead — irrelevant
|
|
pulled := false
|
|
u.pullFn = func(string) error { pulled = true; return nil }
|
|
|
|
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)")
|
|
}
|
|
if n := len(agent.swapCalls()); n != 0 {
|
|
t.Errorf("agent called %d times, want 0 (at floor)", n)
|
|
}
|
|
}
|
|
|
|
// Scenario D — no floor set: inert.
|
|
func TestMaybeAutoUpdate_NoFloor_Inert(t *testing.T) {
|
|
agent := &fakeAgent{}
|
|
u := newTestUpdater(t, "0.86.0", agent)
|
|
u.queryFn = func() (string, error) { return "0.99.0", nil }
|
|
pulled := false
|
|
u.pullFn = func(string) error { pulled = true; return nil }
|
|
|
|
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()))
|
|
}
|
|
}
|
|
|
|
// Rule 2 — floor exceeds the latest available tag (operator misconfig): do nothing (don't chase a
|
|
// non-existent image).
|
|
func TestMaybeAutoUpdate_FloorAboveLatest_NoChase(t *testing.T) {
|
|
agent := &fakeAgent{}
|
|
u := newTestUpdater(t, "0.86.0", agent)
|
|
u.queryFn = func() (string, error) { return "0.87.0", nil } // latest available
|
|
pulled := false
|
|
u.pullFn = func(string) error { pulled = true; return nil }
|
|
|
|
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")
|
|
}
|
|
if n := len(agent.swapCalls()); n != 0 {
|
|
t.Errorf("agent called %d times, want 0 (floor>latest)", n)
|
|
}
|
|
}
|
|
|
|
// Rule 4 — no flapping/storm: repeated reconciles for the same below-floor condition trigger exactly
|
|
// one auto-update.
|
|
func TestMaybeAutoUpdate_NoFlap(t *testing.T) {
|
|
agent := &fakeAgent{}
|
|
u := newTestUpdater(t, "0.86.0", agent)
|
|
u.queryFn = func() (string, error) { return "0.87.0", nil }
|
|
u.pullFn = func(string) error { return nil }
|
|
|
|
u.SetFloor("0.87.0")
|
|
u.MaybeAutoUpdate()
|
|
waitDone(t, u)
|
|
// Simulate further report cycles with the same floor + still-below current.
|
|
u.SetFloor("0.87.0")
|
|
u.MaybeAutoUpdate()
|
|
u.SetFloor("0.87.0")
|
|
u.MaybeAutoUpdate()
|
|
waitDone(t, u)
|
|
|
|
if n := len(agent.swapCalls()); n != 1 {
|
|
t.Errorf("agent swap calls = %d, want exactly 1 (no flapping)", n)
|
|
}
|
|
}
|
|
|
|
// Scenario C / E — precedence is reflected by whatever floor the notifier sets: a raised floor (e.g.
|
|
// an operator override or a global bump) is honored on the next reconcile.
|
|
func TestMaybeAutoUpdate_HonorsRaisedFloor(t *testing.T) {
|
|
agent := &fakeAgent{}
|
|
u := newTestUpdater(t, "0.86.0", agent)
|
|
u.queryFn = func() (string, error) { return "0.87.0", nil }
|
|
var pulled []string
|
|
u.pullFn = func(img string) error { pulled = append(pulled, img); return nil }
|
|
|
|
// First the global floor equals current → no action.
|
|
u.SetFloor("0.86.0")
|
|
u.MaybeAutoUpdate()
|
|
if len(agent.swapCalls()) != 0 {
|
|
t.Fatalf("floor==current should not update")
|
|
}
|
|
// Operator raises the floor (override or global bump) → now auto-updates to the new floor.
|
|
u.SetFloor("0.87.0")
|
|
u.MaybeAutoUpdate()
|
|
waitDone(t, u)
|
|
|
|
want := imageBase + ":0.87.0"
|
|
calls := agent.swapCalls()
|
|
if len(calls) != 1 || calls[0] != want {
|
|
t.Errorf("agent swap calls = %v, want [%q] after raised floor", calls, want)
|
|
}
|
|
}
|
|
|
|
// A dev build never auto-updates (can't compare versions).
|
|
func TestMaybeAutoUpdate_DevCurrent_NoAction(t *testing.T) {
|
|
agent := &fakeAgent{}
|
|
u := newTestUpdater(t, "dev", agent)
|
|
u.queryFn = func() (string, error) { return "0.87.0", nil }
|
|
pulled := false
|
|
u.pullFn = func(string) error { pulled = true; return nil }
|
|
|
|
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()))
|
|
}
|
|
}
|
|
|
|
// No agent (un-provisioned guest) → no auto-update.
|
|
func TestMaybeAutoUpdate_NoAgent_NoAction(t *testing.T) {
|
|
u := newTestUpdater(t, "0.86.0", nil)
|
|
u.queryFn = func() (string, error) { return "0.87.0", nil }
|
|
pulled := false
|
|
u.pullFn = func(string) error { pulled = true; return nil }
|
|
|
|
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)
|
|
}
|
|
}
|