package reconcile import "testing" // F-LEAK (Campaign 8): a restore-test whose RESTORE FAILED could not destroy its own scratch guest — // `DELETE /nodes/x/lxc/990000` → 403 "missing privilege VM.Allocate". The cause is pool membership, // not privsep: VM.Allocate is granted at /pool/felhom only, and a failed restore never completes the // `--pool felhom` association, so the guest's own path resolves to / where the token holds nothing. // // The fix adopts the stranded scratch into the pool (Pool.Allocate — already granted) and retries the // destroy. scratchAdoptAllowed is the guard that keeps that from becoming "co-opt any guest into the // pool and delete it", and this file is its mirror test. // // Scenario D (the scratch is destroyed) is the live replay; Scenario E — the agent still cannot reach // a non-scratch guest this way — is HERE, because it must hold as a pure property and not depend on // what PVE happens to refuse. // Scenario E — the adoption path REFUSES anything outside the scratch band. // // RED-PROOF: replace the band check with `return true, ""` → every case below reports allowed and the // test fails with "adoption ALLOWED for vmid 9201 — that would let the agent co-opt a customer guest // into the pool and destroy it". func TestScratchAdoptAllowed_RefusesNonScratch(t *testing.T) { const min, max = 990000, 990009 for _, tc := range []struct { name string vmid int scratch bool }{ {"a live customer guest", 9201, true}, // scratch-flagged but OUTSIDE the band {"the golden image", 9100, true}, // ditto {"just below the band", min - 1, true}, // off-by-one {"just above the band", max + 1, true}, // off-by-one {"in-band but NOT scratch provenance", min, false}, {"neither", 100, false}, } { t.Run(tc.name, func(t *testing.T) { ok, why := scratchAdoptAllowed(tc.vmid, min, max, tc.scratch) if ok { t.Errorf("adoption ALLOWED for vmid %d — that would let the agent co-opt a non-scratch guest into the pool and destroy it", tc.vmid) } if why == "" { t.Error("refusal carried no reason — a silent refusal is unreviewable") } }) } } // The in-band, scratch-provenanced case IS allowed — otherwise the fix does nothing and the leak stays. // // RED-PROOF: make scratchAdoptAllowed always return false → this fails with "adoption refused for a // genuine in-band scratch guest", i.e. F-LEAK is not fixed at all. func TestScratchAdoptAllowed_AllowsGenuineScratch(t *testing.T) { const min, max = 990000, 990009 for _, vmid := range []int{min, min + 5, max} { ok, why := scratchAdoptAllowed(vmid, min, max, true) if !ok { t.Errorf("adoption refused for a genuine in-band scratch guest %d: %s", vmid, why) } } } // An unconfigured band must refuse everything rather than defaulting to something permissive — a // zero-valued band is a wiring bug, and the safe reading of a wiring bug is "do nothing". func TestScratchAdoptAllowed_UnconfiguredBandRefuses(t *testing.T) { for _, tc := range []struct{ min, max int }{{0, 0}, {0, 990009}, {990009, 990000}, {-1, 5}} { if ok, _ := scratchAdoptAllowed(990000, tc.min, tc.max, true); ok { t.Errorf("adoption allowed with an unconfigured band [%d,%d]", tc.min, tc.max) } } }