v0.297.0: burn-down round 2 — 24 small rows (R-591 R-568 R-567 R-363 R-547 R-10 R-552 R-251 R-104 R-619 R-362 R-675 R-256 R-257 R-240 R-365 R-425 R-565 R-564 R-603 R-454 R-208 R-457-swept) + the banner countdown and deepCopyStack twins; MinAgent 0.131.0
gates / gates (push) Failing after 50s
gates / gates (push) Failing after 50s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user