package service import ( "bytes" "context" "errors" "log" "strings" "scm.bstein.dev/bstein/ananke/internal/cluster" "scm.bstein.dev/bstein/ananke/internal/execx" "testing" "time" "scm.bstein.dev/bstein/ananke/internal/config" ) // TestDaemonMaybeRunPostStartAutoHeal runs one orchestration or CLI step. // Signature: TestDaemonMaybeRunPostStartAutoHeal(t *testing.T). // Why: covers the daemon-side interval and on-battery guards for the new // post-start repair loop. func TestDaemonMaybeRunPostStartAutoHeal(t *testing.T) { calls := 0 d := &Daemon{ cfg: config.Config{ Startup: config.Startup{ PostStartAutoHealSeconds: 10, }, }, postStartAutoHealOverride: func(context.Context) error { calls++ return nil }, } d.cfg.Coordination.Role = "peer" var last time.Time d.maybeRunPostStartAutoHeal(context.Background(), &last, false) if calls != 0 || !last.IsZero() { t.Fatal("peer must not mutate shared cluster recovery state") } d.cfg.Coordination.Role = "coordinator" d.maybeRunPostStartAutoHeal(context.Background(), &last, false) if calls != 1 { t.Fatalf("expected first auto-heal invocation, got %d", calls) } d.maybeRunPostStartAutoHeal(context.Background(), &last, false) if calls != 1 { t.Fatalf("expected interval guard to suppress second call, got %d", calls) } last = time.Now().Add(-11 * time.Second) d.maybeRunPostStartAutoHeal(context.Background(), &last, true) if calls != 1 { t.Fatalf("expected on-battery guard to suppress call, got %d", calls) } last = time.Now().Add(-11 * time.Second) d.maybeRunPostStartAutoHeal(context.Background(), &last, false) if calls != 2 { t.Fatalf("expected second allowed auto-heal call, got %d", calls) } } // TestDaemonRecoveryFailureDoesNotStopMonitoring exercises optional repair isolation. // Signature: TestDaemonRecoveryFailureDoesNotStopMonitoring(t *testing.T). // Why: a missing or failed repair helper must not terminate UPS monitoring. func TestDaemonRecoveryFailureDoesNotStopMonitoring(t *testing.T) { var messages bytes.Buffer d := &Daemon{cfg: config.Config{Startup: config.Startup{PostStartAutoHealSeconds: 60}}, log: log.New(&messages, "", 0)} var last time.Time d.maybeRunPostStartAutoHeal(context.Background(), &last, false) if !last.IsZero() { t.Fatal("missing helper must not consume an interval") } if err := d.runPostStartAutoHeal(context.Background()); err != nil { t.Fatal(err) } d.orch = cluster.New(config.Config{}, &execx.Runner{DryRun: true}, nil, d.log) if err := d.runPostStartAutoHeal(context.Background()); err != nil { t.Fatal(err) } d.postStartAutoHealOverride = func(context.Context) error { return errors.New("synthetic repair failure") } d.maybeRunPostStartAutoHeal(context.Background(), &last, false) if last.IsZero() || !strings.Contains(messages.String(), "synthetic repair failure") { t.Fatal("failed repair must be logged and rate limited") } }