ananke/internal/service/daemon_poststart_autorepair_test.go

90 lines
2.9 KiB
Go

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