245 lines
8.9 KiB
Go
245 lines
8.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"scm.bstein.dev/bstein/ananke/internal/cluster"
|
|
"scm.bstein.dev/bstein/ananke/internal/config"
|
|
"scm.bstein.dev/bstein/ananke/internal/service"
|
|
"scm.bstein.dev/bstein/ananke/internal/state"
|
|
)
|
|
|
|
// TestRunDaemonErrorBranches runs one orchestration or CLI step.
|
|
// Signature: TestRunDaemonErrorBranches(t *testing.T).
|
|
// Why: covers daemon configuration and runtime error branches.
|
|
func TestRunDaemonErrorBranches(t *testing.T) {
|
|
restore := stubCommandHandlerHooks()
|
|
defer restore()
|
|
logger := log.New(io.Discard, "", 0)
|
|
|
|
cfgPath := writeTestConfig(t)
|
|
buildOrchestratorCommand = func(_ *log.Logger, _ string, dryRun bool) (config.Config, *cluster.Orchestrator, error) {
|
|
cfg, err := config.Load(cfgPath)
|
|
if err != nil {
|
|
return config.Config{}, nil, err
|
|
}
|
|
cfg.UPS.Enabled = false
|
|
return cfg, newTestOrchestrator(cfg, dryRun), nil
|
|
}
|
|
if err := runDaemon(logger, []string{"--config", cfgPath}); err == nil {
|
|
t.Fatalf("expected daemon UPS-disabled error")
|
|
}
|
|
|
|
restore = stubCommandHandlerHooks()
|
|
defer restore()
|
|
buildOrchestratorCommand = func(_ *log.Logger, _ string, dryRun bool) (config.Config, *cluster.Orchestrator, error) {
|
|
cfg, err := config.Load(cfgPath)
|
|
if err != nil {
|
|
return config.Config{}, nil, err
|
|
}
|
|
cfg.UPS.Enabled = true
|
|
return cfg, newTestOrchestrator(cfg, dryRun), nil
|
|
}
|
|
buildUPSTargetsCommand = func(_ config.Config) ([]service.Target, error) {
|
|
return nil, errors.New("targets failed")
|
|
}
|
|
if err := runDaemon(logger, []string{"--config", cfgPath}); err == nil {
|
|
t.Fatalf("expected buildUPSTargets error")
|
|
}
|
|
|
|
restore = stubCommandHandlerHooks()
|
|
defer restore()
|
|
buildOrchestratorCommand = func(_ *log.Logger, _ string, dryRun bool) (config.Config, *cluster.Orchestrator, error) {
|
|
cfg, err := config.Load(cfgPath)
|
|
if err != nil {
|
|
return config.Config{}, nil, err
|
|
}
|
|
cfg.UPS.Enabled = true
|
|
return cfg, newTestOrchestrator(cfg, dryRun), nil
|
|
}
|
|
buildUPSTargetsCommand = func(_ config.Config) ([]service.Target, error) {
|
|
return []service.Target{{Name: "Pyrphoros", Target: "pyrphoros@localhost"}}, nil
|
|
}
|
|
daemonRunCommand = func(_ context.Context, _ *service.Daemon) error {
|
|
return errors.New("daemon crashed")
|
|
}
|
|
if err := runDaemon(logger, []string{"--config", cfgPath}); err == nil {
|
|
t.Fatalf("expected daemon runtime error")
|
|
}
|
|
}
|
|
|
|
// TestRunIntentErrorBranches runs one orchestration or CLI step.
|
|
// Signature: TestRunIntentErrorBranches(t *testing.T).
|
|
// Why: covers intent read error and intent write error branches.
|
|
func TestRunIntentErrorBranches(t *testing.T) {
|
|
restore := stubCommandHandlerHooks()
|
|
defer restore()
|
|
cfgPath := writeTestConfig(t)
|
|
logger := log.New(io.Discard, "", 0)
|
|
|
|
readIntentCommand = func(_ string) (state.Intent, error) {
|
|
return state.Intent{}, errors.New("read intent failed")
|
|
}
|
|
if err := runIntent(logger, []string{"--config", cfgPath}); err == nil {
|
|
t.Fatalf("expected read intent error")
|
|
}
|
|
|
|
writeIntentCommand = func(_ string, _, _, _ string) error {
|
|
return errors.New("write failed")
|
|
}
|
|
if err := runIntent(logger, []string{"--config", cfgPath, "--set", state.IntentNormal, "--execute"}); err == nil {
|
|
t.Fatalf("expected write intent error")
|
|
}
|
|
}
|
|
|
|
// TestRunShutdownAndEtcdRestoreBuildError runs one orchestration or CLI step.
|
|
// Signature: TestRunShutdownAndEtcdRestoreBuildError(t *testing.T).
|
|
// Why: covers orchestrator build failure propagation in shutdown and restore handlers.
|
|
func TestRunShutdownAndEtcdRestoreBuildError(t *testing.T) {
|
|
restore := stubCommandHandlerHooks()
|
|
defer restore()
|
|
buildOrchestratorCommand = func(_ *log.Logger, _ string, _ bool) (config.Config, *cluster.Orchestrator, error) {
|
|
return config.Config{}, nil, errors.New("build failed")
|
|
}
|
|
logger := log.New(io.Discard, "", 0)
|
|
if err := runShutdown(logger, []string{"--config", "/ignored"}); err == nil {
|
|
t.Fatalf("expected shutdown build error")
|
|
}
|
|
if err := runEtcdRestore(logger, []string{"--config", "/ignored"}); err == nil {
|
|
t.Fatalf("expected etcd restore build error")
|
|
}
|
|
if err := runStatus(logger, []string{"--config", "/ignored"}); err == nil {
|
|
t.Fatalf("expected status build error")
|
|
}
|
|
}
|
|
|
|
// TestLoadStartupStatusSnapshotReadError runs one orchestration or CLI step.
|
|
// Signature: TestLoadStartupStatusSnapshotReadError(t *testing.T).
|
|
// Why: covers file read errors for startup status snapshots.
|
|
func TestLoadStartupStatusSnapshotReadError(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "startup-progress.json")
|
|
if err := os.MkdirAll(path, 0o755); err != nil {
|
|
t.Fatalf("mkdir unexpected progress path: %v", err)
|
|
}
|
|
_, _, err := loadStartupStatusSnapshot(tmp)
|
|
if err == nil {
|
|
t.Fatalf("expected snapshot read error")
|
|
}
|
|
}
|
|
|
|
// TestRunStatusNoSnapshotIntentReadError runs one orchestration or CLI step.
|
|
// Signature: TestRunStatusNoSnapshotIntentReadError(t *testing.T).
|
|
// Why: covers status output when no startup report exists and intent read fails.
|
|
func TestRunStatusNoSnapshotIntentReadError(t *testing.T) {
|
|
restore := stubCommandHandlerHooks()
|
|
defer restore()
|
|
|
|
cfg := minimalHandlerConfig(t)
|
|
cfgPath := writeTestConfig(t)
|
|
buildOrchestratorCommand = func(_ *log.Logger, _ string, dryRun bool) (config.Config, *cluster.Orchestrator, error) {
|
|
return cfg, newTestOrchestrator(cfg, dryRun), nil
|
|
}
|
|
readIntentCommand = func(_ string) (state.Intent, error) {
|
|
return state.Intent{}, errors.New("intent unavailable")
|
|
}
|
|
var loggerOut strings.Builder
|
|
logger := log.New(&loggerOut, "", 0)
|
|
if err := runStatus(logger, []string{"--config", cfgPath}); err != nil {
|
|
t.Fatalf("runStatus failed: %v", err)
|
|
}
|
|
if !strings.Contains(loggerOut.String(), "intent_read_error=") {
|
|
t.Fatalf("expected intent read error in output, got:\n%s", loggerOut.String())
|
|
}
|
|
}
|
|
|
|
// TestRunStatusDerivesFallbackStatusFields runs one orchestration or CLI step.
|
|
// Signature: TestRunStatusDerivesFallbackStatusFields(t *testing.T).
|
|
// Why: covers status/phase fallback derivation for snapshots missing explicit status fields.
|
|
func TestRunStatusDerivesFallbackStatusFields(t *testing.T) {
|
|
restore := stubCommandHandlerHooks()
|
|
defer restore()
|
|
|
|
cfg := minimalHandlerConfig(t)
|
|
cfgPath := writeTestConfig(t)
|
|
buildOrchestratorCommand = func(_ *log.Logger, _ string, dryRun bool) (config.Config, *cluster.Orchestrator, error) {
|
|
return cfg, newTestOrchestrator(cfg, dryRun), nil
|
|
}
|
|
progress := startupStatusSnapshot{
|
|
StartedAt: time.Now().UTC().Add(-10 * time.Second),
|
|
Checks: map[string]startupCheckRecord{
|
|
"phase": {Status: "running", Detail: "waiting"},
|
|
},
|
|
}
|
|
writeStartupStatusFixture(t, filepath.Join(cfg.State.Dir, "startup-progress.json"), progress)
|
|
|
|
var loggerOut strings.Builder
|
|
logger := log.New(&loggerOut, "", 0)
|
|
if err := runStatus(logger, []string{"--config", cfgPath}); err != nil {
|
|
t.Fatalf("runStatus failed: %v", err)
|
|
}
|
|
out := loggerOut.String()
|
|
if !strings.Contains(out, "startup_status=running") {
|
|
t.Fatalf("expected derived running status, got:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "startup_phase=unknown") {
|
|
t.Fatalf("expected derived unknown phase fallback, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// TestRunStatusDerivesTerminalFailedStatus runs one orchestration or CLI step.
|
|
// Signature: TestRunStatusDerivesTerminalFailedStatus(t *testing.T).
|
|
// Why: covers derived failed status when a completed report omits explicit status.
|
|
func TestRunStatusDerivesTerminalFailedStatus(t *testing.T) {
|
|
restore := stubCommandHandlerHooks()
|
|
defer restore()
|
|
|
|
cfg := minimalHandlerConfig(t)
|
|
cfgPath := writeTestConfig(t)
|
|
buildOrchestratorCommand = func(_ *log.Logger, _ string, dryRun bool) (config.Config, *cluster.Orchestrator, error) {
|
|
return cfg, newTestOrchestrator(cfg, dryRun), nil
|
|
}
|
|
report := startupStatusSnapshot{
|
|
StartedAt: time.Now().UTC().Add(-2 * time.Minute),
|
|
Completed: time.Now().UTC(),
|
|
Success: false,
|
|
Checks: map[string]startupCheckRecord{},
|
|
}
|
|
writeStartupStatusFixture(t, filepath.Join(cfg.State.Dir, "last-startup-report.json"), report)
|
|
|
|
var loggerOut strings.Builder
|
|
logger := log.New(&loggerOut, "", 0)
|
|
if err := runStatus(logger, []string{"--config", cfgPath}); err != nil {
|
|
t.Fatalf("runStatus failed: %v", err)
|
|
}
|
|
if !strings.Contains(loggerOut.String(), "startup_status=failed") {
|
|
t.Fatalf("expected derived failed status, got:\n%s", loggerOut.String())
|
|
}
|
|
}
|
|
|
|
// TestLoadStartupStatusSnapshotInitializesChecks runs one orchestration or CLI step.
|
|
// Signature: TestLoadStartupStatusSnapshotInitializesChecks(t *testing.T).
|
|
// Why: covers nil-check-map initialization path in snapshot loading.
|
|
func TestLoadStartupStatusSnapshotInitializesChecks(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "startup-progress.json")
|
|
if err := os.WriteFile(path, []byte(`{"status":"running"}`), 0o644); err != nil {
|
|
t.Fatalf("write snapshot: %v", err)
|
|
}
|
|
snapshot, _, err := loadStartupStatusSnapshot(tmp)
|
|
if err != nil {
|
|
t.Fatalf("load snapshot failed: %v", err)
|
|
}
|
|
if snapshot == nil || snapshot.Checks == nil {
|
|
t.Fatalf("expected snapshot with initialized checks map")
|
|
}
|
|
}
|