ananke/cmd/ananke/command_handlers_status_error_test.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")
}
}