package main // The page that shows what the dry-run rule would stop (uipolicy.go), driven // end to end through the real mux. // // NOT tagged !windows, for the same reason ui_test.go is not: this page is for // the machine where the bastion has no console, which is the Windows desktop. // // Every test below was MUTATION-CHECKED — the code or the template it covers was // broken, the test confirmed red, and it was restored. The mutation is named in // each comment. import ( "encoding/json" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strings" "testing" "time" ) func policyGet(t *testing.T, h http.Handler, path, cookie string) string { t.Helper() r := uiReq(http.MethodGet, path) r.Header.Set("Cookie", cookie) w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusOK { t.Fatalf("%s = %d: %s", path, w.Code, w.Body.String()) } return w.Body.String() } // writeLedger puts a census on disk without running one, for the cases that are // about a record made days ago. func writeLedger(t *testing.T, cfg string, l policyLedger) { t.Helper() if err := os.MkdirAll(cfg, 0o700); err != nil { t.Fatal(err) } b, err := json.MarshalIndent(l, "", " ") if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(cfg, "policy-dryrun.json"), b, 0o600); err != nil { t.Fatal(err) } } // ── the headline ──────────────────────────────────────────────────────────── // The page's subject is what the rule would take away, so that is what it leads // with. "N automated programs are running" is the agents page's question and is // already answered one page over. // // MUTATION: render `{{.Observed}}` where the template says `{{.WouldStop}}`. // RESULT: red — the page announced 3 of 3 on a machine where the rule proposed // stopping one thing. func TestThePolicyPageLeadsWithWhatWouldBeStopped(t *testing.T) { candidate := policyCandidateObs("claude-code", "/usr/local/bin/claude", 4242) mine := policyCandidateObs("aider", "/usr/local/bin/aider", 4243) mine.Origin = originInteractive app := policyCandidateObs("claude-desktop", "/Applications/Claude.app/Contents/MacOS/claude", 51) app.Kind = kindApp policyTestBox(t, candidate, mine, app) u := uiTestServer(t) h := u.handler() cookie := uiSignedIn(t, u) body := policyGet(t, h, "/policy", cookie) if !strings.Contains(body, "1 of 3 would be stopped by this rule.") { t.Fatalf("the page does not lead with what would be stopped: %s", firstLines(body)) } if !strings.Contains(body, "/usr/local/bin/claude") { t.Error("the page does not name the one it would stop") } // Vouch for it, and the headline has to move. if _, err := addSanction(candidate, sanctionByLocalUI); err != nil { t.Fatal(err) } body = policyGet(t, h, "/fragments/policy", cookie) if !strings.Contains(body, "Nothing running here would be stopped by this rule.") { t.Errorf("the headline did not move after a mark: %s", firstLines(body)) } } // A verdict this machine could not reach is counted apart and said out loud. It // is the state a person has to do something about, and folding it into "left // alone" is how a blind spot becomes an all-clear. // // MUTATION: drop the `{{if .CannotJudge}}` block from policy.html. // RESULT: red — a machine that could not read a single origin reported one // number, and it was the reassuring one. func TestWhatCannotBeJudgedIsCountedApart(t *testing.T) { blind := policyCandidateObs("goose", "/usr/local/bin/goose", 4244) blind.Origin = originUnknown policyTestBox(t, blind) u := uiTestServer(t) body := policyGet(t, u.handler(), "/policy", uiSignedIn(t, u)) if !strings.Contains(body, "1 of 1 cannot be judged at all.") { t.Fatalf("the page does not count what it could not judge: %s", firstLines(body)) } if !strings.Contains(body, "cannot judge") { t.Error("the row does not carry the verdict in words") } } // ── the misleading directions this page must not take ─────────────────────── // A read that FAILED is not a machine with nothing on it (ui_approvals_flow_test.go: // 113-151). Every verdict needs the registry, so a registry that will not load // means no count on this page means anything. // // MUTATION: drop the `{{if not .RegistryError}}` guard around the count block. // RESULT: red — "0 of 1 would be stopped by this rule" about a machine whose // record of what is vouched for it had just failed to read. func TestAnUnreadableRegistryIsAnAlarmAndNeverACountHere(t *testing.T) { cfg := policyTestBox(t, policyCandidateObs("claude-code", "/usr/local/bin/claude", 4242)) // A census with something in it, so BOTH counts on this page are reachable // before the registry is broken — otherwise the second half of this test // would be asserting about a block that never renders. policyCensusTick() if err := os.WriteFile(filepath.Join(cfg, "sanctions.json"), []byte("{not json at all"), 0o600); err != nil { t.Fatal(err) } u := uiTestServer(t) body := policyGet(t, u.handler(), "/policy", uiSignedIn(t, u)) if !strings.Contains(body, "cannot read its own record") { t.Fatalf("the page did not say the record is unreadable: %s", firstLines(body)) } if strings.Contains(body, "would be stopped by this rule.") { t.Error("the page counted candidates on a machine whose record of marks it could not read") } // The census count comes out of the same record, so it cannot be printed // either. Found by review rather than by a test going red: "Vouched for // since: 0" is the same reassuring sentence in the second half of the page. // // MUTATION: render the ordinary census headline regardless of RegistryError. // RESULT: red — "Vouched for since: 0" about a machine that could not read // what had been vouched for. if strings.Contains(body, "Vouched for since:") { t.Error("the census counted mistakes out of a record it could not read") } if !strings.Contains(body, "How many were mistakes is not known.") { t.Errorf("the census does not say the count is out of reach: %s", firstLines(body)) } if !strings.Contains(body, "/usr/local/bin/claude") { t.Error("the past candidates stopped being listed at all — they are still " + "what would have been stopped; it is what became of them that is unknown") } if !strings.Contains(body, "It is not reporting that there is nothing to judge.") { t.Error("the page does not distinguish 'I cannot judge' from 'there is nothing to judge'") } if !strings.Contains(body, "cannot judge") { t.Error("the row does not say its verdict is out of reach") } } // A build with no enumerator must say so rather than render an empty list. // agentScanSupported is a compile-time constant (agents_other.go:13), so the // branch is driven through the template directly — which is where the sentence // either exists or does not. // // MUTATION: delete the `{{if .Blind}}` block from policy.html. // RESULT: red — a machine that cannot list its own processes rendered as one // with nothing to stop. func TestABlindBuildNeverRendersAsAQuietMachineHere(t *testing.T) { policyTestBox(t) u := uiTestServer(t) w := httptest.NewRecorder() u.renderPolicy(w, "policy", uiPolicyView{Version: clientVersion, Blind: true}) body := w.Body.String() if !strings.Contains(body, "cannot watch itself") { t.Fatalf("a build with no enumerator does not say so: %s", firstLines(body)) } for _, banned := range []string{"would be stopped by this rule.", "Nothing automated is running on this machine."} { if strings.Contains(body, banned) { t.Errorf("a blind machine printed %q — README's whole quiet-versus-blind "+ "distinction is that these are different states", banned) } } } // ── this page cannot stop anything ────────────────────────────────────────── // The unit is the dry run (step 3 of the design) and acting (step 4) is a // separate decision. A page with a Stop button on it would have made that // decision on everybody's behalf. // // MUTATION: add a `
…
` // to the row block in policy.html. // RESULT: red — three times: the form, the button and the post target. func TestThePolicyPageOffersNoControlAtAll(t *testing.T) { policyTestBox(t, policyCandidateObs("claude-code", "/usr/local/bin/claude", 4242)) u := uiTestServer(t) h := u.handler() cookie := uiSignedIn(t, u) body := policyGet(t, h, "/policy", cookie) for _, banned := range []string{"hx-post", "