Files
felhom-agent/internal/storage/claim_test.go
T
admin 52098302ab v0.54.0: format-safety foundation — unclaimed-disk guard + guarded-mkfs wrapper
Impl-1. Format now runs a mandatory unclaimed-disk guard (internal/storage/claim.go:
SystemDisks + lsblk member-FSTYPE + foreign-mount + RO + pvs/zpool; fail-safe →
CLAIMED) before any mkfs — refuses the OS disk / LVM PV / ZFS-mdraid member /
foreign-mounted device even when non-data-bearing (guard sits in Format, not the
handler). Below the agent, mkfs goes ONLY through configs/felhom-mkfs-guarded.sh
(sudoers no longer allowlists raw mkfs.*), which re-checks the catastrophic cases
as root. Read-only pvs/zpool added to FELHOM_DISK. Tests + red-proof; capability
manifest updated. go build/vet/test clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:52:31 +02:00

156 lines
6.4 KiB
Go

package storage
import (
"context"
"strings"
"testing"
)
// classifyClaim is the pure guard verdict — the load-bearing safety logic. Table-driven over every
// claim signal + the fail-safe, plus the two ALLOW cases (clean disk, and re-init of our own drive).
func TestClassifyClaim(t *testing.T) {
base := func() claimFacts {
return claimFacts{device: "/dev/sdd", wholeDisk: "/dev/sdd", wholeDiskOK: true,
nodes: []claimNode{{name: "sdd", fstype: "", mountpoint: ""}, {name: "sdd1", fstype: "ntfs", mountpoint: ""}}}
}
cases := []struct {
name string
mutate func(f *claimFacts)
wantUnclaimed bool
reasonHas string
}{
{"clean unclaimed disk (ntfs, unmounted)", func(f *claimFacts) {}, true, "unclaimed"},
{"blank disk", func(f *claimFacts) { f.nodes = []claimNode{{name: "sdd"}} }, true, "unclaimed"},
{"our own drive re-init (mounted under /mnt/felhom-drives)", func(f *claimFacts) {
f.nodes = []claimNode{{name: "sdd"}, {name: "sdd1", fstype: "ext4", mountpoint: "/mnt/felhom-drives/felhom-flash"}}
}, true, "unclaimed"},
{"system/OS disk (non-data-bearing) — RED-PROOF vs DataBearing", func(f *claimFacts) {
f.isSystem = true
f.nodes = []claimNode{{name: "sda"}} // blank: DataBearing would say benign; the guard must still refuse
}, false, "system/OS disk"},
{"LVM PV (pvs)", func(f *claimFacts) { f.lvmPV = true }, false, "LVM physical volume"},
{"LVM2_member via lsblk fstype", func(f *claimFacts) {
f.nodes = []claimNode{{name: "sdd"}, {name: "sdd1", fstype: "LVM2_member"}}
}, false, "LVM2_member"},
{"ZFS pool member (zpool)", func(f *claimFacts) { f.zfsMember = true }, false, "ZFS pool member"},
{"zfs_member via lsblk fstype", func(f *claimFacts) {
f.nodes = []claimNode{{name: "sdd", fstype: "zfs_member"}}
}, false, "zfs_member"},
{"mdraid member", func(f *claimFacts) {
f.nodes = []claimNode{{name: "sdd1", fstype: "linux_raid_member"}}
}, false, "linux_raid_member"},
{"active swap signature", func(f *claimFacts) {
f.nodes = []claimNode{{name: "sdd1", fstype: "swap"}}
}, false, "swap"},
{"foreign mount (outside /mnt/felhom-drives)", func(f *claimFacts) {
f.nodes = []claimNode{{name: "sdd1", fstype: "ext4", mountpoint: "/srv/data"}}
}, false, "mounted at /srv/data"},
{"read-only device", func(f *claimFacts) { f.readonly = true }, false, "read-only"},
{"undeterminable topology", func(f *claimFacts) { f.wholeDiskOK = false }, false, "undeterminable"},
{"fail-safe: gather error", func(f *claimFacts) { f.gatherErr = "lsblk failed" }, false, "could not determine"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
f := base()
tc.mutate(&f)
gotUnclaimed, reason := classifyClaim(f)
if gotUnclaimed != tc.wantUnclaimed {
t.Fatalf("unclaimed=%v want %v (reason %q)", gotUnclaimed, tc.wantUnclaimed, reason)
}
if !strings.Contains(reason, tc.reasonHas) {
t.Errorf("reason %q missing %q", reason, tc.reasonHas)
}
})
}
}
func TestParseLsblkNodes(t *testing.T) {
out := []byte(`{"blockdevices":[{"name":"sdd","fstype":null,"mountpoint":null,"children":[{"name":"sdd1","fstype":"ntfs","mountpoint":null}]}]}`)
nodes, err := parseLsblkNodes(out)
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(nodes) != 2 || nodes[0].name != "sdd" || nodes[1].name != "sdd1" || nodes[1].fstype != "ntfs" {
t.Fatalf("nodes = %+v", nodes)
}
if _, err := parseLsblkNodes([]byte("not json")); err == nil {
t.Error("expected a parse error on garbage")
}
}
// --- Format guard integration: the guard is wired into Format and actually gates mkfs. ---
// newGuardOps builds a SudoHostOps with stubbed reads: a scriptRunner for lsblk/mkfs, a fakeHostReader
// for SystemDisks, Pvs/Zpool empty (→ binaryPresent=false → skipped), and readWholeDiskRO stubbed.
func newGuardOps(sr *scriptRunner, mounts []Mount) *SudoHostOps {
return &SudoHostOps{
runner: sr,
// Pvs/Zpool empty ⇒ binaryPresent=false ⇒ skipped (deterministic across OSes; lsblk carries the signals).
bins: Binaries{Lsblk: "/usr/bin/lsblk", MkfsGuarded: "/usr/local/sbin/felhom-mkfs-guarded"},
host: &fakeHostReader{mounts: mounts},
logger: quietLogger(),
}
}
func mkfsCalled(sr *scriptRunner) bool {
for _, c := range sr.calls {
if strings.Contains(c[0], "mkfs") {
return true
}
}
return false
}
func TestFormatGuard_RefusesSystemDisk(t *testing.T) {
defer stubRO(false)()
sr := &scriptRunner{out: map[string][]byte{
"/usr/bin/lsblk": []byte(`{"blockdevices":[{"name":"sda","children":[{"name":"sda2","mountpoint":"/"}]}]}`),
}}
// / on /dev/sda2 → SystemDisks resolves {/dev/sda}; formatting /dev/sda must be refused.
ops := newGuardOps(sr, []Mount{{Device: "/dev/sda2", MountPoint: "/"}})
err := ops.Format(context.Background(), "/dev/sda", "ext4")
if err == nil || !strings.Contains(err.Error(), "system/OS disk") {
t.Fatalf("want system-disk refusal, got %v", err)
}
if mkfsCalled(sr) {
t.Fatal("mkfs was invoked on a claimed device — guard failed")
}
}
func TestFormatGuard_RefusesLVMMember(t *testing.T) {
defer stubRO(false)()
sr := &scriptRunner{out: map[string][]byte{
"/usr/bin/lsblk": []byte(`{"blockdevices":[{"name":"sdd","children":[{"name":"sdd1","fstype":"LVM2_member"}]}]}`),
}}
ops := newGuardOps(sr, []Mount{{Device: "/dev/sda2", MountPoint: "/"}})
err := ops.Format(context.Background(), "/dev/sdd", "ext4")
if err == nil || !strings.Contains(err.Error(), "LVM2_member") {
t.Fatalf("want LVM-member refusal, got %v", err)
}
if mkfsCalled(sr) {
t.Fatal("mkfs invoked on an LVM member — guard failed")
}
}
func TestFormatGuard_AllowsUnclaimed(t *testing.T) {
defer stubRO(false)()
sr := &scriptRunner{out: map[string][]byte{
"/usr/bin/lsblk": []byte(`{"blockdevices":[{"name":"sdd","children":[{"name":"sdd1","fstype":"ntfs"}]}]}`),
}}
// /dev/sdd is not the system disk, not a member, not mounted → guard passes → mkfs runs.
ops := newGuardOps(sr, []Mount{{Device: "/dev/sda2", MountPoint: "/"}})
if err := ops.Format(context.Background(), "/dev/sdd", "ext4"); err != nil {
t.Fatalf("unclaimed disk must format, got %v", err)
}
if !mkfsCalled(sr) {
t.Fatal("mkfs was NOT invoked on an unclaimed disk — guard over-refused")
}
}
// stubRO overrides the /sys read-only probe for the duration of a test; the returned func restores it.
func stubRO(ro bool) func() {
orig := readWholeDiskRO
readWholeDiskRO = func(string) (bool, error) { return ro, nil }
return func() { readWholeDiskRO = orig }
}