201 lines
6.5 KiB
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)
|
|
}
|
|
}
|