package localapi import ( "context" "errors" "strings" "testing" "gitea.dooplex.hu/admin/felhom-agent/internal/proxmox" ) // R-1010: a half-removed vzdump snapshot. The error text is the one demo-hp's task printed on 2026-10-14 // (`audits/power-cut-fixes-2026-10-14/C4/hand-fix.txt`). const r1010MissingLV = `lvremove snapshot 'pve/snap_vm-9201-disk-0_vzdump' error: Failed to find logical volume "pve/snap_vm-9201-disk-0_vzdump"` func r1010TaskErr(exit string) error { return &proxmox.TaskError{UPID: "UPID:demo-hp:0001:0002:0003:vzdelsnapshot:9201:root@pam:", ExitStatus: exit, LogTail: []string{exit}} } // COMPANION RED-PROOF (observed 2026-10-19): with DeleteVzdumpSnapshot back to the single plain delete, the first case // fails with // // stalelock_r1010_test.go: missing volume: calls [plain], want [plain force]; err = proxmox: task … failed: exitstatus "lvremove snapshot …" // // Restored. func TestStaleLock_R1010_ForcesOnceOnlyWhenTheVolumeIsGone(t *testing.T) { cases := []struct { name string errs []error wantCalls []string wantErr string }{ {"missing volume", []error{r1010TaskErr(r1010MissingLV)}, []string{"plain", "force"}, ""}, {"missing volume, named only in the log tail", []error{&proxmox.TaskError{UPID: "x", ExitStatus: "command 'lvremove' failed: exit code 5", LogTail: []string{r1010MissingLV}}}, []string{"plain", "force"}, ""}, {"the plain delete works", nil, []string{"plain"}, ""}, {"another error: no force", []error{r1010TaskErr("lvremove snapshot 'pve/snap_vm-9201-disk-0_vzdump' error: Failed to update pool pve/data.")}, []string{"plain"}, "Failed to update pool"}, {"a missing volume of ANOTHER snapshot: no force", []error{r1010TaskErr(`Failed to find logical volume "pve/snap_vm-9201-disk-0_mine"`)}, []string{"plain"}, "_mine"}, {"the forced delete fails too: returned, never a second force", []error{r1010TaskErr(r1010MissingLV), errors.New("still broken")}, []string{"plain", "force"}, "still broken"}, } for _, c := range cases { api := &fakeStaleLockAPI{delErrs: c.errs} ctrl := NewStaleLockController(api, &recordingRunner{}, "felhom", nil) err := ctrl.DeleteVzdumpSnapshot(context.Background(), 9201) if strings.Join(api.delCalls, " ") != strings.Join(c.wantCalls, " ") { t.Errorf("%s: calls %v, want %v; err = %v", c.name, api.delCalls, c.wantCalls, err) } switch { case c.wantErr == "" && err != nil: t.Errorf("%s: err = %v, want nil", c.name, err) case c.wantErr != "" && (err == nil || !strings.Contains(err.Error(), c.wantErr)): t.Errorf("%s: err = %v, want one naming %q", c.name, err, c.wantErr) } } }