package selfupdate import ( "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() 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() 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() 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() 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() if pulled { t.Error("must not pull when no agent is wired") } }