ananke/cmd/ananke/power_safety_test.go

201 lines
6.5 KiB
Go

package main
import (
"context"
"errors"
"testing"
"time"
"scm.bstein.dev/bstein/ananke/internal/config"
"scm.bstein.dev/bstein/ananke/internal/service"
"scm.bstein.dev/bstein/ananke/internal/ups"
)
type fakeUPSProvider struct {
samples []ups.Sample
errs []error
idx int
}
// Read runs one orchestration or CLI step.
// Signature: (p *fakeUPSProvider) Read(ctx context.Context) (ups.Sample, error).
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
func (p *fakeUPSProvider) Read(_ context.Context) (ups.Sample, error) {
if p.idx < len(p.errs) && p.errs[p.idx] != nil {
err := p.errs[p.idx]
p.idx++
return ups.Sample{}, err
}
if p.idx < len(p.samples) {
s := p.samples[p.idx]
p.idx++
return s, nil
}
if len(p.samples) > 0 {
return p.samples[len(p.samples)-1], nil
}
return ups.Sample{}, errors.New("no sample")
}
// TestBuildUPSTargetsSupportsSingleTarget runs one orchestration or CLI step.
// Signature: TestBuildUPSTargetsSupportsSingleTarget(t *testing.T).
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
func TestBuildUPSTargetsSupportsSingleTarget(t *testing.T) {
cfg := config.Config{
UPS: config.UPS{
Provider: "nut",
Target: "pyrphoros@localhost",
},
}
targets, err := buildUPSTargets(cfg)
if err != nil {
t.Fatalf("buildUPSTargets failed: %v", err)
}
if len(targets) != 1 {
t.Fatalf("expected one target, got %d", len(targets))
}
if targets[0].Name != "primary" || targets[0].Target != "pyrphoros@localhost" {
t.Fatalf("unexpected target: %+v", targets[0])
}
}
// TestBuildUPSTargetsRejectsUnsupportedProvider runs one orchestration or CLI step.
// Signature: TestBuildUPSTargetsRejectsUnsupportedProvider(t *testing.T).
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
func TestBuildUPSTargetsRejectsUnsupportedProvider(t *testing.T) {
cfg := config.Config{UPS: config.UPS{Provider: "unknown"}}
if _, err := buildUPSTargets(cfg); err == nil {
t.Fatalf("expected unsupported provider error")
}
}
// TestBuildUPSTargetsRejectsMissingTarget runs one orchestration or CLI step.
// Signature: TestBuildUPSTargetsRejectsMissingTarget(t *testing.T).
// Why: covers missing-target validation when using the single-target NUT mode.
func TestBuildUPSTargetsRejectsMissingTarget(t *testing.T) {
cfg := config.Config{
UPS: config.UPS{
Provider: "nut",
Target: "",
Targets: nil,
},
}
if _, err := buildUPSTargets(cfg); err == nil {
t.Fatalf("expected missing target error")
}
}
// TestBuildUPSTargetsSupportsNamedTargets runs one orchestration or CLI step.
// Signature: TestBuildUPSTargetsSupportsNamedTargets(t *testing.T).
// Why: covers multi-target expansion including automatic fallback naming.
func TestBuildUPSTargetsSupportsNamedTargets(t *testing.T) {
cfg := config.Config{
UPS: config.UPS{
Provider: "nut",
Targets: []config.UPSTarget{
{Name: "Pyrphoros", Target: "pyrphoros@localhost"},
{Name: "", Target: "statera@localhost"},
},
},
}
targets, err := buildUPSTargets(cfg)
if err != nil {
t.Fatalf("buildUPSTargets failed: %v", err)
}
if len(targets) != 2 {
t.Fatalf("expected two targets, got %d", len(targets))
}
if targets[0].Name != "Pyrphoros" || targets[1].Name != "target-2" {
t.Fatalf("unexpected target names: %+v", targets)
}
}
// TestEnsureStartupPowerSafePassesWhenOnlineAndCharged runs one orchestration or CLI step.
// Signature: TestEnsureStartupPowerSafePassesWhenOnlineAndCharged(t *testing.T).
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
func TestEnsureStartupPowerSafePassesWhenOnlineAndCharged(t *testing.T) {
targets := []service.Target{
{
Name: "Pyrphoros",
Target: "pyrphoros@localhost",
Provider: &fakeUPSProvider{
samples: []ups.Sample{
{OnBattery: false, BatteryCharge: 90, RawStatus: "OL"},
},
},
},
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := ensureStartupPowerSafe(ctx, targets, 20); err != nil {
t.Fatalf("expected startup power safety to pass: %v", err)
}
}
// TestEnsureStartupPowerSafeRejectsOnBattery runs one orchestration or CLI step.
// Signature: TestEnsureStartupPowerSafeRejectsOnBattery(t *testing.T).
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
func TestEnsureStartupPowerSafeRejectsOnBattery(t *testing.T) {
targets := []service.Target{
{
Name: "Statera",
Target: "statera@localhost",
Provider: &fakeUPSProvider{
samples: []ups.Sample{
{OnBattery: true, RuntimeSeconds: 1200, RawStatus: "OB"},
},
},
},
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := ensureStartupPowerSafe(ctx, targets, 20); err == nil {
t.Fatalf("expected startup blocked error while on battery")
}
}
// TestEnsureStartupPowerSafeRejectsLowBatteryCharge runs one orchestration or CLI step.
// Signature: TestEnsureStartupPowerSafeRejectsLowBatteryCharge(t *testing.T).
// Why: covers battery percentage threshold enforcement when node is online.
func TestEnsureStartupPowerSafeRejectsLowBatteryCharge(t *testing.T) {
targets := []service.Target{
{
Name: "Pyrphoros",
Target: "pyrphoros@localhost",
Provider: &fakeUPSProvider{
samples: []ups.Sample{
{OnBattery: false, BatteryCharge: 10, RawStatus: "OL"},
},
},
},
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := ensureStartupPowerSafe(ctx, targets, 20); err == nil {
t.Fatalf("expected low-battery startup rejection")
}
}
// TestEnsureStartupPowerSafeRecoversAfterTransientTelemetryError runs one orchestration or CLI step.
// Signature: TestEnsureStartupPowerSafeRecoversAfterTransientTelemetryError(t *testing.T).
// Why: covers the allSeen gate where a target must produce at least one good sample before pass.
func TestEnsureStartupPowerSafeRecoversAfterTransientTelemetryError(t *testing.T) {
targets := []service.Target{
{
Name: "Statera",
Target: "statera@localhost",
Provider: &fakeUPSProvider{
errs: []error{context.DeadlineExceeded},
samples: []ups.Sample{
{OnBattery: false, BatteryCharge: 95, RawStatus: "OL"},
},
},
},
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ensureStartupPowerSafe(ctx, targets, 20); err != nil {
t.Fatalf("expected startup power safety to recover after transient telemetry error: %v", err)
}
}