controller v0.240.0: seven defects from the any-tier proof and the first nightly rotation
gates / gates (push) Successful in 13s
gates / gates (push) Successful in 13s
R-486 (P1): removing an app with its backups KEPT keeps its Tier-2 record, so the second-drive restore is no longer refused over an intact mirror. R-484: postgis/pgvector/timescaledb images are Postgres (logical dumps). R-485: the backup card sizes the recovery unit and the mirror(s). R-480: a held update's sentence leaves the card once the hold is lifted. R-477: the update's off-site lookup is one snapshots call, no stats. R-478: a copy older than this install's deploy does not count. R-474: "delete backups" deletes the unit, the mirror(s) and the prefs. Tests and red-proofs per row; evidence in felhom.eu documentation/audits/v0240-2026-09-13/ and nightly-2026-09-13-adventurelog/.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// R-480 — the failed update's sentence goes when the hold it announced is lifted, or the app is gone.
|
||||
|
||||
func heldFailedUpdate(t *testing.T) (*Manager, *fakeGuards) {
|
||||
t.Helper()
|
||||
m, _, g, _ := newSlice4Manager(t)
|
||||
m.updateHealthFn = func(context.Context, string, time.Duration) (bool, string) { return false, "crash loop" }
|
||||
if err := m.StartGuardedUpdate("nextcloud"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st := waitUpdateDone(t, m, "nextcloud")
|
||||
if st.UpdatePhase != UpdatePhaseFailed || st.UpdateError != "HELD-SENTENCE" {
|
||||
t.Fatalf("fixture: a held failure must show the hold sentence while held; phase=%q err=%q", st.UpdatePhase, st.UpdateError)
|
||||
}
|
||||
return m, g
|
||||
}
|
||||
|
||||
func TestR480_TheFailureSentenceGoesWhenItsHoldIsLifted(t *testing.T) {
|
||||
m, g := heldFailedUpdate(t)
|
||||
g.mu.Lock()
|
||||
g.held, g.holdWhy = false, "" // a successful restore cleared the hold
|
||||
g.mu.Unlock()
|
||||
if st, _ := m.GetStack("nextcloud"); st.UpdateError != "" || st.UpdatePhase != "" {
|
||||
t.Errorf("GetStack after the hold is lifted: phase=%q err=%q — the card would say a running app is stopped", st.UpdatePhase, st.UpdateError)
|
||||
}
|
||||
for _, st := range m.GetStacks() {
|
||||
if st.Name == "nextcloud" && st.UpdateError != "" {
|
||||
t.Errorf("GetStacks after the hold is lifted still carries %q", st.UpdateError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestR480_ARemovedAppShowsNoUpdateOutcome(t *testing.T) {
|
||||
m, _ := heldFailedUpdate(t)
|
||||
m.mu.Lock()
|
||||
m.stacks["nextcloud"].Deployed = false
|
||||
m.mu.Unlock()
|
||||
if st, _ := m.GetStack("nextcloud"); st.UpdateError != "" {
|
||||
t.Errorf("a removed app must not carry the held update's sentence, got %q", st.UpdateError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestR480_AFailureThatHeldNothingKeepsItsSentence(t *testing.T) {
|
||||
m, _, _, c := newSlice4Manager(t)
|
||||
c.fail["pull"] = errors.New("manifest unknown")
|
||||
if err := m.StartGuardedUpdate("nextcloud"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st := waitUpdateDone(t, m, "nextcloud")
|
||||
if st.UpdateError != MsgUpdatePullFailed || st.UpdatePhase != UpdatePhaseFailed {
|
||||
t.Errorf("a pull failure held nothing and its sentence is still true; phase=%q err=%q", st.UpdatePhase, st.UpdateError)
|
||||
}
|
||||
}
|
||||
|
||||
// R-478 — a copy older than THIS install's deploy does not carry the update.
|
||||
|
||||
func runWithDeployedAt(t *testing.T, deployedAgo time.Duration) *fakeGuards {
|
||||
t.Helper()
|
||||
m, _, g, _ := newSlice4Manager(t)
|
||||
m.mu.Lock()
|
||||
if m.stacks["nextcloud"].AppConfig == nil {
|
||||
m.stacks["nextcloud"].AppConfig = &AppConfig{}
|
||||
}
|
||||
m.stacks["nextcloud"].AppConfig.DeployedAt = slice4T0.Add(-deployedAgo).Format(time.RFC3339)
|
||||
m.mu.Unlock()
|
||||
g.points = []UpdateRestorePoint{{Tier: UpdateTierLocal, ProvenAt: slice4T0.Add(-2 * time.Hour)}}
|
||||
g.pointsAfterBackup = []UpdateRestorePoint{{Tier: UpdateTierLocal, ProvenAt: slice4T0.Add(-time.Minute)}}
|
||||
if err := m.StartGuardedUpdate("nextcloud"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st := waitUpdateDone(t, m, "nextcloud"); st.UpdatePhase != UpdatePhaseDone {
|
||||
t.Fatalf("phase=%q err=%q", st.UpdatePhase, st.UpdateError)
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func TestR478_ACopyOlderThanThisInstallDoesNotCount(t *testing.T) {
|
||||
if g := runWithDeployedAt(t, 10*time.Minute); !hasGuardCall(g, "BackupNow") {
|
||||
t.Errorf("a 2-hour-old unit under a 10-minute-old install belongs to the previous install — back up first; calls=%v", g.callList())
|
||||
}
|
||||
// Control: the same unit under a 3-hour-old install is this install's own copy.
|
||||
if g := runWithDeployedAt(t, 3*time.Hour); hasGuardCall(g, "BackupNow") {
|
||||
t.Errorf("control: a copy newer than the deploy must carry the update with no backup; calls=%v", g.callList())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user