hub v0.45.0: floor-UI separation + effective-floor source + per-box MinAgent conditional floor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||
)
|
||||
|
||||
// seedReport writes a latest report carrying a controller version for customerID.
|
||||
func seedReport(t *testing.T, st *store.Store, customerID, ctrlVer string) {
|
||||
t.Helper()
|
||||
if err := st.SaveReport(customerID, []byte(`{"controller_version":"`+ctrlVer+`"}`)); err != nil {
|
||||
t.Fatalf("SaveReport(%s): %v", customerID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestManifestSave_DoesNotTouchFloor is Part C's central guarantee: saving the Day-0 artifact
|
||||
// manifest must NOT write hub_settings.min_controller_version (the publish-train footgun). Companion
|
||||
// red-proof: add a SetGlobalMinControllerVersion call into handleSetArtifacts → the floor changes →
|
||||
// this fails.
|
||||
func TestManifestSave_DoesNotTouchFloor(t *testing.T) {
|
||||
s, st := newTestServer(t)
|
||||
st.SetDefaultMinControllerVersion("0.87.0")
|
||||
if err := st.SetGlobalMinControllerVersion("0.113.0"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := st.ResolveGlobalFloor()
|
||||
|
||||
form := url.Values{"agent_version": {"0.82.0"}, "golden_version": {"0.115.0"}}
|
||||
// No Gitea client in the test → resolveArtifactSHA takes the manual path; provide valid shas.
|
||||
form.Set("agent_sha256", strings.Repeat("a", 64))
|
||||
form.Set("golden_sha256", strings.Repeat("b", 64))
|
||||
r := httptest.NewRequest(http.MethodPost, "/configuration/artifacts", strings.NewReader(form.Encode()))
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
s.handleSetArtifacts(w, r)
|
||||
|
||||
if m := st.GetArtifactManifest(); m.AgentVersion != "0.82.0" || m.GoldenVersion != "0.115.0" {
|
||||
t.Fatalf("manifest not saved: %+v", m)
|
||||
}
|
||||
after := st.ResolveGlobalFloor()
|
||||
if after.Effective != before.Effective || after.Source != before.Source || after.DBValue != before.DBValue {
|
||||
t.Errorf("manifest save MUST NOT change the floor: before=%+v after=%+v", before, after)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGlobalFloorImpact_Count: the confirm-dialog probe counts boxes below a proposed floor,
|
||||
// honoring per-customer overrides (an overridden box is governed by its own floor, not the proposed
|
||||
// global). Companion red-proof: drop the override exclusion → the overridden box is miscounted.
|
||||
func TestGlobalFloorImpact_Count(t *testing.T) {
|
||||
s, st := newTestServer(t)
|
||||
// Three reporting boxes.
|
||||
seedReport(t, st, "below-a", "0.110.0") // below a 0.113 proposal
|
||||
seedReport(t, st, "below-b", "0.112.0") // below
|
||||
seedReport(t, st, "current", "0.113.0") // at the proposal (not below)
|
||||
seedReport(t, st, "overridden", "0.90.0") // below the global BUT has its own override at 0.90.0
|
||||
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "overridden", RetrievalPassword: "x", APIKey: "y"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.SetMinControllerVersion("overridden", "0.90.0"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/configuration/global-floor/impact?v=0.113.0", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.handleGlobalFloorImpact(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("impact: %d", w.Code)
|
||||
}
|
||||
var resp struct {
|
||||
Version string `json:"version"`
|
||||
Valid bool `json:"valid"`
|
||||
Below int `json:"below"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !resp.Valid || resp.Version != "0.113.0" {
|
||||
t.Fatalf("resp = %+v", resp)
|
||||
}
|
||||
// below-a + below-b = 2; current is AT the floor; overridden is governed by its own 0.90.0.
|
||||
if resp.Below != 2 {
|
||||
t.Errorf("below-floor count = %d, want 2 (overridden box excluded, at-floor excluded)", resp.Below)
|
||||
}
|
||||
|
||||
// Invalid version → valid:false, below:0.
|
||||
req2 := httptest.NewRequest(http.MethodGet, "/configuration/global-floor/impact?v=notaversion", nil)
|
||||
w2 := httptest.NewRecorder()
|
||||
s.handleGlobalFloorImpact(w2, req2)
|
||||
var resp2 struct {
|
||||
Valid bool `json:"valid"`
|
||||
Below int `json:"below"`
|
||||
}
|
||||
_ = json.Unmarshal(w2.Body.Bytes(), &resp2)
|
||||
if resp2.Valid || resp2.Below != 0 {
|
||||
t.Errorf("invalid version → valid=false below=0, got %+v", resp2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigurationPage_RendersFloorSource: the effective-floor source line renders both the
|
||||
// DB-wins and env-fallback states through the production template.
|
||||
func TestConfigurationPage_RendersFloorSource(t *testing.T) {
|
||||
render := func(setup func(st *store.Store)) string {
|
||||
t.Helper()
|
||||
s, st := newTestServer(t)
|
||||
setup(st)
|
||||
req := httptest.NewRequest(http.MethodGet, "/configuration", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.handleConfiguration(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("configuration page: %d", w.Code)
|
||||
}
|
||||
return w.Body.String()
|
||||
}
|
||||
|
||||
dbWins := render(func(st *store.Store) {
|
||||
st.SetDefaultMinControllerVersion("0.87.0")
|
||||
_ = st.SetGlobalMinControllerVersion("0.113.0")
|
||||
})
|
||||
if !strings.Contains(dbWins, "DB (hub_settings)") || !strings.Contains(dbWins, "v0.113.0") {
|
||||
t.Errorf("db-wins source line missing:\n%s", excerpt(dbWins))
|
||||
}
|
||||
if !strings.Contains(dbWins, "env fallback would be") || !strings.Contains(dbWins, "v0.87.0") {
|
||||
t.Errorf("db-wins must also surface the env fallback value:\n%s", excerpt(dbWins))
|
||||
}
|
||||
|
||||
envOnly := render(func(st *store.Store) { st.SetDefaultMinControllerVersion("0.87.0") })
|
||||
if !strings.Contains(envOnly, "env fallback (DEFAULT_MIN_CONTROLLER_VERSION)") {
|
||||
t.Errorf("env-fallback source line missing:\n%s", excerpt(envOnly))
|
||||
}
|
||||
// The type-to-confirm wiring must be present (no bare submit button).
|
||||
if !strings.Contains(envOnly, "confirmGlobalFloor()") || !strings.Contains(envOnly, "global-floor/impact") {
|
||||
t.Errorf("type-to-confirm dialog wiring missing")
|
||||
}
|
||||
}
|
||||
|
||||
func excerpt(s string) string {
|
||||
if i := strings.Index(s, "Managed updates"); i >= 0 {
|
||||
end := i + 900
|
||||
if end > len(s) {
|
||||
end = len(s)
|
||||
}
|
||||
return s[i:end]
|
||||
}
|
||||
if len(s) > 600 {
|
||||
return s[:600]
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user