diff --git a/internal/service/daemon_coverage_closeout_test.go b/internal/service/daemon_coverage_closeout_test.go new file mode 100644 index 0000000..441fc76 --- /dev/null +++ b/internal/service/daemon_coverage_closeout_test.go @@ -0,0 +1,242 @@ +package service + +import ( + "context" + "io" + "log" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "scm.bstein.dev/bstein/ananke/internal/config" + "scm.bstein.dev/bstein/ananke/internal/metrics" + "scm.bstein.dev/bstein/ananke/internal/ups" +) + +// TestDaemonRunMetricsEnabledBranchError runs one orchestration or CLI step. +// Signature: TestDaemonRunMetricsEnabledBranchError(t *testing.T). +// Why: covers Run path where metrics are enabled and startup fails before the ticker loop. +func TestDaemonRunMetricsEnabledBranchError(t *testing.T) { + stateDir := t.TempDir() + orch := newDaemonTestOrchestrator(t, stateDir) + d := &Daemon{ + cfg: config.Config{ + UPS: config.UPS{ + Enabled: true, + PollSeconds: 1, + RuntimeSafetyFactor: 1.0, + DebounceCount: 1, + }, + Metrics: config.Metrics{ + Enabled: true, + BindAddr: "", + Path: "/metrics", + }, + State: config.State{ + IntentPath: filepath.Join(stateDir, "intent.json"), + }, + }, + orch: orch, + targets: []Target{ + { + Name: "Pyrphoros", + Target: "pyrphoros@localhost", + Provider: &daemonFakeProvider{ + samples: []ups.Sample{{OnBattery: false, RuntimeSeconds: 10000, RawStatus: "OL"}}, + }, + }, + }, + log: log.New(io.Discard, "", 0), + exporter: metrics.New(), + } + err := d.Run(context.Background()) + if err == nil || !strings.Contains(err.Error(), "metrics.bind_addr must not be empty") { + t.Fatalf("expected metrics bind error from Run metrics-enabled path, got %v", err) + } +} + +// TestDaemonRunProviderErrorContinueBranch runs one orchestration or CLI step. +// Signature: TestDaemonRunProviderErrorContinueBranch(t *testing.T). +// Why: covers provider-read error continue branch where telemetry timeout does not trigger. +func TestDaemonRunProviderErrorContinueBranch(t *testing.T) { + stateDir := t.TempDir() + orch := newDaemonTestOrchestrator(t, stateDir) + d := &Daemon{ + cfg: config.Config{ + UPS: config.UPS{ + Enabled: true, + PollSeconds: 1, + RuntimeSafetyFactor: 1.0, + DebounceCount: 3, + TelemetryTimeoutSeconds: 120, + }, + State: config.State{ + IntentPath: filepath.Join(stateDir, "intent.json"), + }, + }, + orch: orch, + targets: []Target{ + { + Name: "Statera", + Target: "statera@localhost", + Provider: &daemonFakeProvider{ + errs: []error{context.DeadlineExceeded, context.DeadlineExceeded, context.DeadlineExceeded}, + }, + }, + }, + log: log.New(io.Discard, "", 0), + exporter: metrics.New(), + } + ctx, cancel := context.WithTimeout(context.Background(), 1200*time.Millisecond) + defer cancel() + if err := d.Run(ctx); err == nil { + t.Fatalf("expected context cancellation while exercising provider-error continue path") + } +} + +// TestTriggerShutdownForwardedIntentWriteFailureWarningBranch runs one orchestration or CLI step. +// Signature: TestTriggerShutdownForwardedIntentWriteFailureWarningBranch(t *testing.T). +// Why: covers triggerShutdown branch where forwarded completion intent write fails but trigger still succeeds. +func TestTriggerShutdownForwardedIntentWriteFailureWarningBranch(t *testing.T) { + tmp := t.TempDir() + sshPath := filepath.Join(tmp, "ssh") + if err := os.WriteFile(sshPath, []byte("#!/usr/bin/env bash\nset -euo pipefail\necho forwarded\n"), 0o755); err != nil { + t.Fatalf("write fake ssh: %v", err) + } + t.Setenv("PATH", tmp+":"+os.Getenv("PATH")) + + intentPath := filepath.Join(tmp, "intent-dir") + if err := os.MkdirAll(intentPath, 0o755); err != nil { + t.Fatalf("mkdir intent dir: %v", err) + } + + d := &Daemon{ + cfg: config.Config{ + SSHUser: "atlas", + SSHPort: 2277, + State: config.State{ + IntentPath: intentPath, + }, + Coordination: config.Coordination{ + ForwardShutdownHost: "titan-db", + ForwardShutdownConfig: "/etc/ananke/ananke.yaml", + CommandTimeoutSeconds: 3, + }, + }, + log: log.New(io.Discard, "", 0), + exporter: metrics.New(), + } + if err := d.triggerShutdown(context.Background(), "forwarded-intent-write-fail"); err != nil { + t.Fatalf("expected forwarded shutdown to succeed even when completion intent write fails, got %v", err) + } +} + +// TestTriggerShutdownLocalIntentWriteFailureWarningBranch runs one orchestration or CLI step. +// Signature: TestTriggerShutdownLocalIntentWriteFailureWarningBranch(t *testing.T). +// Why: covers triggerShutdown local completion intent warning branch when write fails after successful shutdown. +func TestTriggerShutdownLocalIntentWriteFailureWarningBranch(t *testing.T) { + stateDir := t.TempDir() + orch := newDaemonTestOrchestrator(t, stateDir) + + intentPath := filepath.Join(stateDir, "intent-dir") + if err := os.MkdirAll(intentPath, 0o755); err != nil { + t.Fatalf("mkdir intent dir: %v", err) + } + + d := &Daemon{ + cfg: config.Config{ + State: config.State{ + IntentPath: intentPath, + }, + Shutdown: config.Shutdown{ + EmergencySkipDrain: true, + EmergencySkipEtcd: true, + }, + }, + orch: orch, + log: log.New(io.Discard, "", 0), + exporter: metrics.New(), + } + if err := d.triggerShutdown(context.Background(), "local-intent-write-fail"); err != nil { + t.Fatalf("expected local shutdown success even when completion intent write fails, got %v", err) + } +} + +// TestForwardShutdownDefaultTimeoutBranch runs one orchestration or CLI step. +// Signature: TestForwardShutdownDefaultTimeoutBranch(t *testing.T). +// Why: covers forwardShutdown default command-timeout branch when timeout config is unset. +func TestForwardShutdownDefaultTimeoutBranch(t *testing.T) { + tmp := t.TempDir() + sshPath := filepath.Join(tmp, "ssh") + if err := os.WriteFile(sshPath, []byte("#!/usr/bin/env bash\nset -euo pipefail\necho ok\n"), 0o755); err != nil { + t.Fatalf("write fake ssh: %v", err) + } + t.Setenv("PATH", tmp+":"+os.Getenv("PATH")) + + d := &Daemon{ + cfg: config.Config{ + SSHUser: "atlas", + Coordination: config.Coordination{ + ForwardShutdownHost: "titan-db", + ForwardShutdownConfig: "/etc/ananke/ananke.yaml", + CommandTimeoutSeconds: 0, + }, + }, + log: log.New(io.Discard, "", 0), + } + if err := d.forwardShutdown(context.Background(), "default-timeout-branch"); err != nil { + t.Fatalf("expected forward shutdown with default timeout to succeed, got %v", err) + } +} + +// TestForwardShutdownKnownHostsRepairIncludesJumpHost runs one orchestration or CLI step. +// Signature: TestForwardShutdownKnownHostsRepairIncludesJumpHost(t *testing.T). +// Why: covers known-host repair host-list branch that appends the configured SSH jump host. +func TestForwardShutdownKnownHostsRepairIncludesJumpHost(t *testing.T) { + tmp := t.TempDir() + attemptMarker := filepath.Join(tmp, "attempt") + sshPath := filepath.Join(tmp, "ssh") + script := `#!/usr/bin/env bash +set -euo pipefail +marker="` + attemptMarker + `" +if [[ ! -f "$marker" ]]; then + echo "REMOTE HOST IDENTIFICATION HAS CHANGED!" >&2 + touch "$marker" + exit 255 +fi +echo "forwarded" +` + if err := os.WriteFile(sshPath, []byte(script), 0o755); err != nil { + t.Fatalf("write fake ssh: %v", err) + } + sshKeygenPath := filepath.Join(tmp, "ssh-keygen") + if err := os.WriteFile(sshKeygenPath, []byte("#!/usr/bin/env bash\nset -euo pipefail\nexit 0\n"), 0o755); err != nil { + t.Fatalf("write fake ssh-keygen: %v", err) + } + sshKeyscanPath := filepath.Join(tmp, "ssh-keyscan") + if err := os.WriteFile(sshKeyscanPath, []byte("#!/usr/bin/env bash\nset -euo pipefail\necho fake-key\n"), 0o755); err != nil { + t.Fatalf("write fake ssh-keyscan: %v", err) + } + t.Setenv("PATH", tmp+":"+os.Getenv("PATH")) + + d := &Daemon{ + cfg: config.Config{ + SSHUser: "atlas", + SSHPort: 2277, + SSHConfigFile: filepath.Join(tmp, "ssh-config"), + SSHIdentityFile: filepath.Join(tmp, "id_ed25519"), + SSHJumpHost: "titan-jh", + Coordination: config.Coordination{ + ForwardShutdownHost: "titan-db", + ForwardShutdownConfig: "/etc/ananke/ananke.yaml", + CommandTimeoutSeconds: 3, + }, + }, + log: log.New(io.Discard, "", 0), + } + if err := d.forwardShutdown(context.Background(), "repair-includes-jump-host"); err != nil { + t.Fatalf("expected forward shutdown to recover after known-host repair with jump host, got %v", err) + } +}