Files
felhom-agent/internal/reconcile/restoretest_mounts_test.go
T
admin b1697874ec GL-5b: restore-test full-fidelity verification (v0.76.0)
The restore-test had GL-5 finding #2's mirror image: its live-source-config
bind-override path tripped PVE's drop-unlisted-mountpoints rule, so scratch
guests boot-verified WITHOUT their storage mpN - weaker verification than
claimed. Params now derive from the ARCHIVE's own embedded config via
ExtractArchiveConfig + drRestoreOverrides (the object under test; full
layout, content genuinely extracted - the added runtime IS the
verification); unreadable/unknown-topology archives refuse up front. NEW
mount-parity assert (2b, pre-start): restored mpN set vs the archive's -
missing/mispathed/undersized/extra mpN fail the test naming the delta, so
constraint (b) can never regress into a green light. MountParity +
MountInventory ride the result + hub wire record (additive). Dead
bindMountOverrides/archiveVMID path deleted with its tests (no reachable
lookalike). DR bring-up untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-08 09:46:30 +02:00

101 lines
4.1 KiB
Go

package reconcile
import (
"strings"
"testing"
)
// (TestArchiveVMID + TestBindMountOverrides were deleted with their subjects in v0.76.0/GL-5b —
// the restore-test now derives its params from the ARCHIVE's embedded config via
// drRestoreOverrides; the live-source-config path was the pre-fix shape.)
func TestRootfsSizeGB(t *testing.T) {
cases := map[string]int{
"local-lvm:vm-9201-disk-0,size=8G": 8,
"local-lvm:base,size=512M": 1, // rounds up
"local-lvm:base,size=1T": 1024,
"local-lvm:base,size=0": 0,
"local-lvm:base": 0, // no size field
"local-lvm:base,size=8": 8, // unit-less → GB
}
for spec, want := range cases {
if got := rootfsSizeGB(spec); got != want {
t.Errorf("rootfsSizeGB(%q) = %d, want %d", spec, got, want)
}
}
}
// GL-5b: the mount-parity comparator — the assert that keeps PVE's drop-unlisted-mountpoints rule
// from regressing into a green light. Pure-function cases; the engine-level Scenario A/B live in
// restoretest_test.go / the fake-driven tests.
func TestMountParity(t *testing.T) {
archive := map[string]string{
"hostname": "demo",
"rootfs": "local-lvm:vm-9201-disk-0,size=32G",
"mp0": "local-lvm:vm-9201-disk-1,mp=/var/lib/docker,backup=1,size=200G",
"mp1": "local-lvm:vm-9201-disk-2,mp=/mnt/sys_drive,backup=1,size=50G",
"mp8": "/mnt/felhom-drives,mp=/mnt/felhom-drives",
"mp9": "/var/lib/felhom-agent/guests/9201/bootstrap,mp=/etc/felhom-bootstrap,ro=1",
}
// full parity: storage mpN at archived path+size, binds as throwaway stand-ins
restored := map[string]string{
"mp0": "local-lvm:vm-990000-disk-1,mp=/var/lib/docker,backup=1,size=200G",
"mp1": "local-lvm:vm-990000-disk-2,mp=/mnt/sys_drive,backup=1,size=50G",
"mp8": "local-lvm:vm-990000-disk-3,mp=/mnt/felhom-drives,backup=0,size=1G",
"mp9": "local-lvm:vm-990000-disk-4,mp=/etc/felhom-bootstrap,backup=0,size=1G",
}
inv, delta := mountParity(archive, restored)
if len(delta) != 0 {
t.Fatalf("full parity expected, got delta: %v", delta)
}
if len(inv) != 4 {
t.Fatalf("inventory must carry all 4 verified mpN, got %v", inv)
}
// the GL-5 constraint-(b) shape: a storage mpN silently dropped → NAMED delta (Scenario B core)
dropped := map[string]string{
"mp1": restored["mp1"], "mp8": restored["mp8"], "mp9": restored["mp9"],
}
_, delta = mountParity(archive, dropped)
if len(delta) != 1 || !strings.Contains(delta[0], "mp0 MISSING") {
t.Fatalf("dropped mp0 must be a named delta, got %v", delta)
}
// wrong path
wrongPath := map[string]string{
"mp0": "local-lvm:vm-990000-disk-1,mp=/elsewhere,size=200G",
"mp1": restored["mp1"], "mp8": restored["mp8"], "mp9": restored["mp9"],
}
_, delta = mountParity(archive, wrongPath)
if len(delta) != 1 || !strings.Contains(delta[0], `mp0 path "/elsewhere"`) {
t.Fatalf("wrong path must be a named delta, got %v", delta)
}
// undersized volume (a 1G stand-in where 200G of data should be = the dropped-content shape)
small := map[string]string{
"mp0": "local-lvm:vm-990000-disk-1,mp=/var/lib/docker,size=1G",
"mp1": restored["mp1"], "mp8": restored["mp8"], "mp9": restored["mp9"],
}
_, delta = mountParity(archive, small)
if len(delta) != 1 || !strings.Contains(delta[0], "mp0 size 1G < archive 200G") {
t.Fatalf("undersized mpN must be a named delta, got %v", delta)
}
// an extra restored mpN the archive never had is a delta too
extra := map[string]string{
"mp0": restored["mp0"], "mp1": restored["mp1"], "mp8": restored["mp8"], "mp9": restored["mp9"],
"mp3": "local-lvm:vm-990000-disk-9,mp=/stray,size=1G",
}
_, delta = mountParity(archive, extra)
if len(delta) != 1 || !strings.Contains(delta[0], "mp3 present on the restored guest but not in the archive") {
t.Fatalf("extra mpN must be a named delta, got %v", delta)
}
// no mpN anywhere = trivially parity (the golden-shaped archive)
inv, delta = mountParity(map[string]string{"rootfs": "l:d,size=8G"}, map[string]string{})
if len(delta) != 0 || len(inv) != 0 {
t.Fatalf("mpN-less archive must be trivial parity, got inv=%v delta=%v", inv, delta)
}
}