package backup import ( "io" "log" "testing" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) // R-1347: a restore that lifts an UPDATE hold must also forget the stack manager's in-memory „last update ended held" // flag. Left set, the crash-loop watcher does not watch the app and the night leg skips it until the controller restarts // (D64's sibling: the operator's clear_hold already forgets it, R-1343; the restore did not). // // The consequence on the real flag — ObserveUnhealthy and legCandidate — is TestR1347_RestoreLiftsTheWatchersBlindSpot // (cmd/controller), and the seam's wiring TestR1347_UpdateHeldForgetterIsWiredAtStartup. // // RED-PROOF (2026-11-02): seen failing before the seam existed — the build failed on SetUpdateHeldForgetter; with the // call dropped from clearUpdateHoldAfterRestore it fails on „the flag was not forgotten". func TestR1347_ARestoreThatLiftsAnUpdateHoldForgetsTheHeldFlag(t *testing.T) { sett := slice4Settings(t) m := &Manager{logger: log.New(io.Discard, "", 0), settings: sett} forgot := map[string]int{} m.SetUpdateHeldForgetter(func(s string) bool { forgot[s]++; return true }) _ = m.HoldAfterFailedUpdate("nextcloud", time.Now(), time.Now(), UpdateTierLocal) m.clearUpdateHoldAfterRestore("nextcloud") if _, held := sett.GetRestoreHold("nextcloud"); held { t.Fatal("the update hold must be lifted") } if forgot["nextcloud"] != 1 { t.Fatalf("the restore lifted the update hold, but the flag was not forgotten (calls: %v)", forgot) } // A RESTORE hold (R-379) stays, so nothing about it is forgotten. _ = sett.SetRestoreHold(settings.RestoreHold{Stack: "rst", At: "2026-08-22T14:00:00Z"}) m.clearUpdateHoldAfterRestore("rst") if _, held := sett.GetRestoreHold("rst"); !held || forgot["rst"] != 0 { t.Fatalf("a restore hold is not lifted by a restore (held %v, forget calls %d)", held, forgot["rst"]) } // No hold at all: nothing to forget (a plain restore of a healthy app). m.clearUpdateHoldAfterRestore("plain") if forgot["plain"] != 0 { t.Fatalf("no hold was lifted, yet the flag was touched %d time(s)", forgot["plain"]) } // nil seam: no crash (the pre-R-1347 behaviour). m2 := &Manager{logger: log.New(io.Discard, "", 0), settings: sett} _ = m2.HoldAfterFailedUpdate("other", time.Now(), time.Now(), UpdateTierLocal) m2.clearUpdateHoldAfterRestore("other") }