Files
mesh-host/internal/units/units_test.go
T
jschoubben 62af89f85d
mesh/merge-gate head 62af89f85d0d merged into trunk 1a2677882380 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 62af89f85d0d merged into trunk 1a2677882380 — pass: its merge-check.sh passed
mesh/delivery delivered
Start no account's manager to look at its units (hq issue 764)
Every 15 s the failed-units look read the own manager of every account
the declaration creates, and reading one is a login that starts it: six
module accounts' managers on the control node started and stopped all
day, most of its journal. A look only reads (ADR 0240 rule 6) and never
asks an account's manager (ADR 0177).

An account's manager is read now only when the declaration places a
user-scoped unit in it, and only after the machine's own manager says
user@<uid>.service is active, as the apply asks before it. A manager not
running is said away in the units statement, with no failed units; in
liveness its units read unknown with the apply's words, never down. The
operator's account on a desktop is still read while logged in, since a
module places a user unit there (issue 315's Razer daemon stays said).
2026-10-15 00:33:34 +02:00

403 lines
15 KiB
Go

package units
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/novox/mesh-host/internal/declaration"
)
// The reading of failed units (novox/hq issue 315, "how it is checked"): a failed unit a module states,
// writes or installs is that module's, in either manager; one no module places is the machine's; a unit
// liveness judges is left to it; the two-look rule holds both ways; a manager that cannot be read is
// unread, never healthy; and only reads are asked.
type fakeReader struct {
failed map[string][]Listed // by "system" or "user:<account>"
shown map[string]Shown // by unit
owners map[string]string // by path
unread map[string]error
ownerQs int
// away is each account whose own manager is not running; noAccount each the machine lacks.
away map[string]bool
noAccount map[string]bool
// read is every manager whose failed units were asked, in order.
read []string
}
func mgr(scope, user string) string {
if scope == ScopeUser {
return "user:" + user
}
return "system"
}
func (f *fakeReader) Failed(_ context.Context, scope, user string) ([]Listed, error) {
f.read = append(f.read, mgr(scope, user))
if err := f.unread[mgr(scope, user)]; err != nil {
return nil, err
}
return f.failed[mgr(scope, user)], nil
}
func (f *fakeReader) Show(_ context.Context, _, _ string, units []string) (map[string]Shown, error) {
out := map[string]Shown{}
for _, u := range units {
out[u] = f.shown[u]
}
return out, nil
}
func (f *fakeReader) UserManagerRunning(_ context.Context, account string) (bool, bool, error) {
if f.noAccount[account] {
return false, false, nil
}
return !f.away[account], true, nil
}
func (f *fakeReader) PackageOwning(_ context.Context, path string) (string, bool, error) {
f.ownerQs++
p, ok := f.owners[path]
return p, ok, nil
}
// shanks is the desktop as found on 2026-10-08: two mounts of the machine's own mount table, the
// Razer daemon's packaged user unit, and a unit a removed package left behind.
func shanks() *fakeReader {
return &fakeReader{
failed: map[string][]Listed{
"system": {{Unit: "mnt-recalbox.mount", Load: "loaded"}, {Unit: "storage-media.mount", Load: "loaded"}},
"user:operator": {{Unit: "greenclip.service", Load: "not-found"},
{Unit: "openrazer-daemon.service", Load: "loaded"}},
},
shown: map[string]Shown{
"mnt-recalbox.mount": {FragmentPath: "/run/systemd/generator/mnt-recalbox.mount", Result: "exit-code"},
"storage-media.mount": {FragmentPath: "/run/systemd/generator/storage-media.mount", Result: "timeout"},
"greenclip.service": {},
"openrazer-daemon.service": {FragmentPath: "/usr/lib/systemd/user/openrazer-daemon.service", Result: "exit-code"},
},
owners: map[string]string{"/usr/lib/systemd/user/openrazer-daemon.service": "openrazer-daemon"},
}
}
func declared() *declaration.Declaration {
return &declaration.Declaration{Resources: []declaration.Resource{
&declaration.Package{ID: "openrazer.daemon", Type: declaration.TypePackage, Package: "openrazer-daemon"},
&declaration.Package{ID: "clipmenu.greenclip", Type: declaration.TypePackage, Package: "rofi-greenclip", Absent: true},
&declaration.User{ID: "zsh.account", Type: declaration.TypeUser, Name: "operator"},
// The operator's own manager is read because a module places a unit in it (dunst, on a desktop).
&declaration.Service{ID: "dunst.reload", Type: declaration.TypeService, Unit: "dunst.service",
Scope: declaration.ScopeUser, User: "operator"},
// A module account the declaration only creates: its manager is never asked (issue 764).
&declaration.User{ID: "telegram.account", Type: declaration.TypeUser, Name: "telegram"},
&declaration.Service{ID: "sshd.run", Type: declaration.TypeService, Unit: "sshd.service", State: "running"},
&declaration.Service{ID: "power.after-boot", Type: declaration.TypeService, Unit: "mesh-power-after-boot.service"},
&declaration.File{ID: "watcher.unit", Type: declaration.TypeFile, Path: "/home/operator/.config/systemd/user/watcher.service"},
}}
}
var t0 = time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC)
func lookTwice(t *testing.T, j *Judge) Statement {
t.Helper()
now := t0
j.Now = func() time.Time { return now }
first, _ := j.Look(context.Background())
if len(first.Failed) != 0 {
t.Fatalf("a unit was said failed on its first look: %+v", first.Failed)
}
now = now.Add(15 * time.Second)
st, _ := j.Look(context.Background())
return st
}
func TestTheModulesAndTheMachinesFailures(t *testing.T) {
r := shanks()
j := New(r)
j.Set(OwnedBy(declared(), nil))
st := lookTwice(t, j)
if st.State != Degraded {
t.Fatalf("state %q, want degraded", st.State)
}
owned := st.Owned()
if len(owned) != 1 || owned[0].Unit != "openrazer-daemon.service" || owned[0].Module != "openrazer" ||
owned[0].Resource != "openrazer.daemon" || owned[0].Via != ViaPackage || owned[0].Scope != ScopeUser {
t.Fatalf("the Razer daemon's packaged user unit is openrazer's: %+v", owned)
}
if owned[0].Since != t0 || owned[0].Streak != 2 || owned[0].Result != "exit-code" {
t.Fatalf("since the first look that found it, two in a row, how it failed: %+v", owned[0])
}
var machine []string
for _, f := range st.Unowned() {
machine = append(machine, f.Scope+"/"+f.Unit)
}
want := "system/mnt-recalbox.mount system/storage-media.mount user/greenclip.service"
if strings.Join(machine, " ") != want {
t.Fatalf("the machine's own: %v, want %s", machine, want)
}
}
func TestADeclaredUnitAndAWrittenFileAreTheirModules(t *testing.T) {
r := &fakeReader{
failed: map[string][]Listed{
"system": {{Unit: "mesh-power-after-boot.service", Load: "loaded"}, {Unit: "sshd.service", Load: "loaded"}},
"user:operator": {{Unit: "watcher.service", Load: "loaded"}},
},
shown: map[string]Shown{
"watcher.service": {FragmentPath: "/home/operator/.config/systemd/user/watcher.service"},
},
}
j := New(r)
j.Set(OwnedBy(declared(), nil))
st := lookTwice(t, j)
got := map[string]string{}
for _, f := range st.Failed {
got[f.Unit] = f.Module + " " + f.Via
}
if got["mesh-power-after-boot.service"] != "power unit" {
t.Errorf("a stateless service the declaration names is its module's: %q", got["mesh-power-after-boot.service"])
}
if got["watcher.service"] != "watcher file" {
t.Errorf("a user unit the mesh wrote is its module's: %q", got["watcher.service"])
}
if _, said := got["sshd.service"]; said {
t.Errorf("a service stated running is liveness's to judge, and said once: %v", got)
}
if r.ownerQs != 0 {
t.Errorf("the package database was asked %d time(s) for units the declaration already names", r.ownerQs)
}
}
func TestTwoLooksEachWay(t *testing.T) {
r := shanks()
j := New(r)
j.Set(OwnedBy(declared(), nil))
now := t0
j.Now = func() time.Time { return now }
st, changed := j.Look(context.Background())
if len(st.Failed) != 0 || !changed {
t.Fatalf("first look: nothing said failed, the state said: %+v %v", st.Failed, changed)
}
now = now.Add(15 * time.Second)
if st, changed = j.Look(context.Background()); len(st.Failed) != 4 || !changed {
t.Fatalf("second look: four said failed: %+v %v", st.Failed, changed)
}
now = now.Add(15 * time.Second)
if _, changed = j.Look(context.Background()); changed {
t.Fatal("a third look the same is no change")
}
// The operator mounts the media library again: no longer failed on the first look that says so.
r.failed["system"] = r.failed["system"][:1]
now = now.Add(15 * time.Second)
st, changed = j.Look(context.Background())
if len(st.Failed) != 3 || !changed {
t.Fatalf("a unit no longer failed is no longer said: %+v", st.Failed)
}
// Every manager clean: running.
r.failed = map[string][]Listed{}
now = now.Add(15 * time.Second)
if st, _ = j.Look(context.Background()); st.State != Running || len(st.Failed) != 0 {
t.Fatalf("no failed unit is running: %+v", st)
}
}
func TestAManagerThatCannotBeReadIsUnread(t *testing.T) {
r := shanks()
j := New(r)
j.Set(OwnedBy(declared(), nil))
lookTwice(t, j)
r.unread = map[string]error{"user:operator": errors.New("Failed to connect to bus: No medium found")}
st, _ := j.Look(context.Background())
if len(st.Unread) != 1 || !strings.Contains(st.Unread[0], "operator") {
t.Fatalf("the account manager is said unread: %v", st.Unread)
}
// Its failures are not counted while unread, and not forgotten either: back on the next read, said
// at once, from when they were first found.
r.unread = nil
st, _ = j.Look(context.Background())
for _, f := range st.Failed {
if f.Unit == "openrazer-daemon.service" && f.Since == t0 {
return
}
}
t.Fatalf("a manager read again says what it held, from when it was first found: %+v", st.Failed)
}
func TestNoManagerReadIsUnknown(t *testing.T) {
r := &fakeReader{unread: map[string]error{"system": errors.New("systemctl: not found"),
"user:operator": errors.New("systemctl: not found")}}
j := New(r)
j.Set(OwnedBy(declared(), nil))
if st, _ := j.Look(context.Background()); st.State != Unknown {
t.Fatalf("no manager read is unknown, never running: %q", st.State)
}
}
func TestAnAdoptedMachinesHoldingIsTheMachines(t *testing.T) {
d := &declaration.Declaration{Resources: []declaration.Resource{
&declaration.Service{ID: "found.cups", Type: declaration.TypeService, Unit: "cups.service"},
}}
o := OwnedBy(d, map[string]bool{"found.cups": true})
if _, ok := o.Units["system/cups.service"]; ok {
t.Fatal("a unit an adopted machine holds as found is the machine's, not a module's")
}
}
func TestTheReaderOnlyReads(t *testing.T) {
var asked []string
run := func(_ context.Context, name string, args ...string) (string, error) {
asked = append(asked, name+" "+strings.Join(args, " "))
switch {
case name == "systemctl" && contains(args, "list-units"):
return "● greenclip.service not-found failed failed greenclip.service\n" +
"openrazer-daemon.service loaded failed failed Daemon to manage razer devices in userspace\n", nil
case name == "systemctl" && contains(args, "show"):
return "Id=greenclip.service\nFragmentPath=\nResult=start-limit-hit\n\n" +
"Id=openrazer-daemon.service\nFragmentPath=/usr/lib/systemd/user/openrazer-daemon.service\nResult=exit-code\n", nil
case name == "pacman" && contains(args, "-Qqo"):
return "openrazer-daemon\n", nil
}
return "", nil
}
e := Exec{Run: run, System: "arch"}
listed, err := e.Failed(context.Background(), ScopeUser, "operator")
if err != nil || len(listed) != 2 || listed[0].Unit != "greenclip.service" || listed[0].Load != "not-found" {
t.Fatalf("the list, its mark skipped: %+v %v", listed, err)
}
shown, err := e.Show(context.Background(), ScopeUser, "operator", []string{"greenclip.service", "openrazer-daemon.service"})
if err != nil || shown["openrazer-daemon.service"].FragmentPath == "" || shown["greenclip.service"].Result != "start-limit-hit" {
t.Fatalf("the show: %+v %v", shown, err)
}
if pkg, ok, err := e.PackageOwning(context.Background(), "/usr/lib/systemd/user/openrazer-daemon.service"); !ok ||
pkg != "openrazer-daemon" || err != nil {
t.Fatalf("the owner: %q %v %v", pkg, ok, err)
}
for _, a := range asked {
if !strings.Contains(a, "list-units") && !strings.Contains(a, " show ") && !strings.HasPrefix(a, "pacman -Q") {
t.Errorf("asked something that is not a read: %q", a)
}
if !strings.HasPrefix(a, "pacman") && !strings.Contains(a, "--machine=operator@") {
t.Errorf("an account's manager is asked as that account's: %q", a)
}
}
}
func contains(args []string, s string) bool {
for _, a := range args {
if a == s {
return true
}
}
return false
}
func TestNothingIsReadBeforeADeclaration(t *testing.T) {
r := shanks()
j := New(r)
if st, changed := j.Look(context.Background()); st.State != "" || changed {
t.Fatalf("before a declaration every unit would read as the machine's: %+v", st)
}
}
// Issue 764: a look starts no account's manager. Only an account the declaration places a user-scoped
// unit for is read at all, and its manager only while the machine's says it runs.
func TestAnAccountWithNoUserUnitsIsNeverRead(t *testing.T) {
o := OwnedBy(declared(), nil)
if strings.Join(o.Accounts, " ") != "operator" {
t.Fatalf("only an account with a user-scoped unit is read: %v", o.Accounts)
}
r := shanks()
j := New(r)
j.Set(o)
lookTwice(t, j)
for _, m := range r.read {
if m == "user:telegram" {
t.Fatalf("a module account the declaration only creates had its manager read: %v", r.read)
}
}
}
func TestAManagerNotRunningIsAwayAndNotRead(t *testing.T) {
r := shanks()
j := New(r)
j.Set(OwnedBy(declared(), nil))
lookTwice(t, j)
if len(j.Last().Owned()) != 1 {
t.Fatalf("logged in, the Razer daemon is said: %+v", j.Last().Failed)
}
// The operator logs out: the manager is not asked, nothing in it is failed, it is said away.
r.away = map[string]bool{"operator": true}
r.read = nil
st, changed := j.Look(context.Background())
if strings.Join(r.read, " ") != "system" {
t.Fatalf("a manager not running is not read: %v", r.read)
}
if !changed || len(st.Away) != 1 || !strings.Contains(st.Away[0], "operator") || len(st.Unread) != 0 {
t.Fatalf("away, said, and not unread: %+v", st)
}
for _, f := range st.Failed {
if f.Scope == ScopeUser {
t.Fatalf("a manager not running has no failed units: %+v", f)
}
}
if st.State != Degraded {
t.Fatalf("the machine's own failed mounts still say degraded: %q", st.State)
}
// Back in: read as before, and a failure is new again (two looks).
r.away = nil
st, _ = j.Look(context.Background())
if len(st.Away) != 0 || len(st.Owned()) != 0 {
t.Fatalf("a manager running again is read, its failure on its first look not yet said: %+v", st)
}
st, _ = j.Look(context.Background())
if len(st.Owned()) != 1 {
t.Fatalf("and said on the second: %+v", st.Failed)
}
}
func TestAnAccountTheMachineLacksIsUnread(t *testing.T) {
r := shanks()
r.noAccount = map[string]bool{"operator": true}
j := New(r)
j.Set(OwnedBy(declared(), nil))
st, _ := j.Look(context.Background())
if strings.Join(r.read, " ") != "system" || len(st.Unread) != 1 {
t.Fatalf("an account the machine lacks is unread and its manager never asked: %v %+v", r.read, st)
}
}
func TestWhetherAManagerRunsIsAskedOfTheMachines(t *testing.T) {
var asked []string
state := "inactive"
run := func(_ context.Context, name string, args ...string) (string, error) {
asked = append(asked, name+" "+strings.Join(args, " "))
switch name {
case "getent":
return "telegram:x:10007:10007::/var/lib/telegram:/usr/bin/nologin\n", nil
case "systemctl":
if state != "active" {
return state + "\n", errors.New("systemctl exited 3")
}
return state + "\n", nil
}
return "", errors.New("unexpected")
}
e := Exec{Run: run, System: "arch"}
if running, exists, err := e.UserManagerRunning(context.Background(), "telegram"); running || !exists || err != nil {
t.Fatalf("an inactive manager is not running: %v %v %v", running, exists, err)
}
state = "active"
if running, _, err := e.UserManagerRunning(context.Background(), "telegram"); !running || err != nil {
t.Fatalf("an active manager runs: %v %v", running, err)
}
for _, a := range asked {
if a != "getent passwd telegram" && a != "systemctl is-active user@10007.service" {
t.Errorf("asked something but the user database and the machine's manager: %q", a)
}
}
}