agent v0.85.0 WIP: F12/F11/F10/F9/F2/F1 boot-recovery plane + appliance self-heal (pre-build)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// F2/F1 (CAMPAIGN-3): a remove (and, via the same path, a rolled-back add) must leave ZERO residue —
|
||||
// no failed-state units, no leftover mountpoint dir. RemoveNetworkMount must reset-failed the stuck
|
||||
// unit BEFORE removing the files (or systemd keeps it as not-found/failed) and rmdir the mountpoint.
|
||||
func TestRemoveNetworkMount_ZeroResidue(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("systemd-escaped unit filename contains a backslash; exercised on the Linux build server")
|
||||
}
|
||||
unitDir := t.TempDir()
|
||||
stageDir := t.TempDir()
|
||||
spec := NetworkMountSpec{Name: "media", Protocol: ProtocolNFS, Server: "10.0.0.5", Export: "/srv/media", MappedUID: 1000, MappedGID: 1000}
|
||||
where := spec.Where()
|
||||
mountName, err := UnitNameForMount(where)
|
||||
if err != nil {
|
||||
t.Fatalf("unit name: %v", err)
|
||||
}
|
||||
autoName := strings.TrimSuffix(mountName, ".mount") + ".automount"
|
||||
// Both unit files present on disk (rm is recorded, so they stay for the assertion).
|
||||
if err := os.WriteFile(filepath.Join(unitDir, mountName), []byte(renderNetworkMountUnit(spec)), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(unitDir, autoName), []byte(renderNetworkAutomountUnit(spec)), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := &recordingRunner{}
|
||||
ops := NewSudoHostOps(SudoHostOpsConfig{Runner: rr, Bins: Binaries{}.withDefaults(), UnitDir: unitDir, StageDir: stageDir, Host: &fakeHostReader{}, Logger: quietLogger()})
|
||||
// The automount is in the failed state (start-limit residue) — must be reset-failed.
|
||||
ops.unitFailed = func(_ context.Context, unit string) bool { return unit == autoName }
|
||||
|
||||
if err := ops.RemoveNetworkMount(context.Background(), "media"); err != nil {
|
||||
t.Fatalf("remove: %v", err)
|
||||
}
|
||||
|
||||
var sawReset, sawRmUnit, sawRmdir, sawReload bool
|
||||
for _, c := range rr.calls {
|
||||
j := strings.Join(c, " ")
|
||||
switch {
|
||||
case strings.Contains(j, "reset-failed -- "+autoName):
|
||||
sawReset = true
|
||||
case strings.Contains(j, "rm -f") && strings.Contains(j, autoName):
|
||||
sawRmUnit = true
|
||||
case strings.Contains(j, "rmdir") && strings.Contains(j, where):
|
||||
sawRmdir = true
|
||||
case strings.Contains(j, "daemon-reload"):
|
||||
sawReload = true
|
||||
}
|
||||
}
|
||||
if !sawReset {
|
||||
t.Errorf("F2: a failed unit must be reset-failed on remove; calls: %v", rr.calls)
|
||||
}
|
||||
if !sawRmUnit {
|
||||
t.Errorf("the unit files must be removed; calls: %v", rr.calls)
|
||||
}
|
||||
if !sawRmdir {
|
||||
t.Errorf("F1: the empty mountpoint dir must be rmdir'd; calls: %v", rr.calls)
|
||||
}
|
||||
if !sawReload {
|
||||
t.Errorf("daemon-reload must run after removal; calls: %v", rr.calls)
|
||||
}
|
||||
}
|
||||
|
||||
// rmdir is used (never rm -rf) — the fail-safe: a non-empty dir is left in place, not force-removed.
|
||||
func TestRemoveNetworkMount_NeverForceRemovesMountpoint(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("systemd-escaped unit filename contains a backslash; exercised on the Linux build server")
|
||||
}
|
||||
unitDir := t.TempDir()
|
||||
spec := NetworkMountSpec{Name: "media", Protocol: ProtocolNFS, Server: "10.0.0.5", Export: "/srv/media", MappedUID: 1000, MappedGID: 1000}
|
||||
mountName, _ := UnitNameForMount(spec.Where())
|
||||
autoName := strings.TrimSuffix(mountName, ".mount") + ".automount"
|
||||
_ = os.WriteFile(filepath.Join(unitDir, mountName), []byte(renderNetworkMountUnit(spec)), 0o644)
|
||||
_ = os.WriteFile(filepath.Join(unitDir, autoName), []byte(renderNetworkAutomountUnit(spec)), 0o644)
|
||||
|
||||
rr := &recordingRunner{}
|
||||
ops := NewSudoHostOps(SudoHostOpsConfig{Runner: rr, Bins: Binaries{}.withDefaults(), UnitDir: unitDir, StageDir: t.TempDir(), Host: &fakeHostReader{}, Logger: quietLogger()})
|
||||
if err := ops.RemoveNetworkMount(context.Background(), "media"); err != nil {
|
||||
t.Fatalf("remove: %v", err)
|
||||
}
|
||||
for _, c := range rr.calls {
|
||||
j := strings.Join(c, " ")
|
||||
if strings.Contains(j, "rm -rf") || (strings.Contains(j, "rm ") && strings.Contains(j, "/mnt/felhom-drives/media") && !strings.Contains(j, "rmdir")) {
|
||||
t.Errorf("mountpoint cleanup must be rmdir-only (never rm -rf); offending call: %s", j)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user