188 lines
10 KiB
Go
188 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
"github.com/novox/mesh-host/internal/accounts"
|
|
"github.com/novox/mesh-host/internal/link"
|
|
"github.com/novox/mesh-host/internal/liveness"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
"github.com/novox/mesh-host/internal/units"
|
|
)
|
|
|
|
// The machine's failed units in the engine's statement (novox/hq issue 315): a module's is among the
|
|
// resources, unhealthy, as that module's unit, naming it; the machine's own beside them; and an engine
|
|
// not reading them says nothing of them.
|
|
func TestTheStatementCarriesTheFailedUnits(t *testing.T) {
|
|
at := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC)
|
|
us := &units.Statement{At: at, State: units.Degraded, Failed: []units.Failed{
|
|
{Unit: "openrazer-daemon.service", Scope: units.ScopeUser, Load: "loaded", Result: "exit-code",
|
|
User: "operator", Module: "openrazer", Resource: "openrazer.daemon", Via: units.ViaPackage, Since: at, Streak: 2},
|
|
{Unit: "storage-media.mount", Scope: units.ScopeSystem, Load: "loaded", Result: "timeout", Since: at, Streak: 2},
|
|
}}
|
|
h := withUnits(healthAsReported(liveness.Statement{At: at}, nil), us)
|
|
if len(h.Resources) != 1 {
|
|
t.Fatalf("one module's unit among the resources: %+v", h.Resources)
|
|
}
|
|
r := h.Resources[0]
|
|
if r.Module != "openrazer" || r.Resource != "openrazer.daemon" || r.Kind != link.KindUnit ||
|
|
r.Target != "openrazer-daemon.service" || r.State != link.StateUnhealthy ||
|
|
!strings.Contains(r.Reason, "account's own service manager") || !strings.Contains(r.Reason, "exit-code") {
|
|
t.Fatalf("the module's failed unit: %+v", r)
|
|
}
|
|
// Whose manager it fails in, so the controller can tell it from a fault (novox/hq ADR 0254).
|
|
if r.Account != "operator" {
|
|
t.Fatalf("the unit in the account's own manager does not name the account: %q", r.Account)
|
|
}
|
|
if h.Units == nil || h.Units.State != units.Degraded || len(h.Units.Failed) != 1 ||
|
|
h.Units.Failed[0].Unit != "storage-media.mount" || h.Units.Failed[0].Result != "timeout" {
|
|
t.Fatalf("the machine's own: %+v", h.Units)
|
|
}
|
|
if h := withUnits(healthAsReported(liveness.Statement{At: at}, nil), nil); h.Units != nil {
|
|
t.Fatal("an engine not reading units said them")
|
|
}
|
|
if h := withUnits(healthAsReported(liveness.Statement{At: at}, nil), &units.Statement{}); h.Units != nil {
|
|
t.Fatal("a judge not yet given a declaration said units")
|
|
}
|
|
}
|
|
|
|
// Every resource that runs in an account's own service manager names the account, and so does the account
|
|
// a module put in a group (novox/hq ADR 0254): the controller reads a unit that cannot run before a new login
|
|
// by the two together. What the machine's own manager runs names none.
|
|
func TestTheStatementNamesTheAccountsManager(t *testing.T) {
|
|
at := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC)
|
|
st := liveness.Statement{At: at, Resources: []liveness.State{
|
|
{Resource: liveness.Resource{Module: "openrazer", ID: "openrazer.daemon", Kind: "service",
|
|
Target: "openrazer-daemon.service", Scope: declaration.ScopeUser, User: "operator"}, State: liveness.Unhealthy,
|
|
Reason: "down", Since: at},
|
|
{Resource: liveness.Resource{Module: "docker", ID: "docker.engine", Kind: "service", Target: "docker.service",
|
|
Scope: declaration.ScopeSystem}, State: liveness.Healthy, Since: at},
|
|
}}
|
|
as := &accounts.Statement{At: at, Accounts: []accounts.Verdict{{Account: accounts.Account{Module: "openrazer",
|
|
ID: "openrazer.operator", Name: "operator", Groups: []string{"openrazer"}}, State: accounts.Unhealthy,
|
|
Reason: accounts.ReasonRelogin + ": operator is in the group openrazer", Since: at}}}
|
|
h := withAccounts(healthAsReported(st, nil), as)
|
|
got := map[string]string{}
|
|
for _, r := range h.Resources {
|
|
got[r.Resource] = r.Account
|
|
}
|
|
want := map[string]string{"openrazer.daemon": "operator", "docker.engine": "", "openrazer.operator": "operator"}
|
|
for id, account := range want {
|
|
if a, said := got[id]; !said || a != account {
|
|
t.Errorf("%s names the account %q (said %v); want %q", id, a, said, account)
|
|
}
|
|
}
|
|
}
|
|
|
|
// An account declared never to become root (novox/hq ADR 0266) is said with root never, under the contract
|
|
// that tells the controller this engine judges it; an account judged for its groups alone is not.
|
|
func TestTheStatementSaysAnAccountJudgedForRoot(t *testing.T) {
|
|
at := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC)
|
|
as := &accounts.Statement{At: at, Accounts: []accounts.Verdict{
|
|
{Account: accounts.Account{Module: "claude-code", ID: "claude-code.agent", Name: "agent", Root: true},
|
|
State: accounts.Unhealthy, Reason: accounts.ReasonRoot + ": in the group docker, which grants root", Since: at},
|
|
{Account: accounts.Account{Module: "openrazer", ID: "openrazer.account", Name: "operator",
|
|
Groups: []string{"openrazer"}}, State: accounts.Healthy, Since: at},
|
|
}}
|
|
h := withAccounts(healthAsReported(liveness.Statement{At: at}, nil), as)
|
|
// At RootContract or later: an engine that states its sealing key to the vault says NodeKeyContract (ADR 0372, issue 786), one above
|
|
// VaultBoxContract (it opens the vault's box for the core's credentials, ADR 0349 §16, issue 755), one above HandedContract (design 57 P7), one above ProvisionerContract (novox/hq ADR
|
|
// 0349 §4), one above ADR 0349 §5's OwnershipContract.
|
|
if h.Contract < link.RootContract || link.RootContract != 3 || h.Contract != link.NodeKeyContract ||
|
|
link.NodeKeyContract != link.VaultBoxContract+1 || link.VaultBoxContract != link.HandedContract+1 ||
|
|
link.HandedContract != link.ProvisionerContract+1 || link.ProvisionerContract != link.OwnershipContract+1 || link.OwnershipContract != link.BusyContract+1 ||
|
|
link.BusyContract != link.OwnGroupContract+1 {
|
|
t.Fatalf("contract %d", h.Contract)
|
|
}
|
|
got := map[string]string{}
|
|
for _, r := range h.Resources {
|
|
got[r.Resource] = r.Root
|
|
}
|
|
if got["claude-code.agent"] != "never" || got["openrazer.account"] != "" {
|
|
t.Fatalf("root said: %v", got)
|
|
}
|
|
// 774 (ADR 0563 §6): the node seat's root-free passes only on the engine's own positive verdict at most 15 minutes old
|
|
// with execute not served here; a stale, missing or negative verdict, or execute served, is no pass and says why.
|
|
t.Run("this machine's root-free", func(t *testing.T) {
|
|
agent := accounts.Verdict{Account: accounts.Account{Module: "claude-code", ID: "claude-code.agent", Name: "agent", Root: true},
|
|
State: accounts.Healthy, Since: at}
|
|
free := &accounts.Statement{At: at, Accounts: []accounts.Verdict{agent}}
|
|
for _, row := range []struct {
|
|
name string
|
|
st *accounts.Statement
|
|
after time.Duration
|
|
served bool
|
|
free bool
|
|
why string
|
|
}{
|
|
{name: "a positive verdict", st: free, after: 14 * time.Minute, free: true, why: "the account agent cannot become root"},
|
|
{name: "a stale verdict", st: free, after: 16 * time.Minute, why: "more than 15 minutes ago"},
|
|
{name: "no verdict", st: nil, why: "judged no account yet"},
|
|
{name: "a way to root", st: as, why: "in the group docker, which grants root"},
|
|
{name: "no agent account", st: &accounts.Statement{At: at}, why: "agents run as the operator's account"},
|
|
{name: "execute served", st: free, served: true, why: "the login shell runs any command"},
|
|
} {
|
|
v := judgeRootFree("ace", row.st, at.Add(row.after), row.served, "the bus hears execute answered here")
|
|
if v.Free != row.free || !strings.Contains(v.Why, row.why) || v.Machine != "ace" {
|
|
t.Errorf("%s: %+v", row.name, v)
|
|
}
|
|
}
|
|
// execute is served while the holder's setting file says anything but withhold, or the bus hears it.
|
|
dir := t.TempDir()
|
|
statePath, conf := filepath.Join(dir, "state.json"), filepath.Join(dir, "tools.conf")
|
|
if err := store.Save(statePath, store.State{Resources: []store.Applied{{ID: "zsh.tools-conf", Type: "file", Target: conf}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
quiet := func() (bool, error) { return false, nil }
|
|
for _, row := range []struct {
|
|
name, setting string
|
|
heard func() (bool, error)
|
|
served bool
|
|
}{
|
|
{name: "withheld and not heard", setting: "# the mesh's\nexecute=withhold\n", heard: quiet},
|
|
{name: "served", setting: "execute=serve\n", heard: quiet, served: true},
|
|
{name: "withheld and heard", setting: "execute=withhold\n", heard: func() (bool, error) { return true, nil }, served: true},
|
|
{name: "the bus not asked", setting: "execute=withhold\n", heard: func() (bool, error) { return false, errors.New("no link") }, served: true},
|
|
{name: "no setting file", heard: quiet, served: true},
|
|
} {
|
|
_ = os.Remove(conf)
|
|
if row.setting != "" {
|
|
if err := os.WriteFile(conf, []byte(row.setting), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if served, why := executeServedHere(statePath, row.heard); served != row.served {
|
|
t.Errorf("%s: served %v (%s)", row.name, served, why)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// A verdict waiting for the setuid search says how far it got, in the reason the root-free verb repeats and as
|
|
// fields (novox/hq issue 361); a mount point, never a path the walk visited or found.
|
|
func TestTheStatementSaysHowFarTheSetuidSearchGot(t *testing.T) {
|
|
at := time.Date(2026, 10, 10, 1, 40, 0, 0, time.UTC)
|
|
sp := &accounts.SearchProgress{Started: at, Filesystems: 3, Done: 1, Walking: "/home", Visited: 42, Found: 2,
|
|
Kept: at, Finished: []accounts.SearchedFilesystem{{Mount: "/", Took: 90 * time.Second}}}
|
|
as := &accounts.Statement{At: at, Accounts: []accounts.Verdict{{Account: accounts.Account{Module: "claude-code",
|
|
ID: "claude-code.agent", Name: "agent", Root: true}, State: accounts.Unknown,
|
|
Reason: accounts.ReasonPending + ": the search for setuid programs, started 2026-10-10 01:27, has not finished yet",
|
|
Since: at, Search: sp}}}
|
|
h := withAccounts(healthAsReported(liveness.Statement{At: at}, nil), as)
|
|
r := h.Resources[0]
|
|
if !strings.HasPrefix(r.Reason, accounts.ReasonPending) || !strings.Contains(r.Reason, "1 of 3 filesystems walked, walking /home, 42 entries visited") {
|
|
t.Fatalf("the reason: %s", r.Reason)
|
|
}
|
|
s := r.Search
|
|
if s == nil || s.Filesystems != 3 || s.Done != 1 || s.Walking != "/home" || s.Visited != 42 || s.Found != 2 ||
|
|
s.Kept == nil || len(s.Finished) != 1 || s.Finished[0].Mount != "/" || *s.Finished[0].TookSeconds != 90 {
|
|
t.Fatalf("the fields: %+v", s)
|
|
}
|
|
}
|