package backup import ( "context" "errors" "strings" "testing" "gitea.dooplex.hu/admin/felhom-agent/internal/proxmox" ) // R-1012: a snapshot backup that fails carries the orphan finding in its error — the text the controller mails to // the operator (`whole_guest_backup_failed`). The check runs ONCE, after the failure — never before the run. // RED-PROOF (P1/redproof-thincheck.txt): with explainFailure's append removed this test fails. RED-PROOF (2026-10-17 // night, felhom-agent REPORT.md): with the v0.159.0 pre-run check restored, calls = 2 and this test fails. func TestBackup_SnapshotFailure_NamesTheOrphan(t *testing.T) { const why = "probable cause: thin pool pve/data holds device 4 that LVM does not know" for name, api := range map[string]*fakeBackupAPI{ "task fails": {vzdumpUPID: "UPID:x", waitErr: errors.New("task failed: job errors")}, "start fails": {vzdumpErr: errors.New("vzdump: 500")}, } { t.Run(name, func(t *testing.T) { r := NewBackupRunner(api, "local", "", "", "", quiet()) calls := 0 r.SetThinCheck(func(context.Context, int) string { calls++; return why }) rec, err := r.Backup(context.Background(), 9201) if err == nil || rec.Success { t.Fatalf("want a failure, got %+v", rec) } if !strings.Contains(rec.Error, why) { t.Fatalf("the failure does not name the orphan: %q", rec.Error) } if calls != 1 { t.Fatalf("thin check ran %d times, want 1 (after the failure only)", calls) } }) } } // A successful backup never runs the check (it takes a metadata snapshot of the live pool, and nobody needs the answer) // and keeps a clean record; a stop-mode backup takes no snapshot, so the check never runs; an unwired runner behaves // exactly as before. RED-PROOF (2026-10-17 night): with the v0.159.0 pre-run check restored, the success leg fails // with calls=1. func TestBackup_ThinCheck_SilentWhenNotApplicable(t *testing.T) { ok := &fakeBackupAPI{vzdumpUPID: "UPID:x", content: []proxmox.StorageContent{{VolID: "local:backup/vzdump-lxc-9201-x", Content: "backup", VMID: 9201, Size: 100, CTime: 1}}} r := NewBackupRunner(ok, "local", "", "", "", quiet()) calls := 0 r.SetThinCheck(func(context.Context, int) string { calls++; return "probable cause: x" }) rec, err := r.Backup(context.Background(), 9201) if err != nil || !rec.Success || rec.Error != "" || calls != 0 { t.Fatalf("success: err=%v rec=%+v calls=%d (want no check on a successful backup, clean record)", err, rec, calls) } stop := NewBackupRunner(&fakeBackupAPI{vzdumpUPID: "UPID:x", waitErr: errors.New("boom")}, "local", proxmox.ModeStop, "", "", quiet()) calls = 0 stop.SetThinCheck(func(context.Context, int) string { calls++; return "probable cause: x" }) rec, _ = stop.Backup(context.Background(), 9201) if calls != 0 || strings.Contains(rec.Error, "probable cause") { t.Fatalf("stop mode: calls=%d error=%q", calls, rec.Error) } plain := NewBackupRunner(&fakeBackupAPI{vzdumpUPID: "UPID:x", waitErr: errors.New("boom")}, "local", "", "", "", quiet()) if rec, _ := plain.Backup(context.Background(), 9201); rec.Error != "boom" { t.Fatalf("unwired: error = %q, want the task error unchanged", rec.Error) } }