ananke/testing/state/state_quality_additional_test.go

217 lines
8.9 KiB
Go
Raw Normal View History

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)
}
}