package backup import ( "context" "fmt" "strings" "testing" "time" ) // R-104: a restic lock that survives BOTH self-heal layers was reported as „ismeretlen okból" — // the class had no lock case, and the lock text lives in restic's OUTPUT, never in the exit error, // so the text classifier could not have seen it anyway. The consequence asserted, through the real // resticStep with a scripted runner: the failure the run would report is classed Locked, and the // message names the lock in both languages instead of admitting an unknown cause. func TestR104_SurvivingLockIsNamed(t *testing.T) { m, _ := newOffboxManager(t) locked := []byte("Fatal: unable to create lock in backend: repository is already locked exclusively by PID 77 on felhom-controller\n") calls := 0 m.SetOffboxRunner(func(_ context.Context, _ []string, args ...string) ([]byte, error) { calls++ if contains(args, "unlock") { return nil, nil // the remove-all "succeeds", and the lock is still there on the retry } return locked, fmt.Errorf("exit status 1") }) _, err := m.resticStep(context.Background(), nil, []string{"-r", "repo"}, "backup:app", "backup", "/x") if err == nil { t.Fatal("setup: the scripted lock did not fail the step") } if calls != 3 { t.Errorf("setup: want step + unlock --remove-all + one retry (3 calls), got %d", calls) } runErr := fmt.Errorf("offbox backup app: %w", err) // exactly how the run wraps a step failure if got := ClassifyOffsiteFailure(runErr); got != OffsiteFailLocked { t.Errorf("R-104: a lock that survived the self-heal is classed %q, want %q", got, OffsiteFailLocked) } hu := offsiteFailureMessage(diagTarget(), runErr, time.Minute, "hu") if strings.Contains(hu, "ismeretlen okb") || !strings.Contains(hu, "zárva hagyta") { t.Errorf("R-104: the Hungarian message does not name the lock: %q", hu) } en := offsiteFailureMessage(diagTarget(), runErr, time.Minute, "en") if strings.Contains(en, "unknown reason") || !strings.Contains(en, "still locked") { t.Errorf("R-104: the English message does not name the lock: %q", en) } // The text path: an error that carries restic's own output is named too. if got := ClassifyOffsiteFailure(fmt.Errorf("restic: %s", locked)); got != OffsiteFailLocked { t.Errorf("R-104: restic's lock text is classed %q, want %q", got, OffsiteFailLocked) } // Negative control: an unrelated failure is still honest about being unknown. if got := ClassifyOffsiteFailure(fmt.Errorf("something entirely new")); got != OffsiteFailUnknown { t.Errorf("an unrelated failure became %q", got) } }