package stacks import ( "io" "log" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/config" ) // D64 = B / R-1343: when the operator clears a hold, the ONE held flag kept outside the hold store — „the last update // ended held" — goes too. Left set, the crash-loop observer and the nightly update leg kept skipping an app whose hold // was gone, until the controller restarted. // RED-PROOF (2026-10-29): with ForgetUpdateHeld's body reduced to `return false`, this fails on „a held update must be forgotten". func TestD64_ForgetUpdateHeld(t *testing.T) { m := &Manager{cfg: &config.Config{}, logger: log.New(io.Discard, "", 0), stacks: map[string]*Stack{ "kimai": {Name: "kimai", Deployed: true}, "running": {Name: "running", Deployed: true, Updating: true}, }} m.finishUpdateKey("kimai", UpdatePhaseFailed, UpdateErrorKeyHeld, "a kimai leállítva marad") m.markUpdateHeld("kimai") m.markUpdateHeld("running") if !m.ForgetUpdateHeld("kimai") { t.Fatal("a held update must be forgotten") } st, _ := m.GetStack("kimai") if st.updateHeld || st.UpdateErrorKey != "" || st.UpdateError != "" || st.UpdatePhase != "" { t.Fatalf("held state left: held=%v key=%q err=%q phase=%q", st.updateHeld, st.UpdateErrorKey, st.UpdateError, st.UpdatePhase) } // The two readers of the flag (ObserveUnhealthy's skip, unhealthy.go; legCandidate's LegSkipHeld, unattended.go) // read the stored stack — not called here, because ObserveUnhealthy reaches docker (R-650). m.mu.RLock() skipped := m.stacks["kimai"].updateHeld || m.stacks["kimai"].UpdateErrorKey == UpdateErrorKeyHeld m.mu.RUnlock() if skipped { t.Fatal("the observer and the update leg still skip kimai") } // A running update is never touched; an app with nothing held reports false. if m.ForgetUpdateHeld("running") { t.Fatal("a running update must not be touched") } if m.ForgetUpdateHeld("kimai") || m.ForgetUpdateHeld("nosuch") { t.Fatal("nothing held → false") } }