package quiesce import ( "context" "log" "strings" "testing" ) // R-88 Part 2 (controller half) — a nil age is no longer self-licensing. // // The valve fires only on a POSITIVE claim of "never backed up". These tests assert BEHAVIOUR (were // stacks stopped?), never a log line — a controller that logs the right thing and then does the // wrong thing must fail here. // stateBackend is a tierBackend whose DueFor also reports an age_state wire string. type stateBackend struct { *tierBackend wire map[string]string // target → age_state as sent by the agent ("" = legacy) } func (b *stateBackend) DueFor(ctx context.Context, target string) (bool, *int64, string, error) { due, age, _, err := b.tierBackend.DueFor(ctx, target) return due, age, b.wire[target], err } func stateLoop(t *testing.T, wire map[string]string, st *fakeStacks, logTo *strings.Builder) *Loop { t.Helper() be := newTierBackend() be.tiers = []BackupTier{{Target: "local"}} be.dueSet["local"] = true be.phases["local"] = []string{phaseDone} sb := &stateBackend{tierBackend: be, wire: wire} l := windowLoop(t, sb, st, "02:30", atBudapest(12, 0)) // 12:00 — firmly OUTSIDE [04:30, 08:30) if logTo != nil { l.logger = log.New(logTo, "", 0) } return l } // ── SCENARIO A — UNKNOWN does not bypass the window ────────────────────────────────────────── // // COMPANION RED-PROOF (observed): make valveLicensed return true for AgeStateUnknown (the pre-fix // behaviour, where any nil age fired the valve) and this fails with // // "R-88 Part 2: an UNKNOWN age bypassed the backup window and stopped 1 stack(s) — an unreadable // storage must not masquerade as a first-ever backup" // // Restored. func TestAgeState_UnknownDoesNotBypassTheWindow(t *testing.T) { st := &fakeStacks{running: []string{"bookstack"}} l := stateLoop(t, map[string]string{"local": "unknown"}, st, nil) if err := l.runOnce(context.Background()); err != nil { t.Fatal(err) } if got := len(st.stoppedNames()); got != 0 { t.Fatalf("R-88 Part 2: an UNKNOWN age bypassed the backup window and stopped %d stack(s) — "+ "an unreadable storage must not masquerade as a first-ever backup", got) } } // ── SCENARIO B — ABSENT still runs outside the window ──────────────────────────────────────── // // B is what makes A safe. An implementation that never licensed the valve would pass A and silently // starve every new box. // // COMPANION RED-PROOF (observed): drop AgeStateAbsent from valveLicensed (keeping only legacy) and // this fails with // // "a genuine first-ever backup (absent) must RUN outside the window; 0 stack(s) stopped — the // safety valve was lost and a new box would starve" // // Restored. func TestAgeState_AbsentStillRunsOutsideTheWindow(t *testing.T) { st := &fakeStacks{running: []string{"bookstack"}} l := stateLoop(t, map[string]string{"local": "absent"}, st, nil) if err := l.runOnce(context.Background()); err != nil { t.Fatal(err) } if len(st.stoppedNames()) == 0 { t.Fatal("a genuine first-ever backup (absent) must RUN outside the window; 0 stack(s) stopped — " + "the safety valve was lost and a new box would starve") } } // ── SCENARIO C — old agent, new controller: TODAY'S behaviour exactly ──────────────────────── // // Asserts BEHAVIOUR, not the degrade log line: a controller that logs the degrade and then defers // would pass a log-only assertion while silently changing behaviour on every un-upgraded box. // // COMPANION RED-PROOF (observed): drop AgeStateLegacy from valveLicensed (treating a missing field // as unknown — the "safer-looking" choice) and this fails with // // "C: a pre-v0.105.0 agent must behave EXACTLY as before — nil age fires the valve. 0 stack(s) // stopped; an un-upgraded box just silently stopped backing up outside its window" // // Restored. func TestAgeState_LegacyAgentKeepsTodaysBehaviour(t *testing.T) { st := &fakeStacks{running: []string{"bookstack"}} var logbuf strings.Builder l := stateLoop(t, map[string]string{"local": ""}, st, &logbuf) // NO field on the wire if err := l.runOnce(context.Background()); err != nil { t.Fatal(err) } if len(st.stoppedNames()) == 0 { t.Fatal("C: a pre-v0.105.0 agent must behave EXACTLY as before — nil age fires the valve. " + "0 stack(s) stopped; an un-upgraded box just silently stopped backing up outside its window") } // ...and the degrade must be VISIBLE, or a fleet drifts without anyone knowing. if !strings.Contains(logbuf.String(), "age_state") { t.Fatalf("the legacy degrade must be logged once; log:\n%s", logbuf.String()) } } // The degrade is logged ONCE, not every poll. func TestAgeState_LegacyDegradeLoggedOnce(t *testing.T) { st := &fakeStacks{running: []string{"bookstack"}} var logbuf strings.Builder l := stateLoop(t, map[string]string{"local": ""}, st, &logbuf) for i := 0; i < 3; i++ { if err := l.runOnce(context.Background()); err != nil { t.Fatal(err) } } if n := strings.Count(logbuf.String(), "pre-v0.105.0"); n != 1 { t.Fatalf("the legacy degrade must be logged ONCE per process, got %d", n) } } // An unrecognised FUTURE state maps to legacy, not to unknown — a newer agent inventing a fourth // value must not accidentally acquire "unknown" semantics from a controller that never heard of it. func TestAgeState_UnrecognisedWireValueIsLegacy(t *testing.T) { for _, wire := range []string{"", "known", "absent", "unknown", "quantum", "TRUE", "0"} { got := ageStateFromWire(wire) switch wire { case "known", "absent", "unknown": if string(got) != wire { t.Errorf("%q must map to itself, got %q", wire, got) } default: if got != AgeStateLegacy { t.Errorf("%q must map to LEGACY (fail toward known behaviour), got %q", wire, got) } } } } // valveLicensed as a truth table — the contract, independent of the loop. func TestAgeState_ValveLicenceTable(t *testing.T) { age := int64(3600) cases := []struct { name string t dueTier want bool }{ {"absent licenses", dueTier{state: AgeStateAbsent}, true}, {"legacy licenses (un-upgraded agent keeps old behaviour)", dueTier{state: AgeStateLegacy}, true}, {"unknown does NOT license", dueTier{state: AgeStateUnknown}, false}, {"known with a real age needs no licence", dueTier{state: AgeStateKnown, ageSecs: &age}, false}, } for _, c := range cases { if got := valveLicensed([]dueTier{c.t}); got != c.want { t.Errorf("%s: valveLicensed = %v, want %v", c.name, got, c.want) } } // One unknown tier must not be licensed by a sibling that is merely known-with-age. if valveLicensed([]dueTier{{state: AgeStateUnknown}, {state: AgeStateKnown, ageSecs: &age}}) { t.Error("a known sibling must not license an unknown tier's valve") } }