Files
felhom-agent/internal/mgmtplane/mgmtplane_test.go
T
admin fd4e177216 feat(mgmtplane): break-glass privsep-dir watchdog + mgmt_plane health (TASK G1) — v0.71.0
Prerequisite for felhom-sshd (H1). Closes the SPIKE-felhom-sshd §8 lockout: a
second sshd's RuntimeDirectory=sshd removed the SHARED /run/sshd privsep dir and
took stock sshd on :22 down (sessions reset after KEXINIT).

Host artifacts (configs/, installed by felhom-host-install):
- felhom-privsep.tmpfiles: layer 1, boot-persistent /run/sshd owned by no unit
- felhom-mgmt-watchdog.sh/.service/.timer: layer 2, AGENT-INDEPENDENT ~60s heal
  (stat-first recreate + reset-failed sshd only if failed + heal-marker); never
  RuntimeDirectory=, never restarts stock sshd, never touches a healthy dir.

Go (internal/mgmtplane): read-only Reporter → additive omitempty mgmt_plane
heartbeat stanza (privsep_dir_ok/sshd_reachable/healed_recently/privsep_healed_at),
wired via Collector.SetMgmtPlaneReporter. Non-hollow tests + red-proofs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-05 18:49:27 +02:00

90 lines
3.2 KiB
Go

package mgmtplane
import (
"context"
"os"
"path/filepath"
"testing"
)
// newTestReporter builds a Reporter with injected probes so tests never touch the real /run or a
// real socket. privsepOK / sshdOK are the probe verdicts; markerContent is written to a temp marker
// file (empty string = no marker file at all).
func newTestReporter(t *testing.T, privsepOK, sshdOK bool, markerContent string) *Reporter {
t.Helper()
marker := filepath.Join(t.TempDir(), "healed")
if markerContent != "" {
if err := os.WriteFile(marker, []byte(markerContent), 0o600); err != nil {
t.Fatalf("write marker: %v", err)
}
}
return &Reporter{
privsepDir: "/run/sshd",
marker: marker,
sshdAddr: "127.0.0.1:22",
statDir: func(string) bool { return privsepOK },
readMarker: readMarkerFile, // the REAL marker reader — exercises the parse (red-proof target)
dialSSHD: func(context.Context, string) bool { return sshdOK },
}
}
func TestMgmtPlane_Healthy_NoHealMarker(t *testing.T) {
st := newTestReporter(t, true, true, "").MgmtPlaneStatus(context.Background())
if !st.PrivsepDirOK || !st.SshdReachable {
t.Fatalf("healthy host: want dir+sshd ok, got %+v", st)
}
if st.HealedRecently || st.PrivsepHealedAt != "" {
t.Fatalf("no marker → HealedRecently must be false + no timestamp, got %+v", st)
}
}
func TestMgmtPlane_PrivsepDirMissing_IsDetected(t *testing.T) {
// The load-bearing detector: /run/sshd absent = the KEXINIT-reset lockout condition.
st := newTestReporter(t, false, true, "").MgmtPlaneStatus(context.Background())
if st.PrivsepDirOK {
t.Fatal("privsep dir missing must report PrivsepDirOK=false (the lockout detector)")
}
if !st.SshdReachable {
t.Fatal("listener still up while privsep gone — sshd_reachable should stay true (that's the trap: TCP up, sessions broken)")
}
}
func TestMgmtPlane_HealMarkerPresent_SurfacesTimestamp(t *testing.T) {
const ts = "2026-07-05T16:42:17Z"
st := newTestReporter(t, true, true, ts).MgmtPlaneStatus(context.Background())
if !st.HealedRecently {
t.Fatal("watchdog heal-marker present → HealedRecently must be true (the recurring-clobber signal)")
}
if st.PrivsepHealedAt != ts {
t.Fatalf("PrivsepHealedAt: want %q, got %q", ts, st.PrivsepHealedAt)
}
}
func TestMgmtPlane_EmptyMarker_TreatedAsAbsent(t *testing.T) {
// A truncated/empty marker must NOT report a heal we can't timestamp (would raise a hub warning
// with an empty healed_at). Red-proof: if readMarkerFile returned ("",true) for an empty file,
// HealedRecently would wrongly be true.
st := newTestReporter(t, true, true, " \n").MgmtPlaneStatus(context.Background())
if st.HealedRecently || st.PrivsepHealedAt != "" {
t.Fatalf("empty marker must be treated as no-heal, got %+v", st)
}
}
func TestMgmtPlane_SshdUnreachable_Reported(t *testing.T) {
st := newTestReporter(t, true, false, "").MgmtPlaneStatus(context.Background())
if st.SshdReachable {
t.Fatal("dial failing → sshd_reachable must be false")
}
}
func TestItoa(t *testing.T) {
for _, c := range []struct {
in int
want string
}{{0, "0"}, {22, "22"}, {8822, "8822"}, {65535, "65535"}} {
if got := itoa(c.in); got != c.want {
t.Fatalf("itoa(%d)=%q want %q", c.in, got, c.want)
}
}
}