package storage import "testing" // v0.87.0 — SystemDisks device-mapper/md walk (IA finding 2, operator ruling 2026-07-13). // The direction that must be IMPOSSIBLE is a root-backing disk classified as a candidate; // the direction being fixed is the legacy-boot all-system over-protection (dead wizard). // // Fixtures mirror the §3 live transcripts: on the drill host / → /dev/mapper/pve-root → // dm-1 → slaves sda3 → parent sda; physical disks carry an EMPTY slaves dir (hasDir=true). // COMPANION red-proofs (run → fail → revert, recorded in REPORT): // B: walk returns only the dm node (pre-fix shape) → TestSystemDisks_WalkTopologies legacy // fixture fails its system-set assertion. // A: pre-fix resolver (no walk) → TestSystemDisks_LegacyBoot_WizardLives fails // (reproduces the dead-wizard live shape). // D: per-branch conservatism removed (skip unresolvable slaves) → the dangling fixture // fails on "scratch classified candidate while walk incomplete". // walkTopology is one fixture: the host's mounts + sysfs slaves tree, with the disks that // MUST be system and a scratch disk that must stay outside the set. type walkTopology struct { name string mounts []Mount slaves map[string][]string wantSystem []string // every entry MUST be in the resolved set (signature assertion B) scratch string // must NOT be in the set (wizard-eligible) } func walkTopologies() []walkTopology { return []walkTopology{ { // The drill host's exact shape (§3 transcript): legacy boot, LVM root, no ESP mount. name: "legacy LVM root (dm -> partition -> disk)", mounts: []Mount{{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}}, slaves: map[string][]string{ "pve-root": {"sda3"}, "sda": {}, // physical disk: slaves dir exists, empty (live-probed) }, wantSystem: []string{"/dev/sda"}, scratch: "/dev/sdd", }, { // md-raid root: BOTH member disks are system. name: "md-raid root (md -> 2 disks)", mounts: []Mount{{Device: "/dev/md0", MountPoint: "/", FSType: "ext4"}}, slaves: map[string][]string{ "md0": {"sda1", "sdb1"}, }, wantSystem: []string{"/dev/sda", "/dev/sdb"}, scratch: "/dev/sdd", }, { // Plain EFI + raw partitions (the pre-walk demo shape) — no virtual layer at all. name: "EFI + raw partitions", mounts: []Mount{ {Device: "/dev/sda2", MountPoint: "/", FSType: "ext4"}, {Device: "/dev/sda1", MountPoint: "/boot/efi", FSType: "vfat"}, }, slaves: map[string][]string{}, wantSystem: []string{"/dev/sda"}, scratch: "/dev/sdd", }, { // felhom-pve's exact shape (§3 baseline): EFI mount AND LVM root — the walk and the // ESP must agree on the same disk (scenario C: output identical pre/post). name: "EFI + LVM root (felhom-pve)", mounts: []Mount{ {Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}, {Device: "/dev/sda2", MountPoint: "/boot/efi", FSType: "vfat"}, }, slaves: map[string][]string{ "pve-root": {"sda3"}, }, wantSystem: []string{"/dev/sda"}, scratch: "/dev/sdd", }, { // Nested virtual layers: dm on md on partitions of two disks. name: "nested dm-on-md", mounts: []Mount{{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}}, slaves: map[string][]string{ "pve-root": {"md0"}, "md0": {"sda2", "sdb2"}, }, wantSystem: []string{"/dev/sda", "/dev/sdb"}, scratch: "/dev/sdd", }, } } // TestSystemDisks_WalkTopologies is the SIGNATURE test (scenario B): in every topology the // walk resolves, the root-backing physical disk(s) are ALWAYS in the system set. This // assertion may never be weakened. func TestSystemDisks_WalkTopologies(t *testing.T) { for _, tc := range walkTopologies() { t.Run(tc.name, func(t *testing.T) { host := &fakeHostReader{mounts: tc.mounts, slaves: tc.slaves} set, ok := SystemDisks(host) if !ok { t.Fatalf("SystemDisks must resolve this topology, got ok=false (set=%v)", set) } for _, d := range tc.wantSystem { if !set[d] { t.Errorf("SIGNATURE VIOLATION: root-backing disk %s missing from system set %v", d, set) } } if set[tc.scratch] { t.Errorf("scratch disk %s wrongly in the system set %v", tc.scratch, set) } // And the classification consequences: the system disk is never user-data, the // scratch disk is never system. if RoleForRawDevice(tc.wantSystem[0], set, ok) != RoleSystem { t.Errorf("root-backing disk %s must classify system", tc.wantSystem[0]) } if RoleForRawDevice(tc.scratch, set, ok) != RoleUserData { t.Errorf("scratch disk %s must classify user-data (wizard-eligible)", tc.scratch) } }) } } // TestSystemDisks_LegacyBoot_WizardLives (scenario A): the drill-box shape resolves — the // scratch disk is wizard-eligible instead of the pre-fix all-system dead end. // RED-PROOF companion: with the pre-fix resolver (wholeDiskOf only, no walk) this fails on // ok=false — the exact dead-wizard live shape from the IA report. func TestSystemDisks_LegacyBoot_WizardLives(t *testing.T) { host := &fakeHostReader{ mounts: []Mount{{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}}, slaves: map[string][]string{"pve-root": {"sda3"}}, } set, ok := SystemDisks(host) if !ok { t.Fatal("legacy-boot topology must resolve (the all-system fail-safe is the ERROR case, not the legacy-boot case)") } if !set["/dev/sda"] { t.Fatalf("root parent disk missing: %v", set) } if isSystemBacked("/dev/sdd", set, ok) { t.Fatal("hot-added scratch disk still classified system — the wizard stays dead") } } // TestSystemDisks_DanglingSlave_FailSafe (scenario D): an unresolvable slave fails the WHOLE // walk and callers see the all-system behavior through the SAME code path as today // (sysKnown=false → isSystemBacked true for everything). // RED-PROOF companion: treating an unresolved slave as skippable makes ok=true here → the // scratch disk classifies candidate while the walk is incomplete → this test fails. func TestSystemDisks_DanglingSlave_FailSafe(t *testing.T) { host := &fakeHostReader{ mounts: []Mount{{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}}, slaves: map[string][]string{ // One branch resolves (sda3 → sda), one is DANGLING (dm-9 has no /sys/block entry). // The resolvable branch is load-bearing for the red-proof: a "skip the unresolved // slave" mutation would yield a plausible non-empty set — exactly the partial // topology the conservatism rule forbids. "pve-root": {"sda3", "dm-9"}, }, } set, ok := SystemDisks(host) if ok { t.Fatalf("a dangling slave must fail the whole walk, got ok=true set=%v", set) } // The fail-safe path itself (not a lookalike): sysKnown=false forces system for EVERY // device, exactly as the pre-walk legacy behavior did. if !isSystemBacked("/dev/sdd", set, ok) { t.Fatal("fail-safe violated: scratch disk classified candidate while the walk is incomplete") } if RoleForRawDevice("/dev/sdd", set, ok) != RoleSystem { t.Fatal("fail-safe violated at role level") } } // TestSystemDisks_CycleGuard: a slaves cycle (corrupt sysfs / hostile fixture) terminates and // fails safe instead of recursing forever. func TestSystemDisks_CycleGuard(t *testing.T) { host := &fakeHostReader{ mounts: []Mount{{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}}, slaves: map[string][]string{ "pve-root": {"dm-1"}, "dm-1": {"pve-root"}, }, } if _, ok := SystemDisks(host); ok { t.Fatal("a slaves cycle must fail safe (ok=false)") } } // TestSystemDisks_VirtualWithEmptySlaves: a virtual root whose slaves dir is EMPTY (nothing to // ground on) fails safe — hasDir alone is not resolution. func TestSystemDisks_VirtualWithEmptySlaves(t *testing.T) { host := &fakeHostReader{ mounts: []Mount{{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}}, slaves: map[string][]string{"pve-root": {}}, } if _, ok := SystemDisks(host); ok { t.Fatal("virtual device with no slaves must fail safe") } }