package service import ( "context" "io" "log" "path/filepath" "strings" "testing" "scm.bstein.dev/bstein/ananke/internal/config" "scm.bstein.dev/bstein/ananke/internal/metrics" "scm.bstein.dev/bstein/ananke/internal/state" ) // TestDaemonRunRejectsDisabledUPS runs one orchestration or CLI step. // Signature: TestDaemonRunRejectsDisabledUPS(t *testing.T). // Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve. func TestDaemonRunRejectsDisabledUPS(t *testing.T) { d := &Daemon{ cfg: config.Config{ UPS: config.UPS{Enabled: false}, }, log: log.New(io.Discard, "", 0), } if err := d.Run(context.Background()); err == nil { t.Fatalf("expected UPS-disabled run to fail") } } // TestDaemonRunRejectsMissingTargets runs one orchestration or CLI step. // Signature: TestDaemonRunRejectsMissingTargets(t *testing.T). // Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve. func TestDaemonRunRejectsMissingTargets(t *testing.T) { d := &Daemon{ cfg: config.Config{ UPS: config.UPS{Enabled: true}, }, log: log.New(io.Discard, "", 0), } if err := d.Run(context.Background()); err == nil { t.Fatalf("expected empty-target run to fail") } } // TestDaemonTargetList runs one orchestration or CLI step. // Signature: TestDaemonTargetList(t *testing.T). // Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve. func TestDaemonTargetList(t *testing.T) { d := &Daemon{ targets: []Target{ {Name: "Pyrphoros", Target: "pyrphoros@localhost"}, {Name: "Statera", Target: "statera@localhost"}, }, } got := d.targetList() if !strings.Contains(got, "Pyrphoros=pyrphoros@localhost") || !strings.Contains(got, "Statera=statera@localhost") { t.Fatalf("unexpected target list: %q", got) } } // TestDaemonResolveSSHPathsPreferConfigured runs one orchestration or CLI step. // Signature: TestDaemonResolveSSHPathsPreferConfigured(t *testing.T). // Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve. func TestDaemonResolveSSHPathsPreferConfigured(t *testing.T) { d := &Daemon{ cfg: config.Config{ SSHConfigFile: "/tmp/custom-ssh-config", SSHIdentityFile: "/tmp/custom-ssh-key", }, } if got := d.resolveSSHConfigFile(); got != "/tmp/custom-ssh-config" { t.Fatalf("unexpected config path: %q", got) } if got := d.resolveSSHIdentityFile(); got != "/tmp/custom-ssh-key" { t.Fatalf("unexpected identity path: %q", got) } } // TestStartMetricsServerRequiresBindAddress runs one orchestration or CLI step. // Signature: TestStartMetricsServerRequiresBindAddress(t *testing.T). // Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve. func TestStartMetricsServerRequiresBindAddress(t *testing.T) { d := &Daemon{ cfg: config.Config{ Metrics: config.Metrics{ Enabled: true, BindAddr: "", Path: "/metrics", }, }, log: log.New(io.Discard, "", 0), exporter: nil, } d.exporter = d.ensureExporterForTest() if err := d.startMetricsServer(); err == nil { t.Fatalf("expected missing bind address error") } } // TestTriggerShutdownSkipsDuplicateWhenIntentActive runs one orchestration or CLI step. // Signature: TestTriggerShutdownSkipsDuplicateWhenIntentActive(t *testing.T). // Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve. func TestTriggerShutdownSkipsDuplicateWhenIntentActive(t *testing.T) { tmp := t.TempDir() intentPath := filepath.Join(tmp, "intent.json") if err := state.MustWriteIntent(intentPath, state.IntentShuttingDown, "already-running", "test"); err != nil { t.Fatalf("seed intent: %v", err) } d := &Daemon{ cfg: config.Config{ State: config.State{ IntentPath: intentPath, }, }, log: log.New(io.Discard, "", 0), exporter: nil, } d.exporter = d.ensureExporterForTest() if err := d.triggerShutdown(context.Background(), "duplicate-check"); err != nil { t.Fatalf("expected duplicate shutdown trigger to be ignored: %v", err) } } // ensureExporterForTest runs one orchestration or CLI step. // Signature: (d *Daemon) ensureExporterForTest() *metrics.Exporter. // Why: local helper keeps setup concise while preserving explicit behavior in each test. func (d *Daemon) ensureExporterForTest() *metrics.Exporter { if d.exporter == nil { d.exporter = metrics.New() } return d.exporter }