ananke/internal/cluster/orchestrator_report_test.go

231 lines
7.1 KiB
Go

package cluster
import (
"encoding/json"
"io"
"log"
"os"
"path/filepath"
"strings"
"testing"
"time"
"scm.bstein.dev/bstein/ananke/internal/config"
"scm.bstein.dev/bstein/ananke/internal/state"
)
// TestSanitizeReportFileName runs one orchestration or CLI step.
// Signature: TestSanitizeReportFileName(t *testing.T).
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
func TestSanitizeReportFileName(t *testing.T) {
if got := sanitizeReportFileName("startup-123"); got != "startup-123" {
t.Fatalf("expected startup-123, got %q", got)
}
if got := sanitizeReportFileName(" run id / weird "); got != "run_id___weird" {
t.Fatalf("unexpected sanitized value: %q", got)
}
if got := sanitizeReportFileName(" "); got != "run" {
t.Fatalf("expected fallback run, got %q", got)
}
}
// TestWriteRunRecordArtifactWritesShutdownLatest runs one orchestration or CLI step.
// Signature: TestWriteRunRecordArtifactWritesShutdownLatest(t *testing.T).
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
func TestWriteRunRecordArtifactWritesShutdownLatest(t *testing.T) {
tmp := t.TempDir()
orch := &Orchestrator{
cfg: config.Config{
State: config.State{
Dir: tmp,
ReportsDir: filepath.Join(tmp, "reports"),
},
},
log: log.New(io.Discard, "", 0),
}
rec := state.RunRecord{
ID: "shutdown-123",
Action: "shutdown",
Reason: "test",
DurationSeconds: 12,
StartedAt: time.Now().UTC().Add(-12 * time.Second),
EndedAt: time.Now().UTC(),
Success: true,
}
if err := orch.writeRunRecordArtifact(rec); err != nil {
t.Fatalf("write run artifact failed: %v", err)
}
archived := filepath.Join(tmp, "reports", "shutdown-123.json")
if _, err := os.Stat(archived); err != nil {
t.Fatalf("expected archived run file %s: %v", archived, err)
}
latest := filepath.Join(tmp, "last-shutdown-report.json")
if _, err := os.Stat(latest); err != nil {
t.Fatalf("expected last shutdown report %s: %v", latest, err)
}
}
// TestFinalizeStartupReportWritesLastAndArchive runs one orchestration or CLI step.
// Signature: TestFinalizeStartupReportWritesLastAndArchive(t *testing.T).
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
func TestFinalizeStartupReportWritesLastAndArchive(t *testing.T) {
tmp := t.TempDir()
orch := &Orchestrator{
cfg: config.Config{
State: config.State{
Dir: tmp,
ReportsDir: filepath.Join(tmp, "reports"),
},
},
log: log.New(io.Discard, "", 0),
}
orch.beginStartupReport("drill")
orch.noteStartupCheck("node-inventory", true, "ok")
orch.finalizeStartupReport(nil)
lastPath := filepath.Join(tmp, "last-startup-report.json")
if _, err := os.Stat(lastPath); err != nil {
t.Fatalf("expected last startup report %s: %v", lastPath, err)
}
entries, err := os.ReadDir(filepath.Join(tmp, "reports"))
if err != nil {
t.Fatalf("read report archive: %v", err)
}
found := false
for _, entry := range entries {
if strings.HasPrefix(entry.Name(), "startup-report-") && strings.HasSuffix(entry.Name(), ".json") {
found = true
break
}
}
if !found {
t.Fatalf("expected archived startup report under %s", filepath.Join(tmp, "reports"))
}
}
// TestBeginStartupReportWritesProgressSnapshot runs one orchestration or CLI step.
// Signature: TestBeginStartupReportWritesProgressSnapshot(t *testing.T).
// Why: status polling during startup depends on a live progress file that exists
// before the final report is written.
func TestBeginStartupReportWritesProgressSnapshot(t *testing.T) {
tmp := t.TempDir()
orch := &Orchestrator{
cfg: config.Config{
State: config.State{
Dir: tmp,
ReportsDir: filepath.Join(tmp, "reports"),
},
},
log: log.New(io.Discard, "", 0),
}
orch.beginStartupReport("drill")
path := filepath.Join(tmp, "startup-progress.json")
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read startup progress: %v", err)
}
var report startupReport
if err := json.Unmarshal(b, &report); err != nil {
t.Fatalf("decode startup progress: %v", err)
}
if report.Status != "running" {
t.Fatalf("expected running status, got %q", report.Status)
}
if report.Phase == "" {
t.Fatalf("expected non-empty phase")
}
}
// TestSetStartupPhaseWritesProgressSnapshot runs one orchestration or CLI step.
// Signature: TestSetStartupPhaseWritesProgressSnapshot(t *testing.T).
// Why: startup phase changes should be externally visible without waiting for completion.
func TestSetStartupPhaseWritesProgressSnapshot(t *testing.T) {
tmp := t.TempDir()
orch := &Orchestrator{
cfg: config.Config{
State: config.State{
Dir: tmp,
ReportsDir: filepath.Join(tmp, "reports"),
},
},
log: log.New(io.Discard, "", 0),
}
orch.beginStartupReport("drill")
orch.setStartupPhase("flux-resume", "resuming flux")
path := filepath.Join(tmp, "startup-progress.json")
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read startup progress: %v", err)
}
var report startupReport
if err := json.Unmarshal(b, &report); err != nil {
t.Fatalf("decode startup progress: %v", err)
}
if report.Phase != "flux-resume" {
t.Fatalf("expected updated phase, got %q", report.Phase)
}
check, ok := report.Checks["phase"]
if !ok {
t.Fatalf("expected phase check entry in progress report")
}
if check.Status != "running" {
t.Fatalf("expected running phase check status, got %q", check.Status)
}
}
// TestFinalizeStartupReportWritesFailedProgressSnapshot runs one orchestration or CLI step.
// Signature: TestFinalizeStartupReportWritesFailedProgressSnapshot(t *testing.T).
// Why: failures must remain visible through the progress file so operators can
// read the terminal state from a single status path.
func TestFinalizeStartupReportWritesFailedProgressSnapshot(t *testing.T) {
tmp := t.TempDir()
orch := &Orchestrator{
cfg: config.Config{
State: config.State{
Dir: tmp,
ReportsDir: filepath.Join(tmp, "reports"),
},
},
log: log.New(io.Discard, "", 0),
}
orch.beginStartupReport("drill")
orch.finalizeStartupReport(assertErr("boom"))
path := filepath.Join(tmp, "startup-progress.json")
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read startup progress: %v", err)
}
var report startupReport
if err := json.Unmarshal(b, &report); err != nil {
t.Fatalf("decode startup progress: %v", err)
}
if report.Status != "failed" {
t.Fatalf("expected failed status, got %q", report.Status)
}
if report.Error != "boom" {
t.Fatalf("expected error message boom, got %q", report.Error)
}
}
// assertErr runs one orchestration or CLI step.
// Signature: assertErr(message string) error.
// Why: keeps error fixtures explicit without repeating inline helper closures.
func assertErr(message string) error {
return &startupTestErr{msg: message}
}
type startupTestErr struct {
msg string
}
// Error runs one orchestration or CLI step.
// Signature: (e *startupTestErr) Error() string.
// Why: keeps deterministic error strings in startup report tests concise.
func (e *startupTestErr) Error() string {
return e.msg
}