162 lines
6.5 KiB
Go
162 lines
6.5 KiB
Go
//go:build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
snapshot "github.com/novox/mesh-controller/internal/facts"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
"github.com/novox/mesh-host/rootsearch"
|
|
)
|
|
|
|
func TestTheSelfCheckSaysAnAgentAccountThatCanBecomeRoot(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
for _, n := range []string{"anchor", "laptop"} {
|
|
if _, err := inv.NodeByName(ctx, n); err != nil {
|
|
if _, err := inv.AddNode(ctx, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := inv.SetAccount(ctx, n, "ops", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := inv.SetAgentAccount(ctx, "anchor", "agent", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d := &doctor{open: open}
|
|
found, err := probeAgentAccounts(ctx, d)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found = onlyMachine(found, "anchor")
|
|
if len(found) != 1 || found[0].Machine != "anchor" || found[0].Severity != conditions.Urgent ||
|
|
!strings.Contains(found[0].Said, "not judged") {
|
|
t.Fatalf("a named agent account with no verdict: %+v", found)
|
|
}
|
|
w := plainWordings[kindAgentCanBecomeRoot](conditions.Observation{Kind: kindAgentCanBecomeRoot, Machine: "anchor"})
|
|
if w.Headline == "" || w.Needs == "" || w.Resolved == "" {
|
|
t.Errorf("the condition has no plain words: %+v", w)
|
|
}
|
|
|
|
healthy := inventory.ResourceHealth{Module: "claude-code", Resource: "claude-code.agent-account",
|
|
Kind: link.KindAccount, Target: "agent", State: link.StateHealthy, Root: link.RootNever, Account: "agent"}
|
|
if _, err := inv.RecordHealth(ctx, inventory.NodeHealth{Node: "anchor", Contract: link.RootContract,
|
|
SaidAt: time.Now(), HeardAt: time.Now(), Resources: []inventory.ResourceHealth{healthy}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if found, err = probeAgentAccounts(ctx, d); err != nil || len(onlyMachine(found, "anchor")) != 0 {
|
|
t.Fatalf("a judged agent account still fails: %+v %v", found, err)
|
|
}
|
|
if named, confined, why, err := agentConfined(ctx, inv, "anchor", time.Now()); err != nil || !named || !confined {
|
|
t.Fatalf("agentConfined on anchor: %v %v %q %v", named, confined, why, err)
|
|
}
|
|
if named, _, why, err := agentConfined(ctx, inv, "laptop", time.Now()); err != nil || named ||
|
|
!strings.Contains(why, "operator account") {
|
|
t.Fatalf("agentConfined on a machine naming none: %v %q %v", named, why, err)
|
|
}
|
|
}
|
|
|
|
func TestTheFactsCarryTheAgentAccountAsAPseudonym(t *testing.T) {
|
|
open, _ := aMeshWithSecrets(t)
|
|
ctx := t.Context()
|
|
if err := open.inventory.SetAccount(ctx, "anchor", "keeper", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := open.inventory.SetAgentAccount(ctx, "anchor", "warden", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := gatherFacts(ctx, open, "2.11.17")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := f.Encode()
|
|
if strings.Contains(string(body), "warden") {
|
|
t.Error("the agent account's name is in the snapshot")
|
|
}
|
|
m, ok := f.Machine(snapshot.Pseudonym("machine", "anchor"))
|
|
if !ok || m.AgentAccount != snapshot.Pseudonym("account", "warden") || m.Account == m.AgentAccount {
|
|
t.Fatalf("the anchor's agent account reads as %q (operator %q)", m.AgentAccount, m.Account)
|
|
}
|
|
}
|
|
|
|
func TestASearchStillRunningAfterARestartIsNotUrgent(t *testing.T) {
|
|
if searchQuietFor != rootsearch.Quiet {
|
|
t.Fatalf("the quiet is %s and the node-engine's %s: they are one value", searchQuietFor, rootsearch.Quiet)
|
|
}
|
|
// A daily search that finishes within the quiet never leaves the account unjudged between two of them.
|
|
if rootsearch.FreshFor < rootsearch.Every+rootsearch.Quiet {
|
|
t.Fatalf("a complete search judges for %s, less than a day's search (%s) and the quiet (%s)",
|
|
rootsearch.FreshFor, rootsearch.Every, rootsearch.Quiet)
|
|
}
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
if _, err := inv.NodeByName(ctx, "anchor"); err != nil {
|
|
if _, err := inv.AddNode(ctx, "anchor"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := inv.SetAgentAccount(ctx, "anchor", "agent", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d := &doctor{open: open}
|
|
say := func(state, reason string) []conditions.Observation {
|
|
t.Helper()
|
|
// The engine's since is always now: it was just restarted.
|
|
v := inventory.ResourceHealth{Module: "claude-code", Resource: "claude-code.agent-account",
|
|
Kind: link.KindAccount, Target: "agent", State: state, Reason: reason, Root: link.RootNever,
|
|
Account: "agent", Since: time.Now()}
|
|
if _, err := inv.RecordHealth(ctx, inventory.NodeHealth{Node: "anchor", Contract: link.RootContract,
|
|
SaidAt: time.Now(), HeardAt: time.Now(), Resources: []inventory.ResourceHealth{v}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found, err := probeAgentAccounts(ctx, d)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return onlyMachine(found, "anchor")
|
|
}
|
|
running := link.ReasonRootPending + ": the search for setuid programs, started at 19:21, has not finished yet"
|
|
healthy := func() {
|
|
t.Helper()
|
|
if found := say(link.StateHealthy, ""); len(found) != 0 {
|
|
t.Fatalf("a healthy verdict raised: %+v", found)
|
|
}
|
|
}
|
|
if found := say(link.StateUnknown, running); len(found) != 0 {
|
|
t.Fatalf("a search first seen now was raised: %+v", found)
|
|
}
|
|
if _, confined, why, _ := agentConfined(ctx, inv, "anchor", time.Now()); confined || !strings.Contains(why, "not judged") {
|
|
t.Fatalf("an account whose search runs was read as confined: %q", why)
|
|
}
|
|
// The engine restarted again and again, each statement's own since fresh: the controller's clock runs on.
|
|
if _, err := inv.RootSearchPending(ctx, "anchor", time.Now().Add(-searchQuietFor-time.Minute)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
healthy() // a complete verdict forgets when the waiting began …
|
|
if _, err := inv.RootSearchPending(ctx, "anchor", time.Now().Add(-searchQuietFor-time.Minute)); err != nil {
|
|
t.Fatal(err) // … and this restart loop began past the bound
|
|
}
|
|
if found := say(link.StateUnknown, running); len(found) != 1 || found[0].Severity != conditions.Urgent {
|
|
t.Fatalf("a search pending past the bound, by the controller's clock, was not urgent: %+v", found)
|
|
}
|
|
healthy()
|
|
incomplete := rootsearch.ReasonIncomplete + ": the search for setuid programs did not finish (last tried at " +
|
|
"19:23: timeout); it is tried again later"
|
|
if found := say(link.StateUnknown, incomplete); len(found) != 1 || found[0].Severity != conditions.Urgent {
|
|
t.Fatalf("a search that did not finish was not urgent: %+v", found)
|
|
}
|
|
if found := say(link.StateUnhealthy, link.ReasonRoot+": in the group docker; "+running); len(found) != 1 ||
|
|
found[0].Severity != conditions.Urgent {
|
|
t.Fatalf("a way to root found while the search runs was not urgent: %+v", found)
|
|
}
|
|
}
|