231 lines
7.1 KiB
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
|
|
}
|