package dr import ( "context" "testing" "gitea.dooplex.hu/admin/felhom-agent/internal/hub" ) func sampleRecipe() *hub.DRRecipeHostHalf { return &hub.DRRecipeHostHalf{ RecipeVersion: 1, Guests: []hub.DRGuest{{VMID: 9201, Cores: 2, MemoryBytes: 12 << 30, DiskBytes: 32 << 30}}, PBS: &hub.DRPBSCoord{RepoID: "felhom-offsite", Namespace: "demo-felhom-01", LatestSnapshotID: "9201"}, Drives: []hub.DRDrive{{DurableID: "uuid:abc", MountPath: "/mnt/felhom-drives/photos", Intent: "enrolled", TotalBytes: 500 << 30}}, } } // TestBuildRestorePlan_HostLoss: a host_loss directive + recipe yields per-guest {vmid, archive, // target, sizing} + per-drive {durable_id → mount} + the offsite PBS coord. func TestBuildRestorePlan_HostLoss(t *testing.T) { dir := &hub.WireRestoreDirective{Mode: "host_loss", VMID: 9201, Archive: "felhom-offsite:backup/ct/9201/2026-07-04T14:55:44Z"} plan, ok := BuildRestorePlan(dir, sampleRecipe(), "local-lvm") if !ok || plan == nil { t.Fatal("host_loss must yield a plan") } if plan.Mode != "host_loss" || len(plan.Guests) != 1 || len(plan.Drives) != 1 { t.Fatalf("plan shape = %+v", plan) } g := plan.Guests[0] if g.VMID != 9201 || g.TargetStorage != "local-lvm" || g.Cores != 2 || g.DiskBytes != 32<<30 { t.Errorf("planned guest = %+v", g) } if g.Archive != dir.Archive { t.Errorf("planned guest archive = %q, want the directive's %q", g.Archive, dir.Archive) } d := plan.Drives[0] if d.DurableID != "uuid:abc" || d.ExpectedMount != "/mnt/felhom-drives/photos" { t.Errorf("planned drive (durable_id→mount) = %+v", d) } if plan.PBS == nil || plan.PBS.RepoID != "felhom-offsite" { t.Errorf("plan must carry the offsite PBS coord, got %+v", plan.PBS) } } // TestBuildRestorePlan_NoPlanCases is the red-proof anchor: guest_loss / absent / nil-recipe yield // NO plan (execute-nothing on the wrong mode). Relaxing the mode gate → the guest_loss case fails. func TestBuildRestorePlan_NoPlanCases(t *testing.T) { if _, ok := BuildRestorePlan(&hub.WireRestoreDirective{Mode: "guest_loss", VMID: 9201}, sampleRecipe(), "local-lvm"); ok { t.Error("guest_loss must NOT yield a host-loss plan") } if _, ok := BuildRestorePlan(nil, sampleRecipe(), "local-lvm"); ok { t.Error("absent directive must NOT yield a plan") } if _, ok := BuildRestorePlan(&hub.WireRestoreDirective{Mode: "host_loss"}, nil, "local-lvm"); ok { t.Error("nil recipe must NOT yield a plan") } } // TestConsumer_SurfacesPlanNeverExecutes: the consumer surfaces the plan on host_loss, consults the // recipe only then, and clears it otherwise. It has NO restore/destroy dependency (execute-nothing // is structural — the type literally cannot call a restore). func TestConsumer_SurfacesPlanNeverExecutes(t *testing.T) { recipeCalls := 0 c := NewConsumer(func(context.Context) *hub.DRRecipeHostHalf { recipeCalls++; return sampleRecipe() }, "local-lvm", nil) ds := func(d *hub.WireRestoreDirective) *hub.DesiredStateResponse { return &hub.DesiredStateResponse{DesiredState: hub.WireDesiredState{RestoreDirective: d}} } // non-host_loss → no plan, recipe NOT consulted. c.OnDesiredState(context.Background(), ds(&hub.WireRestoreDirective{Mode: "guest_loss"})) if c.LastPlan() != nil { t.Error("guest_loss set a plan") } if recipeCalls != 0 { t.Errorf("recipe consulted on a non-host_loss directive (%d calls)", recipeCalls) } // host_loss → plan surfaced, recipe consulted once. c.OnDesiredState(context.Background(), ds(&hub.WireRestoreDirective{Mode: "host_loss", VMID: 9201})) p := c.LastPlan() if p == nil || len(p.Guests) != 1 || p.Guests[0].VMID != 9201 { t.Fatalf("host_loss plan = %+v", p) } if recipeCalls != 1 { t.Errorf("recipe calls = %d, want 1", recipeCalls) } // absent directive clears the plan. c.OnDesiredState(context.Background(), ds(nil)) if c.LastPlan() != nil { t.Error("absent directive did not clear the plan") } }