a7f1d277b1
gates / gates (push) Successful in 7s
v0.97.0 introduced a second hold reason and left both surfaces printing the first. The freshly deployed hub logged, for the campaign box: managed floor HELD for c11: agent "0.125.0" < MinAgent 0.113.0 which is FALSE — 0.125.0 is ABOVE 0.113.0. That box is held because its floor sits above the vouched golden, not because of its agent. CLAUDE.md's corollary exactly: when a verdict changes which field it counts from, the alarm text has to change with it, or a true alarm reads as one to dismiss. Both the ACK log line and the Hosts-dashboard HeldReason now come from one ManagedFloorDecision.HoldReason(), and TestResolveManagedFloor_HoldReasonMatchesTheCause pins each reason to its cause.
232 lines
9.9 KiB
Go
232 lines
9.9 KiB
Go
package store
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// setHostAgent enrolls a host for customerID and records its agent version (the reality column).
|
|
func setHostAgent(t *testing.T, s *Store, customerID, hostID, agentVer string) {
|
|
t.Helper()
|
|
if err := s.UpsertHost(&Host{HostID: hostID, CustomerID: customerID, APIKey: hostID + "-key"}); err != nil {
|
|
t.Fatalf("UpsertHost: %v", err)
|
|
}
|
|
if agentVer != "" {
|
|
if err := s.SaveHostReport(hostID, customerID, []byte(`{}`), HostReportDenorm{AgentVersion: agentVer}); err != nil {
|
|
t.Fatalf("SaveHostReport: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ResolveManagedFloor gates the served controller floor on the box's agent meeting the golden's
|
|
// MinAgent (Part D — the hub-enforced "agent BEFORE controller floor" rule).
|
|
func TestResolveManagedFloor(t *testing.T) {
|
|
base := func(t *testing.T) *Store {
|
|
s := newTestStore(t)
|
|
_ = s.SetGlobalMinControllerVersion("0.113.0") // the controller floor
|
|
return s
|
|
}
|
|
|
|
t.Run("uncoupled release (MinAgent empty) → serve the floor, no gating", func(t *testing.T) {
|
|
s := base(t)
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "0.113.0", MinAgent: ""})
|
|
setHostAgent(t, s, "c1", "h1", "0.70.0") // ancient agent, but release is uncoupled
|
|
fd := s.ResolveManagedFloor("c1")
|
|
if fd.Held || fd.Floor != "0.113.0" {
|
|
t.Fatalf("uncoupled must serve: %+v", fd)
|
|
}
|
|
})
|
|
|
|
t.Run("agent ≥ MinAgent → serve the floor", func(t *testing.T) {
|
|
s := base(t)
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "0.113.0", MinAgent: "0.81.0"})
|
|
setHostAgent(t, s, "c1", "h1", "0.82.0")
|
|
fd := s.ResolveManagedFloor("c1")
|
|
if fd.Held || fd.Floor != "0.113.0" {
|
|
t.Fatalf("at/above MinAgent must serve: %+v", fd)
|
|
}
|
|
})
|
|
|
|
t.Run("agent < MinAgent → HELD, no floor served, flagged", func(t *testing.T) {
|
|
s := base(t)
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "0.113.0", MinAgent: "0.81.0"})
|
|
setHostAgent(t, s, "c1", "h1", "0.79.0")
|
|
fd := s.ResolveManagedFloor("c1")
|
|
if !fd.Held || fd.Floor != "" {
|
|
t.Fatalf("below MinAgent must HOLD: %+v", fd)
|
|
}
|
|
if fd.AgentVersion != "0.79.0" || fd.MinAgent != "0.81.0" {
|
|
t.Fatalf("held decision must carry the inputs: %+v", fd)
|
|
}
|
|
})
|
|
|
|
t.Run("agent_version empty (never reported) → HELD (fail-safe)", func(t *testing.T) {
|
|
s := base(t)
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "0.113.0", MinAgent: "0.81.0"})
|
|
setHostAgent(t, s, "c1", "h1", "") // enrolled, no report yet
|
|
fd := s.ResolveManagedFloor("c1")
|
|
if !fd.Held || fd.Floor != "" {
|
|
t.Fatalf("unknown agent must HOLD (never push blind): %+v", fd)
|
|
}
|
|
})
|
|
|
|
t.Run("no floor configured → nothing to gate", func(t *testing.T) {
|
|
s := newTestStore(t) // no global floor
|
|
_ = s.SetArtifactManifest(ArtifactManifest{MinAgent: "0.81.0"})
|
|
setHostAgent(t, s, "c1", "h1", "0.70.0")
|
|
fd := s.ResolveManagedFloor("c1")
|
|
if fd.Held || fd.Floor != "" {
|
|
t.Fatalf("no floor → no gate, no hold: %+v", fd)
|
|
}
|
|
})
|
|
|
|
// Fleet discriminator (the §Part-D companion): in ONE fleet, an at-MinAgent box is served while a
|
|
// below-MinAgent box is held. Companion red-proof: drop the hold branch → both get served → the
|
|
// held assertion fails.
|
|
t.Run("fleet: at-MinAgent served, below-MinAgent held", func(t *testing.T) {
|
|
s := base(t)
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "0.113.0", MinAgent: "0.81.0"})
|
|
setHostAgent(t, s, "current", "hc", "0.82.0")
|
|
setHostAgent(t, s, "old", "ho", "0.79.0")
|
|
if fd := s.ResolveManagedFloor("current"); fd.Held || fd.Floor != "0.113.0" {
|
|
t.Errorf("current box must be served: %+v", fd)
|
|
}
|
|
if fd := s.ResolveManagedFloor("old"); !fd.Held || fd.Floor != "" {
|
|
t.Errorf("old box must be held: %+v", fd)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ── R-216: the floor may not be served ABOVE the version the manifest describes ─────────────────
|
|
//
|
|
// `publish-train-rules.md` rule 3 is about the FLOOR's controller; the implementation compared
|
|
// against the GOLDEN's MinAgent. Those are the same number only while the floor sits at or below the
|
|
// golden. Measured live 2026-08-05 (CAMPAIGN-11 Phase 1): golden 0.192.0 / MinAgent 0.113.0, a
|
|
// per-customer floor of 0.200.0, a box on agent 0.120.0 — 0.120.0 ≥ 0.113.0, so the floor was served
|
|
// and the box was pushed onto a controller needing agent 0.125.0. Its customer was then told their
|
|
// correct recovery code was wrong.
|
|
//
|
|
// RED-PROOF: delete the `semver.Compare(d.Floor, d.GoldenVersion) > 0` branch from
|
|
// ResolveManagedFloor and TestResolveManagedFloor_R216_FloorAboveGolden FAILS — the box is served
|
|
// past its agent again, reproducing the campaign's measurement exactly. Demonstrated failing before
|
|
// these tests were kept.
|
|
func TestResolveManagedFloor_R216_FloorAboveGolden(t *testing.T) {
|
|
t.Run("the campaign's exact numbers → HELD, not served", func(t *testing.T) {
|
|
s := newTestStore(t)
|
|
_ = s.SetGlobalMinControllerVersion("0.200.0")
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "0.192.0", MinAgent: "0.113.0"})
|
|
setHostAgent(t, s, "c11", "c11-36d660", "0.120.0")
|
|
|
|
fd := s.ResolveManagedFloor("c11")
|
|
if !fd.Held || fd.Floor != "" {
|
|
t.Fatalf("R-216 RETURNED: a floor ABOVE the vouched golden was served, so the hub pushed a controller whose agent requirement it does not know: %+v", fd)
|
|
}
|
|
if !fd.HeldBeyondGolden {
|
|
t.Errorf("the hold must be distinguishable from the ordinary below-MinAgent hold (they need different operator text): %+v", fd)
|
|
}
|
|
if fd.GoldenVersion != "0.192.0" {
|
|
t.Errorf("the decision must carry the golden it compared against: %+v", fd)
|
|
}
|
|
})
|
|
|
|
// The hold is about the FLOOR being unknown, not about the agent — so it holds even for a box
|
|
// whose agent comfortably exceeds the manifest's MinAgent. That is the whole point: the hub cannot
|
|
// know what the served version needs.
|
|
t.Run("held even when the agent clears the manifest's MinAgent", func(t *testing.T) {
|
|
s := newTestStore(t)
|
|
_ = s.SetGlobalMinControllerVersion("0.200.0")
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "0.192.0", MinAgent: "0.113.0"})
|
|
setHostAgent(t, s, "c1", "h1", "0.125.0")
|
|
|
|
if fd := s.ResolveManagedFloor("c1"); !fd.Held {
|
|
t.Fatalf("a floor above the golden must hold regardless of the agent: %+v", fd)
|
|
}
|
|
})
|
|
|
|
// SCENARIO C — an uncoupled release is untouched. A fleet must not be frozen by a comparison that
|
|
// now over-fires: with no MinAgent vouched there is no gating at all, above the golden or not.
|
|
t.Run("uncoupled release above the golden → still served (Scenario C)", func(t *testing.T) {
|
|
s := newTestStore(t)
|
|
_ = s.SetGlobalMinControllerVersion("0.200.0")
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "0.192.0", MinAgent: ""})
|
|
setHostAgent(t, s, "c1", "h1", "0.70.0")
|
|
|
|
fd := s.ResolveManagedFloor("c1")
|
|
if fd.Held || fd.Floor != "0.200.0" {
|
|
t.Fatalf("an uncoupled release must be served exactly as before: %+v", fd)
|
|
}
|
|
})
|
|
|
|
// The ordinary arrangement — floor at or below the golden — is completely unchanged.
|
|
t.Run("floor at the golden → unchanged behaviour", func(t *testing.T) {
|
|
s := newTestStore(t)
|
|
_ = s.SetGlobalMinControllerVersion("0.192.0")
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "0.192.0", MinAgent: "0.113.0"})
|
|
setHostAgent(t, s, "c1", "h1", "0.120.0")
|
|
|
|
fd := s.ResolveManagedFloor("c1")
|
|
if fd.Held || fd.Floor != "0.192.0" {
|
|
t.Fatalf("floor == golden with a satisfied MinAgent must serve: %+v", fd)
|
|
}
|
|
})
|
|
|
|
// An unreadable golden must not gate the fleet — degrade to the pre-existing behaviour.
|
|
t.Run("unparseable golden → falls through to the agent comparison", func(t *testing.T) {
|
|
s := newTestStore(t)
|
|
_ = s.SetGlobalMinControllerVersion("0.200.0")
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "", MinAgent: "0.113.0"})
|
|
setHostAgent(t, s, "c1", "h1", "0.120.0")
|
|
|
|
fd := s.ResolveManagedFloor("c1")
|
|
if fd.Held || fd.Floor != "0.200.0" {
|
|
t.Fatalf("an unknown golden must not freeze the fleet: %+v", fd)
|
|
}
|
|
})
|
|
}
|
|
|
|
// The hold REASON must match the hold CAUSE. This is `CLAUDE.md`'s corollary, and it caught a real
|
|
// slip in this very session: v0.97.0 added a second hold reason, and for one deploy the live hub
|
|
// logged `managed floor HELD for c11: agent "0.125.0" < MinAgent 0.113.0` — a comparison that is
|
|
// FALSE, for a box held because its floor sat above the golden. A true alarm that misdescribes itself
|
|
// is one the operator learns to dismiss.
|
|
func TestResolveManagedFloor_HoldReasonMatchesTheCause(t *testing.T) {
|
|
t.Run("beyond-golden hold names the floor and the golden, not the agent", func(t *testing.T) {
|
|
s := newTestStore(t)
|
|
_ = s.SetGlobalMinControllerVersion("0.200.0")
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "0.192.0", MinAgent: "0.113.0"})
|
|
setHostAgent(t, s, "c11", "c11-36d660", "0.125.0")
|
|
|
|
reason := s.ResolveManagedFloor("c11").HoldReason()
|
|
if strings.Contains(reason, "< MinAgent") {
|
|
t.Fatalf("the beyond-golden hold claims an agent comparison that is FALSE here (0.125.0 > 0.113.0): %q", reason)
|
|
}
|
|
for _, want := range []string{"0.200.0", "0.192.0", "ABOVE"} {
|
|
if !strings.Contains(reason, want) {
|
|
t.Errorf("hold reason must name %q; got %q", want, reason)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("below-MinAgent hold still names the agent comparison", func(t *testing.T) {
|
|
s := newTestStore(t)
|
|
_ = s.SetGlobalMinControllerVersion("0.113.0")
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "0.113.0", MinAgent: "0.81.0"})
|
|
setHostAgent(t, s, "c1", "h1", "0.79.0")
|
|
|
|
reason := s.ResolveManagedFloor("c1").HoldReason()
|
|
if !strings.Contains(reason, "0.79.0") || !strings.Contains(reason, "MinAgent 0.81.0") {
|
|
t.Fatalf("the below-MinAgent hold must name the agent comparison; got %q", reason)
|
|
}
|
|
})
|
|
|
|
t.Run("not held → no reason", func(t *testing.T) {
|
|
s := newTestStore(t)
|
|
_ = s.SetGlobalMinControllerVersion("0.113.0")
|
|
_ = s.SetArtifactManifest(ArtifactManifest{GoldenVersion: "0.113.0", MinAgent: ""})
|
|
setHostAgent(t, s, "c1", "h1", "0.70.0")
|
|
if r := s.ResolveManagedFloor("c1").HoldReason(); r != "" {
|
|
t.Fatalf("a served floor must have no hold reason, got %q", r)
|
|
}
|
|
})
|
|
}
|