223 lines
6.8 KiB
Go
223 lines
6.8 KiB
Go
package state
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestAcquireLockLifecycle runs one orchestration or CLI step.
|
|
// Signature: TestAcquireLockLifecycle(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestAcquireLockLifecycle(t *testing.T) {
|
|
lockPath := filepath.Join(t.TempDir(), "ananke.lock")
|
|
unlock, err := AcquireLock(lockPath)
|
|
if err != nil {
|
|
t.Fatalf("acquire lock: %v", err)
|
|
}
|
|
if _, err := os.Stat(lockPath); err != nil {
|
|
t.Fatalf("expected lock file to exist: %v", err)
|
|
}
|
|
unlock()
|
|
if _, err := os.Stat(lockPath); !os.IsNotExist(err) {
|
|
t.Fatalf("expected lock file to be removed, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestAcquireLockReclaimsStaleLock runs one orchestration or CLI step.
|
|
// Signature: TestAcquireLockReclaimsStaleLock(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestAcquireLockReclaimsStaleLock(t *testing.T) {
|
|
lockPath := filepath.Join(t.TempDir(), "ananke.lock")
|
|
if err := os.WriteFile(lockPath, []byte("pid=999999\n"), 0o600); err != nil {
|
|
t.Fatalf("write stale lock: %v", err)
|
|
}
|
|
|
|
unlock, err := AcquireLock(lockPath)
|
|
if err != nil {
|
|
t.Fatalf("acquire lock with stale predecessor: %v", err)
|
|
}
|
|
defer unlock()
|
|
|
|
b, err := os.ReadFile(lockPath)
|
|
if err != nil {
|
|
t.Fatalf("read lock: %v", err)
|
|
}
|
|
if !strings.Contains(string(b), "pid=") {
|
|
t.Fatalf("expected lock content to contain pid, got %q", string(b))
|
|
}
|
|
}
|
|
|
|
// TestAcquireLockRejectsActiveLock runs one orchestration or CLI step.
|
|
// Signature: TestAcquireLockRejectsActiveLock(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestAcquireLockRejectsActiveLock(t *testing.T) {
|
|
lockPath := filepath.Join(t.TempDir(), "ananke.lock")
|
|
active := "pid=" + strconv.Itoa(os.Getpid()) + "\n"
|
|
if err := os.WriteFile(lockPath, []byte(active), 0o600); err != nil {
|
|
t.Fatalf("write active lock: %v", err)
|
|
}
|
|
|
|
if _, err := AcquireLock(lockPath); err == nil {
|
|
t.Fatalf("expected acquire lock to fail when active pid holds lock")
|
|
}
|
|
}
|
|
|
|
// TestStoreLoadAutoHealsCorruptJSON runs one orchestration or CLI step.
|
|
// Signature: TestStoreLoadAutoHealsCorruptJSON(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestStoreLoadAutoHealsCorruptJSON(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "runs.json")
|
|
if err := os.WriteFile(p, []byte(`{"bad":`), 0o640); err != nil {
|
|
t.Fatalf("write corrupt run history: %v", err)
|
|
}
|
|
|
|
records, err := New(p).Load()
|
|
if err != nil {
|
|
t.Fatalf("load with auto-heal: %v", err)
|
|
}
|
|
if len(records) != 0 {
|
|
t.Fatalf("expected no records after heal, got %d", len(records))
|
|
}
|
|
raw, err := os.ReadFile(p)
|
|
if err != nil {
|
|
t.Fatalf("read healed runs file: %v", err)
|
|
}
|
|
if string(raw) != "[]\n" {
|
|
t.Fatalf("expected healed runs payload '[]', got %q", string(raw))
|
|
}
|
|
matches, err := filepath.Glob(filepath.Join(dir, "runs.json.corrupt-*"))
|
|
if err != nil {
|
|
t.Fatalf("glob backup files: %v", err)
|
|
}
|
|
if len(matches) != 1 {
|
|
t.Fatalf("expected 1 backup file, got %d (%v)", len(matches), matches)
|
|
}
|
|
}
|
|
|
|
// TestShutdownP95WithMinSamplesFallsBackWhenHistorySparse runs one orchestration or CLI step.
|
|
// Signature: TestShutdownP95WithMinSamplesFallsBackWhenHistorySparse(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestShutdownP95WithMinSamplesFallsBackWhenHistorySparse(t *testing.T) {
|
|
p := filepath.Join(t.TempDir(), "runs.json")
|
|
records := []RunRecord{
|
|
{
|
|
ID: "shutdown-1",
|
|
Action: "shutdown",
|
|
Reason: "manual",
|
|
StartedAt: time.Now().UTC(),
|
|
EndedAt: time.Now().UTC(),
|
|
DurationSeconds: 45,
|
|
Success: true,
|
|
},
|
|
}
|
|
b, err := json.Marshal(records)
|
|
if err != nil {
|
|
t.Fatalf("marshal records: %v", err)
|
|
}
|
|
if err := os.WriteFile(p, b, 0o640); err != nil {
|
|
t.Fatalf("write records: %v", err)
|
|
}
|
|
|
|
got := New(p).ShutdownP95WithMinSamples(300, 3)
|
|
if got != 300 {
|
|
t.Fatalf("expected fallback default 300 with sparse history, got %d", got)
|
|
}
|
|
}
|
|
|
|
// TestShutdownP95ByReasonPrefixFiltersSamples runs one orchestration or CLI step.
|
|
// Signature: TestShutdownP95ByReasonPrefixFiltersSamples(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestShutdownP95ByReasonPrefixFiltersSamples(t *testing.T) {
|
|
p := filepath.Join(t.TempDir(), "runs.json")
|
|
now := time.Now().UTC()
|
|
records := []RunRecord{
|
|
{
|
|
ID: "shutdown-1",
|
|
Action: "shutdown",
|
|
Reason: "ups-threshold target=Pyrphoros",
|
|
StartedAt: now,
|
|
EndedAt: now,
|
|
DurationSeconds: 180,
|
|
Success: true,
|
|
},
|
|
{
|
|
ID: "shutdown-2",
|
|
Action: "shutdown",
|
|
Reason: "ups-threshold target=Pyrphoros",
|
|
StartedAt: now,
|
|
EndedAt: now,
|
|
DurationSeconds: 220,
|
|
Success: true,
|
|
},
|
|
{
|
|
ID: "shutdown-3",
|
|
Action: "shutdown",
|
|
Reason: "manual-maintenance",
|
|
StartedAt: now,
|
|
EndedAt: now,
|
|
DurationSeconds: 1200,
|
|
Success: true,
|
|
},
|
|
}
|
|
b, err := json.Marshal(records)
|
|
if err != nil {
|
|
t.Fatalf("marshal records: %v", err)
|
|
}
|
|
if err := os.WriteFile(p, b, 0o640); err != nil {
|
|
t.Fatalf("write records: %v", err)
|
|
}
|
|
|
|
got := New(p).ShutdownP95ByReasonPrefix(420, 2, []string{"ups-"})
|
|
if got != 220 {
|
|
t.Fatalf("expected filtered p95 of 220, got %d", got)
|
|
}
|
|
}
|
|
|
|
// TestShutdownP95IgnoresDryRunSamples runs one orchestration or CLI step.
|
|
// Signature: TestShutdownP95IgnoresDryRunSamples(t *testing.T).
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
|
func TestShutdownP95IgnoresDryRunSamples(t *testing.T) {
|
|
p := filepath.Join(t.TempDir(), "runs.json")
|
|
now := time.Now().UTC()
|
|
records := []RunRecord{
|
|
{
|
|
ID: "shutdown-dryrun",
|
|
Action: "shutdown",
|
|
Reason: "ups-threshold target=Pyrphoros",
|
|
DryRun: true,
|
|
StartedAt: now,
|
|
EndedAt: now,
|
|
DurationSeconds: 5,
|
|
Success: true,
|
|
},
|
|
{
|
|
ID: "shutdown-real",
|
|
Action: "shutdown",
|
|
Reason: "ups-threshold target=Pyrphoros",
|
|
DryRun: false,
|
|
StartedAt: now,
|
|
EndedAt: now,
|
|
DurationSeconds: 210,
|
|
Success: true,
|
|
},
|
|
}
|
|
b, err := json.Marshal(records)
|
|
if err != nil {
|
|
t.Fatalf("marshal records: %v", err)
|
|
}
|
|
if err := os.WriteFile(p, b, 0o640); err != nil {
|
|
t.Fatalf("write records: %v", err)
|
|
}
|
|
|
|
got := New(p).ShutdownP95ByReasonPrefix(420, 1, []string{"ups-"})
|
|
if got != 210 {
|
|
t.Fatalf("expected dry-run sample to be ignored and real value 210 used, got %d", got)
|
|
}
|
|
}
|