package web import ( "errors" "net/http" "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/agentapi" ) // ── R-546 — do not send the household to a ceremony the box cannot run yet ──────────────────────── // // Measured 2026-09-16/17 (chaos night Phase 0): for ~17 minutes after the bind the box has no PBS // storage, the agent's preflight is red, and the ceremony refuses. Meanwhile the R-543 bar is on every // page urging the household there. Readiness is the AGENT'S OWN `ok` — every blocking preflight item // (pbs_storage_id, dr_tier, age_binary, hub_upload, sudo_grant), never a copy of one of them. const escrowNotReadyMarker = `id="escrow-not-ready"` func readinessAgent(ok bool) *fakeEscrowAgent { order := []string{} pf := agentapi.EscrowPreflightResponse{OK: ok} if !ok { pf.Items = []agentapi.EscrowPreflightItem{{ID: "pbs_storage_id", OK: false, Detail: "escrow.pbs_storage_id not configured"}} } return &fakeEscrowAgent{order: &order, version: "0.132.0", pf: pf} } // RED-PROOF: remove the readiness check from escrowBannerVisible → the bar renders on a box that // cannot run the ceremony → "the bar urges a ceremony the box cannot run yet". func TestR546_NotReadyBoxHoldsTheBar(t *testing.T) { s := escrowServer(t, "pending") a := readinessAgent(false) s.escrowAgentFn = func() (escrowAgent, error) { return a, nil } body := getPage(t, s, "/dashboard").Body.String() if strings.Contains(body, escrowBarSentence) { t.Fatal("R-546: the bar urges a ceremony the box cannot run yet (agent preflight is red)") } } // Control: once the agent says ready, the R-543 bar is back. func TestR546_ReadyBoxShowsTheBar(t *testing.T) { s := escrowServer(t, "pending") a := readinessAgent(true) s.escrowAgentFn = func() (escrowAgent, error) { return a, nil } if body := getPage(t, s, "/dashboard").Body.String(); !strings.Contains(body, escrowBarSentence) { t.Fatal("a READY, paused box no longer shows the R-543 bar") } } // Fail loud: readiness UNKNOWN (agent unreachable) keeps the reminder — silence is not a safe default. func TestR546_UnknownReadinessStillAsks(t *testing.T) { s := escrowServer(t, "pending") s.escrowAgentFn = func() (escrowAgent, error) { return nil, errors.New("agent unreachable") } if body := getPage(t, s, "/dashboard").Body.String(); !strings.Contains(body, escrowBarSentence) { t.Fatal("an unknown readiness hid the reminder — an unreachable agent must not silence R-543") } } // The page itself: a calm waiting card, no start form, and none of the agent's raw diagnostics. // RED-PROOF: never set EscrowNotReady → the checklist + start form render → "start form offered". func TestR546_EscrowPageWaitsWhenNotReady(t *testing.T) { s := escrowServer(t, "pending") a := readinessAgent(false) s.escrowAgentFn = func() (escrowAgent, error) { return a, nil } rec := getPage(t, s, "/backup/escrow") if rec.Code != 200 { t.Fatalf("GET /backup/escrow = %d: %s", rec.Code, rec.Body.String()) } body := rec.Body.String() if !strings.Contains(body, escrowNotReadyMarker) || !strings.Contains(body, "A doboz m") { t.Fatal("R-546: the escrow page does not tell the household the box is still preparing") } if strings.Contains(body, `id="start-form"`) { t.Fatal("R-546: the start form is offered on a box whose ceremony would refuse") } // The exact texts chaos night met: the preflight detail, and the ceremony stderr ("…escrow-create // requires -storage …"). NOT a bare "-storage": the menu carries id="nav-group-storage". for _, raw := range []string{"pbs_storage_id not configured", "requires -storage"} { if strings.Contains(body, raw) { t.Fatalf("R-546: the agent's raw diagnostic %q reached the household's page", raw) } } if strings.Contains(body, "zzzz-not-present") { t.Fatal("negative control matched") } } // The direct path — the one that actually produced the stderr in chaos night: POST /api/escrow/start // on a not-ready box is refused BEFORE anything is staged or started, in Hungarian. // RED-PROOF: remove the readiness gate from escrowStartAPIHandler → 200 and "stage,start" run. func TestR546_StartRefusedWhenNotReady(t *testing.T) { h := newEscrowWizardHarness(t) h.configureOffbox(t, "pending") h.agent.pf = agentapi.EscrowPreflightResponse{OK: false, Items: []agentapi.EscrowPreflightItem{{ID: "pbs_storage_id", OK: false}}} w := postStart(t, h.s, wizardPassword) if w.Code != http.StatusConflict { t.Fatalf("start on a not-ready box = %d (%s), want 409", w.Code, w.Body.String()) } for _, op := range h.order { if op == "stage" || op == "start" { t.Fatalf("a not-ready box still ran %q (order %v) — the refusal must come first", op, h.order) } } if !strings.Contains(w.Body.String(), "A doboz m") { t.Fatalf("the refusal is not the Hungarian waiting sentence: %s", w.Body.String()) } }