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) } }