package statequality import ( "errors" "os" "path/filepath" "strings" "testing" "time" "scm.bstein.dev/bstein/ananke/internal/state" ) // TestStateIntentErrorAndParseBranches runs one orchestration or CLI step. // Signature: TestStateIntentErrorAndParseBranches(t *testing.T). // Why: covers non-happy-path intent branches that show up during interrupted drill recovery. func TestStateIntentErrorAndParseBranches(t *testing.T) { root := t.TempDir() // ReadIntent should surface non-not-exist read errors. if _, err := state.ReadIntent(root); err == nil { t.Fatalf("expected read error when intent path is a directory") } // WriteIntent should fail when parent path cannot be created as a directory. parentFile := filepath.Join(root, "parent-file") if err := os.WriteFile(parentFile, []byte("x"), 0o600); err != nil { t.Fatalf("write parent marker: %v", err) } if err := state.WriteIntent(filepath.Join(parentFile, "intent.json"), state.Intent{State: state.IntentNormal}); err == nil { t.Fatalf("expected write failure when parent is not a directory") } ts := time.Now().UTC().Truncate(time.Second) raw := "prefix text intent=startup_in_progress reason=\"manual\" source=peer updated_at=" + ts.Format(time.RFC3339) in, err := state.ParseIntentOutput(raw) if err != nil { t.Fatalf("parse intent output: %v", err) } if in.State != state.IntentStartupInProgress || in.Reason != "manual" || in.Source != "peer" || !in.UpdatedAt.Equal(ts) { t.Fatalf("unexpected parsed intent: %+v", in) } } // TestStateStoreLockAndEstimatorBranches runs one orchestration or CLI step. // Signature: TestStateStoreLockAndEstimatorBranches(t *testing.T). // Why: exercises stale-lock error handling and estimator filtering behavior. func TestStateStoreLockAndEstimatorBranches(t *testing.T) { root := t.TempDir() // AcquireLock should fail when lock parent cannot be created. parentFile := filepath.Join(root, "lock-parent") if err := os.WriteFile(parentFile, []byte("x"), 0o600); err != nil { t.Fatalf("write lock parent marker: %v", err) } if _, err := state.AcquireLock(filepath.Join(parentFile, "ananke.lock")); err == nil { t.Fatalf("expected acquire lock to fail when parent path is a file") } // AcquireLock should report stale-lock check failure when lock file cannot be read. lockDir := filepath.Join(root, "locks") if err := os.MkdirAll(lockDir, 0o755); err != nil { t.Fatalf("mkdir lock dir: %v", err) } lockPath := filepath.Join(lockDir, "ananke.lock") lockTargetDir := filepath.Join(lockDir, "lock-target") if err := os.MkdirAll(lockTargetDir, 0o755); err != nil { t.Fatalf("mkdir lock target dir: %v", err) } if err := os.Symlink(lockTargetDir, lockPath); err != nil { t.Fatalf("create lock symlink to directory: %v", err) } if _, err := state.AcquireLock(lockPath); err == nil || !strings.Contains(err.Error(), "existing lock check failed") { t.Fatalf("expected stale lock check failure, got %v", err) } storePath := filepath.Join(root, "runs.json") st := state.New(storePath) records := []state.RunRecord{ {ID: "a", Action: "shutdown", Reason: "ups-threshold alpha", StartedAt: time.Now().Add(-10 * time.Second), EndedAt: time.Now(), DurationSeconds: 40, Success: true}, {ID: "b", Action: "shutdown", Reason: "UPS-THRESHOLD beta", StartedAt: time.Now().Add(-10 * time.Second), EndedAt: time.Now(), DurationSeconds: 50, Success: true}, } for _, rec := range records { if err := st.Append(rec); err != nil { t.Fatalf("append record: %v", err) } } if got := st.ShutdownP95WithMinSamples(99, 0); got <= 0 { t.Fatalf("expected computed p95 when minSamples<=0 branch is normalized, got %d", got) } if got := st.ShutdownP95ByReasonPrefix(77, 1, []string{" ups-threshold "}); got < 40 { t.Fatalf("expected prefix-filtered p95 to include matching records, got %d", got) } } // TestStateAutoHealFailurePath runs one orchestration or CLI step. // Signature: TestStateAutoHealFailurePath(t *testing.T). // Why: verifies corrupt-state auto-heal reports a useful error when filesystem permissions block quarantine. func TestStateAutoHealFailurePath(t *testing.T) { root := t.TempDir() path := filepath.Join(root, "runs.json") if err := os.WriteFile(path, []byte("{invalid-json"), 0o600); err != nil { t.Fatalf("write corrupt runs file: %v", err) } restore := state.TestHookSetQuarantineCorruptFileOverride(func(path string, payload []byte, replacement []byte, mode os.FileMode) error { return errors.New("forced quarantine failure") }) t.Cleanup(restore) _, err := state.New(path).Load() if err == nil { t.Fatalf("expected auto-heal failure when directory is not writable") } if !strings.Contains(err.Error(), "auto-heal failed") { t.Fatalf("expected auto-heal failure detail, got %v", err) } } // TestStateQuarantineHookBranches runs one orchestration or CLI step. // Signature: TestStateQuarantineHookBranches(t *testing.T). // Why: closes remaining corruption-heal branches so drill recovery file repair stays reliable. func TestStateQuarantineHookBranches(t *testing.T) { root := t.TempDir() // Success path with backup+replacement writes. path := filepath.Join(root, "ok", "intent.json") if err := state.TestHookQuarantineCorruptFile(path, []byte("{bad"), []byte("{}\n"), 0o640); err != nil { t.Fatalf("expected successful quarantine write, got %v", err) } matches, err := filepath.Glob(path + ".corrupt-*") if err != nil { t.Fatalf("glob corrupt backups: %v", err) } if len(matches) == 0 { t.Fatalf("expected a .corrupt backup artifact") } // Parent is a file -> mkdir branch fails. parentFile := filepath.Join(root, "parent-as-file") if err := os.WriteFile(parentFile, []byte("x"), 0o600); err != nil { t.Fatalf("write parent file: %v", err) } err = state.TestHookQuarantineCorruptFile(filepath.Join(parentFile, "intent.json"), []byte("bad"), []byte("{}\n"), 0o640) if err == nil { t.Fatalf("expected mkdir failure when parent is a file") } // Long filename forces backup write failure branch regardless of root privileges. longName := strings.Repeat("a", 245) backupFailPath := filepath.Join(root, "long-name", longName) err = state.TestHookQuarantineCorruptFile(backupFailPath, []byte("bad"), []byte("{}\n"), 0o640) if err == nil || !strings.Contains(err.Error(), "write backup") { t.Fatalf("expected backup write failure for long-name backup path, got %v", err) } } // TestStateIntentParseAndWriteEdgeBranches runs one orchestration or CLI step. // Signature: TestStateIntentParseAndWriteEdgeBranches(t *testing.T). // Why: exercises parser and write edge paths that can otherwise hide intent-state drift. func TestStateIntentParseAndWriteEdgeBranches(t *testing.T) { root := t.TempDir() // Parse branch where reason quote is malformed but token still parses. parsed, err := state.ParseIntentOutput(`intent=startup_in_progress reason="unterminated source=peer`) if err != nil { t.Fatalf("parse with malformed reason quote: %v", err) } if parsed.State != state.IntentStartupInProgress { t.Fatalf("unexpected parsed state: %+v", parsed) } // Parse branch with blank state token should normalize to zero intent. blank, err := state.ParseIntentOutput(`intent= source=peer`) if err != nil { t.Fatalf("parse blank intent token: %v", err) } if blank != (state.Intent{}) { t.Fatalf("expected zero intent for blank state token, got %+v", blank) } // WriteIntent branch with parent path not directory. parentFile := filepath.Join(root, "bad-parent") if err := os.WriteFile(parentFile, []byte("x"), 0o600); err != nil { t.Fatalf("write bad parent marker: %v", err) } if err := state.WriteIntent(filepath.Join(parentFile, "intent.json"), state.Intent{State: state.IntentNormal}); err == nil { t.Fatalf("expected WriteIntent error for invalid parent path") } } // TestStateStoreAppendAndAcquireBranches runs one orchestration or CLI step. // Signature: TestStateStoreAppendAndAcquireBranches(t *testing.T). // Why: covers append error and active-lock branches used by CLI locking semantics. func TestStateStoreAppendAndAcquireBranches(t *testing.T) { root := t.TempDir() // Append should fail when parent path cannot be created as a directory. parentFile := filepath.Join(root, "runs-parent") if err := os.WriteFile(parentFile, []byte("x"), 0o600); err != nil { t.Fatalf("write append parent marker: %v", err) } st := state.New(filepath.Join(parentFile, "runs.json")) if err := st.Append(state.RunRecord{ID: "x", Action: "shutdown", StartedAt: time.Now(), EndedAt: time.Now(), DurationSeconds: 1, Success: true}); err == nil { t.Fatalf("expected append error when parent path is not a directory") } // AcquireLock active process branch: pid=1 should be treated as active on Linux. lockPath := filepath.Join(root, "active.lock") if err := os.WriteFile(lockPath, []byte("pid=1 started=1970-01-01T00:00:00Z\n"), 0o600); err != nil { t.Fatalf("write active lock: %v", err) } if _, err := state.AcquireLock(lockPath); err == nil || !strings.Contains(err.Error(), "active process") { t.Fatalf("expected active lock rejection, got %v", err) } }