Stage 1 — the host reports what a machine is and can do

Tier 0's first slice, per novox/hq 03-DESIGN/01-to-be/05-the-node-host.md. It
applies nothing, connects to nothing, listens on nothing. 2.9 MB, static, no
dynamic dependencies: copy it onto a machine and run it is the whole install,
which is the property ADR 0041 rests on.

A capability is detected, never assumed. Every detector runs something that only
succeeds if the thing FUNCTIONS — the daemon is asked for its version, the
package database is queried, the firewall is asked to list a ruleset, which
needs the privilege as well as the tool. 04-ISSUES/007 is the fault this
prevents: a client on disk with its daemon down looks exactly like a working
runtime, and a node assigned work on that basis fails when the work arrives.

Every verdict carries the reason and the method. A capability reported absent
with no reason is the same fault in a new place: something nobody can act on.

Two bugs found by running rather than reasoning, both silent:

systemctl is-system-running exits non-zero for every state except `running` —
including `degraded`, which means units failed and the init is emphatically
there. Reading the exit code reported NO service manager on a machine whose init
it was. That is 007 in the mirror, and both directions place work wrongly. A
verdict now reads what a tool says about itself, not only how it exited.

And `mesh-host inventory --json` printed text: the standard library stops
parsing at the first non-flag argument, so the flag sat unread and the command
exited 0 having ignored what was asked. The parser now takes the subcommand off
the front, and a stray or mistyped argument is refused rather than dropped.

Detection deliberately does NOT follow ADR 0008. That rule governs applying
state, where a failed step means the machine is not what was asked for. A failed
probe is a finding — "absent, because the probe failed" — and aborting would
replace one legible absence with total ignorance of the rest.

25 tests: structure and logic with a fake runner, and the same detectors against
this machine, because a test that fakes the system under detection asserts only
that the fake behaves as expected.
This commit is contained in:
2026-08-26 00:25:08 +02:00
commit 73c010e7ef
12 changed files with 1257 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
package inventory
import (
"context"
"errors"
"os"
"strings"
"testing"
"time"
"github.com/novox/mesh-host/internal/profile"
)
func TestAnUnreadableFactIsRecordedNotFatal(t *testing.T) {
// An inventory missing one field is useful; an inventory that refused to be taken is not.
// And an absent fact and a fact that failed to read are different things — collapsing them
// loses the one worth acting on.
failing := func(string) ([]byte, error) { return nil, errors.New("permission denied") }
inv := Collect(context.Background(), failing, nil, time.Second)
if inv.Machine == "" && len(inv.Unreadable) == 0 {
t.Fatal("nothing was read and nothing was reported unreadable")
}
if len(inv.Unreadable) == 0 {
t.Fatal("every file failed and nothing was recorded as unreadable")
}
for _, u := range inv.Unreadable {
if !strings.Contains(u, ":") {
t.Errorf("unreadable entry does not say which fact failed: %q", u)
}
}
if inv.Architecture == "" || inv.CPUs == 0 {
t.Error("facts that need no file were lost along with the ones that did")
}
}
func TestDistributionIsReadFromTheMachineNotAssumed(t *testing.T) {
cases := map[string]string{
"PRETTY_NAME=\"Arch Linux\"\nID=arch\n": "Arch Linux",
"ID=arch\n": "arch",
"# a comment\n\nID=debian\nPRETTY_NAME='Debian 13'\n": "Debian 13",
"NAME=Weird\nPRETTY_NAME=\"Has=Equals\"\n": "Has=Equals",
"": "",
}
for input, want := range cases {
if got := distributionFrom(input); got != want {
t.Errorf("distributionFrom(%q) = %q, want %q", input, got, want)
}
}
}
func TestMemoryIsReadOrReportedMissing(t *testing.T) {
if kb, ok := memoryFrom("MemTotal: 32762868 kB\nMemFree: 1 kB\n"); !ok || kb != 32762868 {
t.Errorf("memoryFrom returned %d, %v", kb, ok)
}
// A meminfo without MemTotal must not read as zero memory — a machine reporting no memory
// would be excluded from placement for a parsing failure.
if _, ok := memoryFrom("MemFree: 1 kB\n"); ok {
t.Error("a meminfo with no MemTotal was read as a successful measurement")
}
if _, ok := memoryFrom("MemTotal: not-a-number kB\n"); ok {
t.Error("an unparseable MemTotal was read as a successful measurement")
}
}
func TestTheInventorySaysWhenItWasTaken(t *testing.T) {
// A node unreachable for a week is an ordinary situation (novox/hq ADR 0036), so an
// inventory that cannot be told from a stale one is missing the fact that matters.
before := time.Now().UTC()
inv := Collect(context.Background(), nil, nil, time.Second)
if inv.ObservedAt.IsZero() {
t.Fatal("the inventory does not say when it was observed")
}
if inv.ObservedAt.Before(before.Add(-time.Second)) || inv.ObservedAt.After(time.Now().UTC().Add(time.Second)) {
t.Errorf("ObservedAt is not the moment of observation: %s", inv.ObservedAt)
}
}
func TestTheHostDoesNotNameTheNode(t *testing.T) {
// A node's name is assigned by the mesh. A host that named itself would be deciding
// something, which is precisely what novox/hq ADR 0037 forbids it to do.
inv := Collect(context.Background(), nil, nil, time.Second)
hostname, _ := os.Hostname()
if inv.Machine != hostname {
t.Errorf("Machine is %q, not the machine's own hostname %q", inv.Machine, hostname)
}
}
// --- against this machine ---------------------------------------------------------------
func TestAgainstThisMachine_inventoryIsTrue(t *testing.T) {
// novox/hq ADR 0034: behaviour against a real system is tested alongside, not mocked.
inv := Collect(context.Background(), nil, profile.Default(nil), 10*time.Second)
if inv.Machine == "" {
t.Error("this machine did not report a name")
}
if inv.CPUs < 1 {
t.Errorf("this machine reported %d cpus", inv.CPUs)
}
if inv.OS == "linux" {
if inv.Kernel == "" {
t.Error("a linux machine reported no kernel version")
}
if inv.MemoryKB <= 0 {
t.Error("a linux machine reported no memory")
}
}
if len(inv.Profile.Capabilities) == 0 {
t.Error("the inventory carries no profile")
}
t.Logf("machine=%s dist=%q kernel=%s arch=%s cpus=%d mem=%dMB unreadable=%v",
inv.Machine, inv.Distribution, inv.Kernel, inv.Architecture,
inv.CPUs, inv.MemoryKB/1024, inv.Unreadable)
}