Files
felhom-controller/controller/internal/backup/r104_lock_class_test.go
T

56 lines
2.5 KiB
Go

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)
}
}