230 lines
10 KiB
Go
230 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// THE PAGES, RENDERED. walkfail_test.go proves the view models carry the
|
|
// failure; these prove the templates do not draw the tick under the banner.
|
|
// Both summaries were gated on Blind and the registry error only, so a walk
|
|
// failure with a working enumerator rendered the red banner AND "This machine
|
|
// looked; it did not find anything" beneath it.
|
|
|
|
func walkFailedBox(t *testing.T) {
|
|
t.Helper()
|
|
uiTestConfigDir(t)
|
|
prev := observedAgents
|
|
t.Cleanup(func() { observedAgents = prev })
|
|
observedAgents = func() ([]agentObs, error) { return nil, errors.New("read /proc: permission denied") }
|
|
}
|
|
|
|
func TestTheAgentsPageDoesNotSayItLookedWhenTheWalkFailed(t *testing.T) {
|
|
walkFailedBox(t)
|
|
u := uiTestServer(t)
|
|
h := u.handler()
|
|
cookie := uiSignedIn(t, u)
|
|
for _, path := range []string{"/agents", "/fragments/agents"} {
|
|
w := httptest.NewRecorder()
|
|
r := uiReq(http.MethodGet, path)
|
|
r.Header.Set("Cookie", cookie)
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("%s = %d — both routes exist (uisanction.go) and a renamed one must fail here, not pass silently", path, w.Code)
|
|
}
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "could not read its processes") || !strings.Contains(body, "permission denied") {
|
|
t.Fatalf("%s: the failed walk is not bannered: %s", path, firstLines(body))
|
|
}
|
|
if strings.Contains(body, "This machine looked") || strings.Contains(body, "Nothing automated is running") {
|
|
t.Fatalf("%s: the page drew the tick under the walk banner: %s", path, firstLines(body))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestThePolicyPageDoesNotSayItLookedWhenTheWalkFailed(t *testing.T) {
|
|
walkFailedBox(t)
|
|
u := uiTestServer(t)
|
|
h := u.handler()
|
|
cookie := uiSignedIn(t, u)
|
|
body := policyGet(t, h, "/policy", cookie)
|
|
if !strings.Contains(body, "could not read its processes") {
|
|
t.Fatalf("the failed walk is not bannered: %s", firstLines(body))
|
|
}
|
|
for _, tick := range []string{"Nothing automated is running", "would be stopped by this rule", "Nothing running here would be stopped"} {
|
|
if strings.Contains(body, tick) {
|
|
t.Fatalf("the policy page drew the tick %q under the walk banner: %s", tick, firstLines(body))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMarkingDuringAFailedWalkNamesTheWalk(t *testing.T) {
|
|
walkFailedBox(t)
|
|
u := uiTestServer(t)
|
|
h := u.handler()
|
|
cookie := uiSignedIn(t, u)
|
|
r := httptest.NewRequest(http.MethodPost, "/agents/mark", strings.NewReader("id=deadbeef"))
|
|
r.Host = "127.0.0.1:" + uiTestPort
|
|
r.Header.Set("Origin", "http://127.0.0.1:"+uiTestPort)
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
r.Header.Set("Cookie", cookie)
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
body := w.Body.String()
|
|
if w.Code == http.StatusSeeOther || w.Code == http.StatusFound {
|
|
// The flash rides the redirected page.
|
|
body = agentsGet(t, h, w.Header().Get("Location"), cookie).Body.String()
|
|
}
|
|
if !strings.Contains(body, "could not read its processes") {
|
|
t.Fatalf("the mark did not name the failed walk (status %d): %s", w.Code, firstLines(body))
|
|
}
|
|
if strings.Contains(body, "not running on this machine any more") {
|
|
t.Fatalf("the mark blamed the wrong cause: %s", firstLines(body))
|
|
}
|
|
}
|
|
|
|
// THE ROWS THAT NAME A PROGRAM. The summary tick was gated in round two; these
|
|
// two lists said "not running right now" about a NAMED program on a render
|
|
// where nothing looked — one row below the banner. Both take the same shape
|
|
// their unreadable-registry guard already has.
|
|
|
|
func TestThePolicyPastRowsCannotSayNotRunningWhenTheWalkFailed(t *testing.T) {
|
|
cfg := policyTestBox(t)
|
|
obs := policyCandidateObs("claude-code", "/usr/local/bin/claude", 4242)
|
|
id := sanctionIdentityOf(obs)
|
|
now := time.Now()
|
|
writeLedger(t, cfg, policyLedger{Since: now.Add(-time.Hour), LastScan: now,
|
|
Candidates: map[string]policyCandidate{id.fingerprint(): {
|
|
ID: id.fingerprint(), sanctionIdentity: id, FirstSeen: now.Add(-time.Hour), LastSeen: now}}})
|
|
observedAgents = func() ([]agentObs, error) { return nil, errors.New("read /proc: permission denied") }
|
|
u := uiTestServer(t)
|
|
body := policyGet(t, u.handler(), "/policy", uiSignedIn(t, u))
|
|
if strings.Contains(body, policyPastNotSeen) {
|
|
t.Fatalf("a failed walk called a candidate %q: %s", policyPastNotSeen, firstLines(body))
|
|
}
|
|
if !strings.Contains(body, policyPastUnwalked) {
|
|
t.Fatalf("the past row does not say the walk failed: %s", firstLines(body))
|
|
}
|
|
if strings.Contains(body, "Still a candidate right now: 0") {
|
|
t.Fatalf("a failed walk counted zero candidates still running: %s", firstLines(body))
|
|
}
|
|
}
|
|
|
|
func TestTheAgentsPageDoesNotListMarksAsNotRunningWhenTheWalkFailed(t *testing.T) {
|
|
mine := anObservedAgent("claude-code", "/usr/local/bin/claude", 4242)
|
|
agentsTestBox(t, mine)
|
|
if _, err := addSanction(mine, sanctionByLocalUI); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
observedAgents = func() ([]agentObs, error) { return nil, errors.New("read /proc: permission denied") }
|
|
u := uiTestServer(t)
|
|
h := u.handler()
|
|
body := agentsGet(t, h, "/agents", uiSignedIn(t, u)).Body.String()
|
|
if strings.Contains(body, "Marked as yours, but not running right now") || strings.Contains(body, "/usr/local/bin/claude") {
|
|
t.Fatalf("a failed walk listed a marked program as not running: %s", firstLines(body))
|
|
}
|
|
if !strings.Contains(body, "could not read its processes") {
|
|
t.Fatalf("the banner is missing: %s", firstLines(body))
|
|
}
|
|
}
|
|
|
|
// THE CENSUS BLOCK, one line above the rows. "Vouched for since" is read from
|
|
// the registry and stays real on a failed walk; "still a candidate right now"
|
|
// is read from the walk and is not known. The first version of the row switch
|
|
// let the walk take the vouch with it — the count the whole page waits on
|
|
// went to 0 and the box turned green.
|
|
func TestTheCensusBlockKeepsTheVouchAndWithholdsStillOnAFailedWalk(t *testing.T) {
|
|
cfg := policyTestBox(t)
|
|
obs := policyCandidateObs("claude-code", "/usr/local/bin/claude", 4242)
|
|
id := sanctionIdentityOf(obs)
|
|
now := time.Now()
|
|
// Two candidates: one the user vouched for, one nobody did — the second is
|
|
// the row that must wear no "nothing behind it" edge on a failed walk.
|
|
other := policyCandidateObs("aider", "/usr/local/bin/aider", 4243)
|
|
oid := sanctionIdentityOf(other)
|
|
writeLedger(t, cfg, policyLedger{Since: now.Add(-time.Hour), LastScan: now,
|
|
Candidates: map[string]policyCandidate{
|
|
id.fingerprint(): {ID: id.fingerprint(), sanctionIdentity: id, FirstSeen: now.Add(-time.Hour), LastSeen: now},
|
|
oid.fingerprint(): {ID: oid.fingerprint(), sanctionIdentity: oid, FirstSeen: now.Add(-time.Hour), LastSeen: now},
|
|
}})
|
|
if _, err := addSanction(obs, sanctionByLocalUI); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
observedAgents = func() ([]agentObs, error) { return nil, errors.New("read /proc: permission denied") }
|
|
u := uiTestServer(t)
|
|
body := policyGet(t, u.handler(), "/policy", uiSignedIn(t, u))
|
|
if !strings.Contains(body, "Vouched for since: 1") {
|
|
t.Fatalf("a failed walk zeroed the vouched count: %s", firstLines(body))
|
|
}
|
|
if strings.Contains(body, "Still a candidate right now: 0") {
|
|
t.Fatalf("a failed walk counted zero candidates still running: %s", firstLines(body))
|
|
}
|
|
if !strings.Contains(body, "Still a candidate right now: not known") {
|
|
t.Fatalf("the census block does not say the count is unknown: %s", firstLines(body))
|
|
}
|
|
if !strings.Contains(body, `class="state warn"`) || strings.Contains(body, `class="state ok"`) {
|
|
t.Fatalf("a vouched candidate on a failed walk must keep the box amber, never green: %s", firstLines(body))
|
|
}
|
|
if strings.Contains(body, `past dormant`) {
|
|
t.Fatalf("a row nothing looked for wore the 'nothing behind it' edge: %s", firstLines(body))
|
|
}
|
|
}
|
|
|
|
// A mark that no longer counts is a fact about the record, not the walk: it is
|
|
// still said on a failed walk. Only the dormant half (which needs `seen`) is
|
|
// withheld.
|
|
func TestAnExpiredMarkIsStillSaidOnAFailedWalk(t *testing.T) {
|
|
live := anObservedAgent("claude-code", "/usr/local/bin/claude", 4242)
|
|
agentsTestBox(t, live)
|
|
d, err := configDir()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v1ID := v1Fingerprint(kindAgent, "claude-code", "/usr/local/bin/claude", "user")
|
|
registry := `{"` + v1ID + `": {"id": "` + v1ID + `", "kind": "agent",
|
|
"signature": "claude-code", "argv0": "/usr/local/bin/claude",
|
|
"user": "user", "at": "2026-09-01T10:00:00Z", "by": "local-ui"}}`
|
|
if err := os.WriteFile(filepath.Join(d, "sanctions.json"), []byte(registry), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
observedAgents = func() ([]agentObs, error) { return nil, errors.New("read /proc: permission denied") }
|
|
u := uiTestServer(t)
|
|
body := agentsGet(t, u.handler(), "/agents", uiSignedIn(t, u)).Body.String()
|
|
if !strings.Contains(body, "Marks that no longer count") {
|
|
t.Fatalf("a failed walk silenced a mark that no longer counts: %s", firstLines(body))
|
|
}
|
|
if strings.Contains(body, "Marked as yours, but not running right now") {
|
|
t.Fatalf("a failed walk listed a mark as not running: %s", firstLines(body))
|
|
}
|
|
}
|
|
|
|
// A running candidate the registry cannot judge is unknowable too, and wears
|
|
// no "nothing behind it" edge — the live-verdict arm, pre-existing before
|
|
// this PR, set the status without the flag.
|
|
func TestARunningCandidateTheRegistryCannotJudgeWearsNoDormantEdge(t *testing.T) {
|
|
obs := policyCandidateObs("claude-code", "/usr/local/bin/claude", 4242)
|
|
cfg := policyTestBox(t, obs)
|
|
id := sanctionIdentityOf(obs)
|
|
now := time.Now()
|
|
writeLedger(t, cfg, policyLedger{Since: now.Add(-time.Hour), LastScan: now,
|
|
Candidates: map[string]policyCandidate{id.fingerprint(): {
|
|
ID: id.fingerprint(), sanctionIdentity: id, FirstSeen: now.Add(-time.Hour), LastSeen: now}}})
|
|
if err := os.WriteFile(filepath.Join(cfg, "sanctions.json"), []byte("{not json"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
u := uiTestServer(t)
|
|
body := policyGet(t, u.handler(), "/policy", uiSignedIn(t, u))
|
|
if !strings.Contains(body, policyPastUnknowable) {
|
|
t.Fatalf("an unreadable registry did not make the running row unknowable: %s", firstLines(body))
|
|
}
|
|
if strings.Contains(body, "past dormant") {
|
|
t.Fatalf("a running-but-unjudgeable row wore the dormant edge: %s", firstLines(body))
|
|
}
|
|
}
|