ananke/testing/state/state_quality_test.go

201 lines
6.9 KiB
Go
Raw Normal View History

package statequality
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"scm.bstein.dev/bstein/ananke/internal/state"
)
// TestEnsureDirAndIntentIOContracts runs one orchestration or CLI step.
// Signature: TestEnsureDirAndIntentIOContracts(t *testing.T).
// Why: exercises directory and intent IO edges that appear during interrupted drills.
func TestEnsureDirAndIntentIOContracts(t *testing.T) {
if err := state.EnsureDir(""); err == nil {
t.Fatalf("expected empty dir rejection")
}
root := t.TempDir()
intentPath := filepath.Join(root, "intent", "intent.json")
in := state.Intent{
State: state.IntentStartupInProgress,
Reason: "drill",
Source: "test",
}
if err := state.WriteIntent(intentPath, in); err != nil {
t.Fatalf("write intent: %v", err)
}
got, err := state.ReadIntent(intentPath)
if err != nil {
t.Fatalf("read intent: %v", err)
}
if got.State != in.State || got.Reason != in.Reason || got.Source != in.Source {
t.Fatalf("unexpected intent readback: %+v", got)
}
if got.UpdatedAt.IsZero() {
t.Fatalf("expected UpdatedAt to be auto-filled")
}
missingIntent, err := state.ReadIntent(filepath.Join(root, "missing.json"))
if err != nil {
t.Fatalf("read missing intent: %v", err)
}
if missingIntent != (state.Intent{}) {
t.Fatalf("expected zero intent for missing file, got %+v", missingIntent)
}
emptyPath := filepath.Join(root, "empty-intent.json")
if err := os.WriteFile(emptyPath, nil, 0o644); err != nil {
t.Fatalf("write empty intent: %v", err)
}
emptyIntent, err := state.ReadIntent(emptyPath)
if err != nil {
t.Fatalf("read empty intent: %v", err)
}
if emptyIntent != (state.Intent{}) {
t.Fatalf("expected zero intent for empty file, got %+v", emptyIntent)
}
}
// TestIntentReadAutoHealAndParseIntentOutput runs one orchestration or CLI step.
// Signature: TestIntentReadAutoHealAndParseIntentOutput(t *testing.T).
// Why: validates corruption auto-heal behavior and CLI intent output parser edge handling.
func TestIntentReadAutoHealAndParseIntentOutput(t *testing.T) {
root := t.TempDir()
intentPath := filepath.Join(root, "intent.json")
if err := os.WriteFile(intentPath, []byte(`{"state":"broken"`), 0o644); err != nil {
t.Fatalf("write malformed intent: %v", err)
}
parsed, err := state.ReadIntent(intentPath)
if err != nil {
t.Fatalf("read malformed intent should auto-heal: %v", err)
}
if parsed != (state.Intent{}) {
t.Fatalf("expected healed malformed intent to return zero value, got %+v", parsed)
}
corrupt, err := filepath.Glob(filepath.Join(root, "intent.json.corrupt-*"))
if err != nil {
t.Fatalf("glob corrupt files: %v", err)
}
if len(corrupt) == 0 {
t.Fatalf("expected a quarantined corrupt intent artifact")
}
if _, err := state.ParseIntentOutput("status: ok"); err == nil {
t.Fatalf("expected parse error when intent token is missing")
}
if in, err := state.ParseIntentOutput(`intent=none reason="ignored"`); err != nil || in != (state.Intent{}) {
t.Fatalf("expected none intent to parse as zero value, got %+v err=%v", in, err)
}
_, err = state.ParseIntentOutput(`intent=normal updated_at=not-a-timestamp`)
if err == nil {
t.Fatalf("expected invalid updated_at parse failure")
}
}
// TestLockingAndStoreHistoryContracts runs one orchestration or CLI step.
// Signature: TestLockingAndStoreHistoryContracts(t *testing.T).
// Why: covers lock lifecycle, stale lock recovery, history trimming, and P95 calculations.
func TestLockingAndStoreHistoryContracts(t *testing.T) {
root := t.TempDir()
lockPath := filepath.Join(root, "ananke.lock")
// Active lock should reject acquisition.
active := "pid=" + strconvI(os.Getpid()) + " started=" + time.Now().UTC().Format(time.RFC3339) + "\n"
if err := os.WriteFile(lockPath, []byte(active), 0o600); err != nil {
t.Fatalf("write active lock: %v", err)
}
if _, err := state.AcquireLock(lockPath); err == nil {
t.Fatalf("expected active lock rejection")
}
// Malformed/stale lock should be reclaimed.
if err := os.WriteFile(lockPath, []byte("pid=not-a-number\n"), 0o600); err != nil {
t.Fatalf("write malformed lock: %v", err)
}
unlock, err := state.AcquireLock(lockPath)
if err != nil {
t.Fatalf("acquire reclaimed lock: %v", err)
}
unlock()
if _, err := os.Stat(lockPath); !os.IsNotExist(err) {
t.Fatalf("expected lock file cleanup after unlock, stat err=%v", err)
}
storePath := filepath.Join(root, "runs.json")
st := state.New(storePath)
for i := 0; i < 205; i++ {
rec := state.RunRecord{
ID: "run-" + strconvI(i),
Action: "shutdown",
StartedAt: time.Now().UTC().Add(-time.Duration(i+1) * time.Second),
EndedAt: time.Now().UTC(),
DurationSeconds: i + 1,
Success: true,
}
if err := st.Append(rec); err != nil {
t.Fatalf("append record %d: %v", i, err)
}
}
records, err := st.Load()
if err != nil {
t.Fatalf("load records: %v", err)
}
if len(records) != 200 {
t.Fatalf("expected history trim to 200 records, got %d", len(records))
}
if got := st.ShutdownP95(30); got <= 30 {
t.Fatalf("expected computed p95 above default, got %d", got)
}
if got := st.ShutdownP95ByReasonPrefix(77, 3, []string{"missing-prefix"}); got != 77 {
t.Fatalf("expected default fallback when no reason matches, got %d", got)
}
// Corrupt run history should be auto-healed to an empty list.
if err := os.WriteFile(storePath, []byte("{bad-json"), 0o640); err != nil {
t.Fatalf("write corrupt history: %v", err)
}
recovered, err := st.Load()
if err != nil {
t.Fatalf("load corrupt history should auto-heal: %v", err)
}
if len(recovered) != 0 {
t.Fatalf("expected healed corrupt history to return empty list, got %d", len(recovered))
}
// Directory path triggers read error; shutdown estimator should fall back to default.
dirAsFile := filepath.Join(root, "history-dir")
if err := os.MkdirAll(dirAsFile, 0o755); err != nil {
t.Fatalf("mkdir history-dir: %v", err)
}
brokenStore := state.New(dirAsFile)
if got := brokenStore.ShutdownP95(123); got != 123 {
t.Fatalf("expected default on read error, got %d", got)
}
}
// TestMustWriteIntentValidation runs one orchestration or CLI step.
// Signature: TestMustWriteIntentValidation(t *testing.T).
// Why: covers strict intent-state validation guarantees for operator CLI writes.
func TestMustWriteIntentValidation(t *testing.T) {
path := filepath.Join(t.TempDir(), "intent.json")
if err := state.MustWriteIntent(path, state.IntentShutdownComplete, "done", "test"); err != nil {
t.Fatalf("expected valid intent write, got %v", err)
}
err := state.MustWriteIntent(path, "unknown-state", "nope", "test")
if err == nil || !strings.Contains(err.Error(), "invalid intent state") {
t.Fatalf("expected invalid intent state error, got %v", err)
}
}
// strconvI runs one orchestration or CLI step.
// Signature: strconvI(v int) string.
// Why: avoids importing strconv repeatedly in compact coverage-focused tests.
func strconvI(v int) string {
return fmt.Sprintf("%d", v)
}