agent v0.37.0: re-assert mounted-but-DISABLED units (live felhom-usb fix)

The skip-if-mounted optimization defeated the actual root cause: felhom-usb is
mounted now but its unit is `disabled`, so a host reboot would not auto-mount
it. ReassertEnrolledMounts now skips ONLY the durable steady state (mounted AND
enabled) via the pure shouldReassertMount; a mounted-but-disabled unit is
re-asserted so enable --now re-creates the wants-symlink. Enabled-state read by
privilege-free Lstat of the multi-user.target.wants symlink (unitEnabled) — no
systemctl is-enabled subprocess, no new sudoers entry.

Tests: TestShouldReassertMount (4 combos), TestUnitEnabled (wants-symlink).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 17:55:48 +02:00
parent a621f4c5a0
commit 3e39dbb4f8
4 changed files with 96 additions and 12 deletions
+49
View File
@@ -192,3 +192,52 @@ type errExitT int
func (e errExitT) Error() string { return "exit status nonzero" }
func errExit(code int) error { return errExitT(code) }
// TestShouldReassertMount pins the host-reboot re-assert decision. The load-bearing case is
// mounted-but-DISABLED (the live felhom-usb bug): the drive serves now, but with no wants-symlink a
// host reboot would not auto-mount it, so it MUST still be re-asserted (enable --now re-creates the
// symlink). Only mounted+enabled is the durable steady state we skip.
func TestShouldReassertMount(t *testing.T) {
cases := []struct {
mounted, enabled, want bool
name string
}{
{true, true, false, "mounted+enabled → durable, skip"},
{true, false, true, "mounted+DISABLED → re-assert (the live bug: reboot would not auto-mount)"},
{false, true, true, "unmounted+enabled → re-assert (mount it now)"},
{false, false, true, "unmounted+disabled → re-assert (enable + mount)"},
}
for _, c := range cases {
if got := shouldReassertMount(c.mounted, c.enabled); got != c.want {
t.Errorf("%s: shouldReassertMount(%v,%v)=%v want %v", c.name, c.mounted, c.enabled, got, c.want)
}
}
}
// TestUnitEnabled detects the WantedBy=multi-user.target wants-symlink with a privilege-free Lstat
// (no systemctl subprocess), so the re-assert can tell a disabled unit from an enabled one.
func TestUnitEnabled(t *testing.T) {
unitDir := t.TempDir()
ops := NewSudoHostOps(SudoHostOpsConfig{
Runner: &recordingRunner{},
Bins: Binaries{Systemctl: "/usr/bin/systemctl", Install: "/usr/bin/install"},
UnitDir: unitDir,
StageDir: t.TempDir(),
Logger: quietLogger(),
})
const unit = "mnt-felhom-usb.mount"
if ops.unitEnabled(unit) {
t.Fatal("a unit with no wants-symlink must report NOT enabled (the felhom-usb disabled state)")
}
wants := filepath.Join(unitDir, "multi-user.target.wants")
if err := os.MkdirAll(wants, 0o755); err != nil {
t.Fatal(err)
}
// systemd points the wants-symlink at the real unit file; the target need not exist for Lstat.
if err := os.Symlink(filepath.Join(unitDir, unit), filepath.Join(wants, unit)); err != nil {
t.Skipf("symlink unsupported here (Windows privilege): %v", err)
}
if !ops.unitEnabled(unit) {
t.Fatal("a unit WITH a wants-symlink must report enabled")
}
}