package state import ( "encoding/json" "os" "path/filepath" "strconv" "strings" "testing" "time" ) 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) } } 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)) } } 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") } } 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) } } 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) } } 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) } } 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) } }