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
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).
403 lines
15 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|