package web import ( "bytes" "log" "net/http" "net/http/httptest" "net/url" "regexp" "strings" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // Agent-only vouch (`09` §3 decision 250 items 3–4, hub v0.158.0). The golden is baked weekly while the floor carries // controller releases, so mid-week the vouched golden is behind the fleet. The R-120 gate guards a golden SELECTION: // an unchanged golden (same version, same sha) is not a selection and must not block an agent-only save; a changed // golden is judged exactly as before. Both tests drive handleSetArtifacts — the only UI writer of the manifest. func postArtifacts(t *testing.T, s *Server, form url.Values) *httptest.ResponseRecorder { t.Helper() 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) return w } // The mid-week situation: golden 0.330.0 vouched on Sunday, the floor has since carried the fleet to 0.335.0, and the // operator vouches a new agent. RED-PROOF (REPORT, 2026-10-17 night): with the `!goldenUnchanged` condition removed // from the gate, this fails: `agent-only vouch refused: Location="/configuration?flash=golden_behind_fleet"`. func TestVouchAgentOnlyWithStaleGoldenIsSaved(t *testing.T) { s, st := newTestServer(t) seedReport(t, st, "demo", "0.335.0") golden := strings.Repeat("b", 64) if err := st.SetArtifactManifest(store.ArtifactManifest{ AgentVersion: "0.159.0", AgentSHA256: strings.Repeat("a", 64), GoldenVersion: "0.330.0", GoldenSHA256: golden, MinAgent: "0.150.0", }); err != nil { t.Fatal(err) } w := postArtifacts(t, s, url.Values{ "agent_version": {"0.160.0"}, "agent_sha256": {strings.Repeat("c", 64)}, "golden_version": {"0.330.0"}, "golden_sha256": {golden}, "min_agent": {"0.150.0"}, }) if loc := w.Header().Get("Location"); !strings.Contains(loc, "flash=artifacts_set") { t.Fatalf("agent-only vouch refused: Location=%q, want flash=artifacts_set", loc) } m := st.GetArtifactManifest() if m.AgentVersion != "0.160.0" || m.AgentSHA256 != strings.Repeat("c", 64) { t.Fatalf("agent not saved: agent=%s sha=%s…", m.AgentVersion, m.AgentSHA256[:4]) } if m.GoldenVersion != "0.330.0" || m.GoldenSHA256 != golden || m.MinAgent != "0.150.0" { t.Fatalf("golden/MinAgent moved on an agent-only save: %+v", m) } } // A golden CHANGED to an older one is still refused, even though the agent changes in the same save — and nothing is // written. Also the same version under a different sha (a re-push) is a changed golden. func TestVouchGoldenChangedToOlderIsStillRefused(t *testing.T) { s, st := newTestServer(t) seedReport(t, st, "demo", "0.335.0") cur := store.ArtifactManifest{ AgentVersion: "0.159.0", AgentSHA256: strings.Repeat("a", 64), GoldenVersion: "0.330.0", GoldenSHA256: strings.Repeat("b", 64), } if err := st.SetArtifactManifest(cur); err != nil { t.Fatal(err) } for name, form := range map[string]url.Values{ "older version": {"agent_version": {"0.160.0"}, "agent_sha256": {strings.Repeat("c", 64)}, "golden_version": {"0.329.0"}, "golden_sha256": {strings.Repeat("d", 64)}}, "same version, other sha": {"agent_version": {"0.160.0"}, "agent_sha256": {strings.Repeat("c", 64)}, "golden_version": {"0.330.0"}, "golden_sha256": {strings.Repeat("e", 64)}}, } { w := postArtifacts(t, s, form) if loc := w.Header().Get("Location"); !strings.Contains(loc, "golden_behind_fleet") { t.Errorf("%s: Location=%q, want the golden_behind_fleet refusal", name, loc) } if m := st.GetArtifactManifest(); m != cur { t.Errorf("%s: refused but the manifest changed: %+v", name, m) } } } // R-1346: a vouch save names the time of each leg in one line, so the next slow save says where its time went. // RED-PROOF: drop the timing line → FAILS. func TestVouchSaveLogsItsTiming(t *testing.T) { s, st := newTestServer(t) var logs bytes.Buffer s.logger = log.New(&logs, "", 0) seedReport(t, st, "demo", "0.335.0") w := postArtifacts(t, s, url.Values{"agent_version": {"0.160.0"}, "agent_sha256": {strings.Repeat("c", 64)}}) if loc := w.Header().Get("Location"); !strings.Contains(loc, "flash=artifacts_set") { t.Fatalf("vouch refused: %q", loc) } if !regexp.MustCompile(`artifact vouch timing \(R-1346\): registry probes \d+ms, sha lookups \d+ms, bundle sha \d+ms, fleet check \+ save \d+ms, total \d+ms`).MatchString(logs.String()) { t.Fatalf("no timing line:\n%s", logs.String()) } }