package backup import ( "context" "fmt" "sync/atomic" "testing" "time" "gitea.dooplex.hu/admin/felhom-agent/internal/reconcile" ) // R-874 (v0.145.0). THE MEASURED SHAPE (Part F spike, Tester 2): power-on sessions of ~1.5 h and ~5 min against a // 6 h evaluation ticker that restarts at every start — no restore-test ever evaluated. Now the first evaluation runs // FirstEval after start. // COMPANION RED-PROOF: drop the first-evaluation timer in Run (back to the bare ticker) → "no evaluation within". func TestR874_FirstEvaluationAfterStart(t *testing.T) { var picks int32 s := NewScheduler(SchedulerOptions{ Runner: &fakeRTRunner{res: reconcile.RestoreTestResult{Pass: true, Verified: "boot+running"}}, Pick: func(context.Context) (string, error) { atomic.AddInt32(&picks, 1) return fmt.Sprintf("local:backup/vzdump-lxc-9201-%d.tar.zst", atomic.LoadInt32(&picks)), nil }, Store: NewStore(), Spec: (&specSpy{}).build, Cadence: 6 * time.Hour, FirstEval: 30 * time.Millisecond, Logger: quiet(), }) ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) go func() { _ = s.Run(ctx); close(done) }() deadline := time.Now().Add(3 * time.Second) for atomic.LoadInt32(&picks) == 0 && time.Now().Before(deadline) { time.Sleep(10 * time.Millisecond) } cancel() <-done if atomic.LoadInt32(&picks) == 0 { t.Fatal("no evaluation within 3 s of start (FirstEval 30 ms) — a box with short sessions never gets a restore-test") } } // The earned restraint stays: an agent that restarts before FirstEval never evaluates (a crash loop does not // hammer a failing tier). func TestR874_CrashLoopNeverEvaluates(t *testing.T) { var picks int32 for i := 0; i < 5; i++ { // five quick "restarts" s := NewScheduler(SchedulerOptions{ Runner: &fakeRTRunner{res: reconcile.RestoreTestResult{Pass: true}}, Pick: func(context.Context) (string, error) { atomic.AddInt32(&picks, 1); return "x", nil }, Store: NewStore(), Spec: (&specSpy{}).build, Cadence: 6 * time.Hour, FirstEval: 200 * time.Millisecond, Logger: quiet(), }) ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) _ = s.Run(ctx) cancel() } if n := atomic.LoadInt32(&picks); n != 0 { t.Fatalf("a restart before FirstEval evaluated %d time(s)", n) } if DefaultFirstEval != 30*time.Minute { t.Fatalf("DefaultFirstEval = %s, the documented 30 min", DefaultFirstEval) } }