157 lines
5.2 KiB
Go
157 lines
5.2 KiB
Go
package state
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestEnsureDirRejectsEmpty runs one orchestration or CLI step.
|
|
// Signature: TestEnsureDirRejectsEmpty(t *testing.T).
|
|
// Why: covers explicit guard branch for empty state directory inputs.
|
|
func TestEnsureDirRejectsEmpty(t *testing.T) {
|
|
if err := EnsureDir(""); err == nil {
|
|
t.Fatalf("expected empty directory error")
|
|
}
|
|
}
|
|
|
|
// TestStoreAppendTrimToMaxRecords runs one orchestration or CLI step.
|
|
// Signature: TestStoreAppendTrimToMaxRecords(t *testing.T).
|
|
// Why: covers retention branch that trims run history to the 200-record cap.
|
|
func TestStoreAppendTrimToMaxRecords(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "runs.json")
|
|
s := New(path)
|
|
now := time.Now().UTC()
|
|
for i := 0; i < 205; i++ {
|
|
if err := s.Append(RunRecord{
|
|
ID: "r-" + strconv.Itoa(i),
|
|
Action: "shutdown",
|
|
StartedAt: now,
|
|
EndedAt: now,
|
|
DurationSeconds: i + 1,
|
|
Success: true,
|
|
}); err != nil {
|
|
t.Fatalf("append %d failed: %v", i, err)
|
|
}
|
|
}
|
|
recs, err := s.Load()
|
|
if err != nil {
|
|
t.Fatalf("load failed: %v", err)
|
|
}
|
|
if len(recs) != 200 {
|
|
t.Fatalf("expected trim to 200 records, got %d", len(recs))
|
|
}
|
|
}
|
|
|
|
// TestStoreLoadHandlesEmptyFile runs one orchestration or CLI step.
|
|
// Signature: TestStoreLoadHandlesEmptyFile(t *testing.T).
|
|
// Why: covers load branch for empty existing run-history file.
|
|
func TestStoreLoadHandlesEmptyFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "runs.json")
|
|
if err := os.WriteFile(path, nil, 0o640); err != nil {
|
|
t.Fatalf("write empty file: %v", err)
|
|
}
|
|
recs, err := New(path).Load()
|
|
if err != nil {
|
|
t.Fatalf("load empty file: %v", err)
|
|
}
|
|
if len(recs) != 0 {
|
|
t.Fatalf("expected no records, got %d", len(recs))
|
|
}
|
|
}
|
|
|
|
// TestStoreLoadReturnsErrorOnUnhealableDecode runs one orchestration or CLI step.
|
|
// Signature: TestStoreLoadReturnsErrorOnUnhealableDecode(t *testing.T).
|
|
// Why: covers decode failure path where replacement write itself can fail.
|
|
func TestStoreLoadReturnsErrorOnUnhealableDecode(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "runs.json")
|
|
if err := os.WriteFile(path, []byte("{bad-json"), 0o640); err != nil {
|
|
t.Fatalf("write invalid file: %v", err)
|
|
}
|
|
// Make directory readonly so quarantine replacement cannot be written.
|
|
if err := os.Chmod(dir, 0o500); err != nil {
|
|
t.Fatalf("chmod dir readonly: %v", err)
|
|
}
|
|
defer os.Chmod(dir, 0o700)
|
|
if _, err := New(path).Load(); err == nil {
|
|
t.Fatalf("expected load failure when auto-heal cannot write replacement")
|
|
}
|
|
}
|
|
|
|
// TestShutdownP95FallsBackOnLoadError runs one orchestration or CLI step.
|
|
// Signature: TestShutdownP95FallsBackOnLoadError(t *testing.T).
|
|
// Why: covers load-error fallback branch in percentile helper.
|
|
func TestShutdownP95FallsBackOnLoadError(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "runs.json")
|
|
if err := os.WriteFile(path, []byte("{bad"), 0o640); err != nil {
|
|
t.Fatalf("write invalid file: %v", err)
|
|
}
|
|
// Use impossible perms to force read failure.
|
|
if err := os.Chmod(path, 0o000); err != nil {
|
|
t.Fatalf("chmod file: %v", err)
|
|
}
|
|
defer os.Chmod(path, 0o640)
|
|
if got := New(path).ShutdownP95(321); got != 321 {
|
|
t.Fatalf("expected fallback default 321, got %d", got)
|
|
}
|
|
}
|
|
|
|
// TestShutdownP95ReturnsDefaultOnNonPositiveQuantile runs one orchestration or CLI step.
|
|
// Signature: TestShutdownP95ReturnsDefaultOnNonPositiveQuantile(t *testing.T).
|
|
// Why: covers branch where computed percentile record is non-positive.
|
|
func TestShutdownP95ReturnsDefaultOnNonPositiveQuantile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "runs.json")
|
|
now := time.Now().UTC()
|
|
records := []RunRecord{
|
|
{Action: "shutdown", StartedAt: now, EndedAt: now, DurationSeconds: 0, Success: true},
|
|
{Action: "shutdown", StartedAt: now, EndedAt: now, DurationSeconds: -1, Success: true},
|
|
}
|
|
b, err := json.Marshal(records)
|
|
if err != nil {
|
|
t.Fatalf("marshal records: %v", err)
|
|
}
|
|
if err := os.WriteFile(path, b, 0o640); err != nil {
|
|
t.Fatalf("write records: %v", err)
|
|
}
|
|
if got := New(path).ShutdownP95WithMinSamples(777, 1); got != 777 {
|
|
t.Fatalf("expected default 777, got %d", got)
|
|
}
|
|
}
|
|
|
|
// TestStaleLockHelpers runs one orchestration or CLI step.
|
|
// Signature: TestStaleLockHelpers(t *testing.T).
|
|
// Why: covers stale-lock parser branches directly for reliability.
|
|
func TestStaleLockHelpers(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
missing := filepath.Join(tmp, "missing.lock")
|
|
stale, err := staleLock(missing)
|
|
if err != nil || !stale {
|
|
t.Fatalf("expected missing lock to be stale=true err=nil, got stale=%v err=%v", stale, err)
|
|
}
|
|
|
|
invalidPID := filepath.Join(tmp, "invalid.lock")
|
|
if err := os.WriteFile(invalidPID, []byte("pid=notanumber\n"), 0o600); err != nil {
|
|
t.Fatalf("write invalid pid lock: %v", err)
|
|
}
|
|
stale, err = staleLock(invalidPID)
|
|
if err != nil || !stale {
|
|
t.Fatalf("expected invalid pid lock to be stale=true err=nil, got stale=%v err=%v", stale, err)
|
|
}
|
|
|
|
active := filepath.Join(tmp, "active.lock")
|
|
if err := os.WriteFile(active, []byte("pid="+strconv.Itoa(os.Getpid())+"\n"), 0o600); err != nil {
|
|
t.Fatalf("write active lock: %v", err)
|
|
}
|
|
stale, err = staleLock(active)
|
|
if err != nil {
|
|
t.Fatalf("active staleLock error: %v", err)
|
|
}
|
|
if stale {
|
|
t.Fatalf("expected active lock to report stale=false")
|
|
}
|
|
}
|