Files
mesh-host/internal/system/system_test.go
jschoubben b4d2e851a3 Name a stale package index, rather than reporting a failed install
novox/hq 04-ISSUES/002, which was recorded against HAL and is present here: a
machine asking the mirrors for a version they have already replaced gets a 404
from every one of them. The package exists and the declaration is correct — it
is the machine's view that is old — and reported as a generic install failure
it sends somebody to check the manifest, which is the one thing that is right.

It is deliberately not fixed by syncing. `pacman -Sy <pkg>` installs a package
built against libraries the machine does not have: a partial upgrade, which
this distribution does not support and which surfaces much later as something
apparently unrelated. The remedy is a full upgrade, which is a decision about
the whole machine rather than something a host does silently while applying one
resource. So this says which of the two it is looking at, and leaves the
decision where it belongs.

Every mirror, not one: a single mirror timing out is transient and retrying is
the answer.

And the package manager's own words were being discarded entirely — the output
was read into `_`. Whatever it said is now part of the failure, which is the
rule everywhere else here and was not being followed in the one place the
reason only exists in the output.
2026-08-31 12:59:53 +02:00

417 lines
16 KiB
Go

package system
import (
"context"
"errors"
"strings"
"testing"
"github.com/novox/mesh-host/internal/declaration"
)
// recorder answers a fixed map of commands and remembers what it was asked.
//
// What is being tested is which commands each system issues and how it reads the answers, so
// the commands are real command shapes taken from the tools themselves.
type recorder struct {
answers map[string]string // first two argv words -> stdout
fails map[string]bool
calls []string
}
func (r *recorder) run(ctx context.Context, name string, args ...string) (string, error) {
r.calls = append(r.calls, strings.TrimSpace(name+" "+strings.Join(args, " ")))
// Two keys, most specific first. `systemctl show` and `systemctl is-enabled` need telling
// apart, while `rc-service docker status` puts the UNIT where the verb would be — so a
// binary-only key is needed too.
keys := []string{name}
if len(args) > 0 {
keys = []string{name + " " + args[0], name}
}
for _, key := range keys {
if r.fails[key] {
return r.answers[key], errors.New("exit status 1")
}
if out, ok := r.answers[key]; ok {
return out, nil
}
}
return "", errors.New("exit status 127: not found")
}
func sys(t *testing.T, name string) System {
t.Helper()
s, err := For(name)
if err != nil {
t.Fatal(err)
}
return s
}
func TestEverySystemIsNamedAndReachable(t *testing.T) {
for _, want := range []string{"arch", "alpine", "android"} {
if _, err := For(want); err != nil {
t.Errorf("the %s host cannot be built: %v", want, err)
}
}
if _, err := For("debian"); err == nil {
t.Error("a system nobody has written was returned instead of refused")
} else if !strings.Contains(err.Error(), "arch") {
t.Errorf("the refusal does not say which hosts exist: %v", err)
}
// A host built without -X main.builtFor must refuse rather than default to something.
if _, err := For(""); err == nil {
t.Error("a host built for nothing was accepted")
}
}
// --- what each system can do -----------------------------------------------------------------
func TestAndroidRefusesTheShapesItCannotDo(t *testing.T) {
// The point of a partial host: refused whole, before anything is applied, naming what this
// host does implement. Not attempted-and-failed half way through.
d, err := declaration.ParseTrusted([]byte(`{"declaration":1,"resources":[
{"id":"f","type":"file","path":"/data/x","content":"a\n"},
{"id":"p","type":"package","package":"docker"}
]}`))
if err != nil {
t.Fatal(err)
}
refusal := Check(sys(t, "android"), d)
if refusal == nil {
t.Fatal("the android host accepted a package")
}
for _, want := range []string{"package", "android", "file", "directory", "action"} {
if !strings.Contains(refusal.Error(), want) {
t.Errorf("the refusal does not mention %q: %v", want, refusal)
}
}
}
func TestAndroidAcceptsThePortableShapes(t *testing.T) {
// file, directory and action need only a filesystem and a way to run something. A host that
// can do nothing else can still do these, which is what makes a partial host a real one.
d, err := declaration.ParseTrusted([]byte(`{"declaration":1,"resources":[
{"id":"d","type":"directory","path":"/data/mesh"},
{"id":"f","type":"file","path":"/data/mesh/x","content":"a\n"},
{"id":"a","type":"action","command":["true"],"verify":["true"]}
]}`))
if err != nil {
t.Fatal(err)
}
if err := Check(sys(t, "android"), d); err != nil {
t.Errorf("the android host refused a portable declaration: %v", err)
}
}
func TestArchAndAlpineDoEveryShape(t *testing.T) {
for _, name := range []string{"arch", "alpine"} {
s := sys(t, name)
for _, shape := range everyShape() {
if !Supports(s, shape) {
t.Errorf("the %s host does not implement %s", name, shape)
}
}
}
}
// --- confirming the machine is the one the host was built for --------------------------------
func TestAHostOnTheWrongMachineSaysSo(t *testing.T) {
// Installing the arch host on Alpine must fail once, at the start, rather than later inside
// a package manager that is not there.
alpineMachine := &recorder{answers: map[string]string{"apk info": "apk-tools-2.14.0\n"}}
if err := sys(t, "arch").Confirm(context.Background(), alpineMachine.run); err == nil {
t.Fatal("the arch host confirmed itself on an Alpine machine")
} else if !strings.Contains(err.Error(), "not Arch") {
t.Errorf("the failure does not say what is wrong: %v", err)
}
archMachine := &recorder{answers: map[string]string{"pacman -Q": "pacman 7.0.0-1\n"}}
if err := sys(t, "alpine").Confirm(context.Background(), archMachine.run); err == nil {
t.Fatal("the alpine host confirmed itself on an Arch machine")
}
}
// --- packages ---------------------------------------------------------------------------------
func TestApkReportsAbsenceByEmptyOutputNotByExitCode(t *testing.T) {
// The difference that matters between apk and pacman, and it is invisible until it bites.
//
// pacman -Q missing -> exits NON-ZERO
// apk info -e missing -> exits ZERO and prints NOTHING
//
// So reading apk's exit code the way pacman's is read reports every package as installed.
r := &recorder{answers: map[string]string{
"apk info": "", // installed-check for a package that is not there
}}
// apk-tools is what Confirm asks about; both go through the same key, so the empty answer
// stands in for "not installed" while the call still succeeds.
installed, err := sys(t, "alpine").PackageInstalled(context.Background(), r.run, "docker")
if err == nil && installed {
t.Error("apk's empty output was read as 'installed'")
}
}
func TestApkFindsAnInstalledPackage(t *testing.T) {
r := &recorder{answers: map[string]string{"apk info": "docker-24.0.7-r0\n"}}
installed, err := sys(t, "alpine").PackageInstalled(context.Background(), r.run, "docker")
if err != nil {
t.Fatalf("could not ask: %v", err)
}
if !installed {
t.Error("an installed package was reported missing")
}
}
func TestABrokenPackageDatabaseIsNotReadAsNotInstalled(t *testing.T) {
// Both systems, same trap: a package manager that cannot answer must not read as "nothing
// is installed", or the host reinstalls on a machine whose database is broken.
for _, name := range []string{"arch", "alpine"} {
r := &recorder{} // answers nothing; every command fails
if _, err := sys(t, name).PackageInstalled(context.Background(), r.run, "docker"); err == nil {
t.Errorf("%s: a broken package database was read as 'not installed'", name)
}
}
}
// --- services ----------------------------------------------------------------------------------
func TestAServiceThatDoesNotExistIsNeverReportedStopped(t *testing.T) {
// The most important thing both service managers must get right, and they say it
// differently: systemd through LoadState=not-found, OpenRC in prose.
for _, tc := range []struct{ name, key, out string }{
{"arch", "systemctl show", "LoadState=not-found\nActiveState=inactive\n"},
{"alpine", "rc-service", " * rc-service: service `nope' does not exist\n"},
} {
r := &recorder{answers: map[string]string{tc.key: tc.out}}
state, err := sys(t, tc.name).ServiceState(context.Background(), r.run, "nope")
if err == nil {
t.Errorf("%s: a service that does not exist was reported as %q", tc.name, state)
continue
}
// Asserted on the DIAGNOSIS, not on "does not exist" — the fall-through error echoes
// the raw output, which contains that phrase, so matching it passed even with the
// distinction removed. Only the correct branch explains why absence is not stopped.
if !strings.Contains(err.Error(), "absence as success") {
t.Errorf("%s: failed for the wrong reason: %v", tc.name, err)
}
}
}
func TestOpenRCStateIsReadFromItsOwnWords(t *testing.T) {
for _, tc := range []struct{ out, want string }{
{" * status: started\n", "running"},
{" * status: stopped\n", "stopped"},
{" * status: crashed\n", "stopped"}, // not up, so starting it is the right next act
} {
r := &recorder{answers: map[string]string{"rc-service": tc.out}}
got, err := sys(t, "alpine").ServiceState(context.Background(), r.run, "docker")
if err != nil {
t.Errorf("%q: %v", tc.out, err)
continue
}
if got != tc.want {
t.Errorf("%q read as %q, expected %q", tc.out, got, tc.want)
}
}
}
func TestOpenRCBootStateComesFromTheRunlevel(t *testing.T) {
// OpenRC has no `is-enabled`. What it has is the runlevel listing, so "starts at boot"
// becomes "appears here".
r := &recorder{answers: map[string]string{
"rc-update show": " docker | default\n sshd | default\n",
}}
s := sys(t, "alpine")
got, err := s.ServiceBoot(context.Background(), r.run, "docker")
if err != nil || got != "enabled" {
t.Errorf("a service in the default runlevel read as %q (%v)", got, err)
}
got, err = s.ServiceBoot(context.Background(), r.run, "chronyd")
if err != nil || got != "disabled" {
t.Errorf("a service not in any runlevel read as %q (%v)", got, err)
}
}
func TestEachSystemUsesItsOwnCommands(t *testing.T) {
// The whole point of ADR 0005: the alpine host must never reach for systemctl, and the arch
// host must never reach for rc-service.
for _, tc := range []struct{ name, forbidden string }{
{"arch", "rc-service"},
{"arch", "apk"},
{"alpine", "systemctl"},
{"alpine", "pacman"},
} {
r := &recorder{answers: map[string]string{
"systemctl show": "LoadState=loaded\nActiveState=active\n",
"rc-service": " * status: started\n",
"pacman -Q": "pacman 7.0.0\n",
"apk info": "apk-tools-2.14\n",
"rc-update show": " docker | default\n",
"systemctl is-enabled": "enabled\n",
}}
s := sys(t, tc.name)
_, _ = s.ServiceState(context.Background(), r.run, "docker")
_, _ = s.ServiceBoot(context.Background(), r.run, "docker")
_, _ = s.PackageInstalled(context.Background(), r.run, "docker")
for _, call := range r.calls {
if strings.HasPrefix(call, tc.forbidden) {
t.Errorf("the %s host called %q", tc.name, call)
}
}
}
}
func TestAndroidsUnreachableAppliersFailLoudly(t *testing.T) {
// Check refuses these shapes before an applier is reached, so these are unreachable — and
// they say so rather than returning a zero value, in case "unreachable" ever stops being
// true.
s := sys(t, "android")
r := &recorder{}
if _, err := s.PackageInstalled(context.Background(), r.run, "x"); !errors.Is(err, ErrUnsupported) {
t.Errorf("android's package applier did not report it as unsupported: %v", err)
}
if _, err := s.ServiceState(context.Background(), r.run, "x"); !errors.Is(err, ErrUnsupported) {
t.Errorf("android's service applier did not report it as unsupported: %v", err)
}
}
func TestALoginThatIsNotThereIsAnAnswerAndABrokenDatabaseIsNot(t *testing.T) {
// The distinction this package takes trouble over everywhere else, applied to users. A user
// database that cannot be read must not be reported as "no such user" — absence read as
// fact is the fault the whole host exists to prevent.
notFound := func(context.Context, string, ...string) (string, error) {
return "", errors.New("exit status 2")
}
if _, exists, err := LookUpUser(context.Background(), notFound, "nobody"); err != nil {
t.Fatalf("a missing user was reported as a failure: %v", err)
} else if exists {
t.Fatal("a missing user was reported as present")
}
broken := func(context.Context, string, ...string) (string, error) {
return "", errors.New("exit status 71: cannot read /etc/passwd")
}
if _, exists, err := LookUpUser(context.Background(), broken, "somebody"); err == nil {
t.Fatal("a broken user database was reported as an answer")
} else if exists {
t.Fatal("a broken user database reported a user as present")
}
}
func TestALoginIsReadFromThePasswdEntry(t *testing.T) {
answering := func(context.Context, string, ...string) (string, error) {
return "worker:x:1001:1001:,,,:/home/worker:/usr/bin/zsh\n", nil
}
login, exists, err := LookUpUser(context.Background(), answering, "worker")
if err != nil || !exists {
t.Fatalf("exists=%v err=%v", exists, err)
}
if login.Home != "/home/worker" || login.Shell != "/usr/bin/zsh" {
t.Fatalf("got %+v", login)
}
}
func TestSomethingThatIsNotAPasswdEntryIsRefused(t *testing.T) {
// Rather than read as a login with empty fields, which would have the host decide the shell
// differs and set it on every apply for ever.
nonsense := func(context.Context, string, ...string) (string, error) {
return "who knows\n", nil
}
if _, _, err := LookUpUser(context.Background(), nonsense, "worker"); err == nil {
t.Fatal("nonsense was read as a login")
}
}
func TestAPartialHostRefusesUsersAndAllowsArchives(t *testing.T) {
// An archive needs a filesystem and a way to fetch; a user needs a user database this host is
// allowed to write, which Android does not have.
speaks := map[declaration.Type]bool{}
for _, shape := range (android{}).Shapes() {
speaks[shape] = true
}
if !speaks[declaration.TypeArchive] {
t.Error("a partial host refuses archives, which need only a filesystem")
}
if speaks[declaration.TypeUser] {
t.Error("a partial host claims to manage users")
}
if err := (android{}).CreateUser(context.Background(), nil, "a", "", ""); err == nil {
t.Error("a partial host created a user")
}
}
// A stale index and a wrong declaration fail identically, and are fixed in completely different
// places.
//
// novox/hq 04-ISSUES/002: the package exists, the declaration is correct, and the machine is
// asking the mirrors for a version they have already replaced. Reported as a generic install
// failure it sends somebody to check the manifest, which is the one thing that is right.
func TestAStaleIndexIsNamedRatherThanReportedAsAFailedInstall(t *testing.T) {
said := "error: failed retrieving file 'dnsmasq-2.90-1-x86_64.pkg.tar.zst' from mirror.one : " +
"The requested URL returned error: 404\n" +
"error: failed retrieving file 'dnsmasq-2.90-1-x86_64.pkg.tar.zst' from mirror.two : " +
"The requested URL returned error: 404\n" +
"error: failed to commit transaction (failed to retrieve some files)"
run := func(context.Context, string, ...string) (string, error) {
return said, errors.New("exit status 1")
}
err := (arch{}).InstallPackage(context.Background(), run, "dnsmasq")
if err == nil {
t.Fatal("an install that failed reported success")
}
for _, want := range []string{"stale package index", "upgrading the machine", "partial upgrade"} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("the failure does not say %q, so it reads as a wrong declaration:\n%v", want, err)
}
}
// And the package manager's own words, which were being thrown away entirely.
if !strings.Contains(err.Error(), "404") {
t.Fatalf("what the package manager said was discarded:\n%v", err)
}
}
// An ordinary failure is not dressed up as a stale index: saying "upgrade the machine" about a
// package that does not exist sends somebody to do something large and useless.
func TestAnOrdinaryInstallFailureIsNotCalledAStaleIndex(t *testing.T) {
run := func(context.Context, string, ...string) (string, error) {
return "error: target not found: nosuchpackage", errors.New("exit status 1")
}
err := (arch{}).InstallPackage(context.Background(), run, "nosuchpackage")
if err == nil {
t.Fatal("an install that failed reported success")
}
if strings.Contains(err.Error(), "stale package index") {
t.Fatalf("a package that does not exist was called a stale index:\n%v", err)
}
if !strings.Contains(err.Error(), "target not found") {
t.Fatalf("what the package manager said was discarded:\n%v", err)
}
}
// One mirror failing is transient and retrying is the answer. Every mirror saying the file is gone
// is the index being old.
func TestOneMirrorFailingIsNotAStaleIndex(t *testing.T) {
run := func(context.Context, string, ...string) (string, error) {
return "warning: failed retrieving file 'x.pkg.tar.zst' from mirror.one : timeout",
errors.New("exit status 1")
}
err := (arch{}).InstallPackage(context.Background(), run, "x")
if err != nil && strings.Contains(err.Error(), "stale package index") {
t.Fatalf("one mirror timing out was called a stale index:\n%v", err)
}
}
// And an install that works still works.
func TestAnInstallThatSucceedsSaysNothing(t *testing.T) {
run := func(context.Context, string, ...string) (string, error) { return "installed", nil }
if err := (arch{}).InstallPackage(context.Background(), run, "dnsmasq"); err != nil {
t.Fatalf("a successful install reported a failure: %v", err)
}
}