ralph-agent-loop 0.4.0

A Rust CLI for managing AI agent loops with a structured JSON task queue
Documentation
//! Lock unit tests.
//!
//! Responsibilities:
//! - Cover split lock helpers that are easiest to exercise without integration harnesses.
//!
//! Not handled here:
//! - Multi-process integration coverage in `crates/ralph/tests/`.
//!
//! Invariants/assumptions:
//! - Current-process PID should be observable on supported platforms.

use super::stale::{LockStalenessAdvisory, classify_lock_owner_at, format_lock_error};
use super::*;

fn test_owner(started_at: &str) -> LockOwner {
    LockOwner {
        pid: 42,
        started_at: started_at.to_string(),
        command: "ralph run loop".to_string(),
        label: "run loop".to_string(),
    }
}

#[test]
fn pid_is_running_current_process() {
    let current_pid = std::process::id();
    assert_eq!(pid_is_running(current_pid), Some(true));
}

#[test]
fn pid_is_running_nonexistent_pid_never_reports_running() {
    assert_ne!(pid_is_running(0xFFFF_FFFE), Some(true));
}

#[test]
fn pid_is_running_system_idle_is_not_definitively_dead() {
    assert_ne!(pid_is_running(0), Some(false));
}

#[test]
fn is_task_owner_file_matches_expected_patterns() {
    assert!(is_task_owner_file("owner_task_1234"));
    assert!(is_task_owner_file("owner_task_1234_0"));
    assert!(is_task_owner_file("owner_task_1234_42"));
    assert!(!is_task_owner_file("owner"));
    assert!(!is_task_owner_file("owner_other"));
    assert!(!is_task_owner_file("owner_task"));
    assert!(!is_task_owner_file(""));
    assert!(!is_task_owner_file("task_owner_1234"));
}

#[test]
fn pid_liveness_helpers_are_consistent() {
    assert!(PidLiveness::NotRunning.is_definitely_not_running());
    assert!(!PidLiveness::Running.is_definitely_not_running());
    assert!(!PidLiveness::Indeterminate.is_definitely_not_running());

    assert!(PidLiveness::Running.is_running_or_indeterminate());
    assert!(PidLiveness::Indeterminate.is_running_or_indeterminate());
    assert!(!PidLiveness::NotRunning.is_running_or_indeterminate());
}

#[test]
fn pid_liveness_wraps_pid_is_running() {
    assert_eq!(pid_liveness(std::process::id()), PidLiveness::Running);
    assert_ne!(pid_liveness(0xFFFF_FFFE), PidLiveness::Running);
}

#[test]
fn lock_staleness_only_auto_stales_definitely_dead_pid() {
    let now = crate::timeutil::parse_rfc3339("2026-04-17T00:00:00Z").unwrap();
    let owner = test_owner("not-a-timestamp");

    let staleness = classify_lock_owner_at(&owner, now, PidLiveness::NotRunning);

    assert!(staleness.is_stale());
    assert_eq!(staleness.advisory, LockStalenessAdvisory::None);
}

#[test]
fn lock_staleness_flags_aged_live_pid_for_review_without_auto_stale() {
    let now = crate::timeutil::parse_rfc3339("2026-04-17T00:00:00Z").unwrap();
    let owner = test_owner("2026-04-09T00:00:00Z");

    let staleness = classify_lock_owner_at(&owner, now, PidLiveness::Running);

    assert!(!staleness.is_stale());
    assert_eq!(staleness.advisory, LockStalenessAdvisory::AgedLivePid);
}

#[test]
fn lock_staleness_flags_unclear_owner_time_for_review_without_auto_stale() {
    let now = crate::timeutil::parse_rfc3339("2026-04-17T00:00:00Z").unwrap();

    let invalid = classify_lock_owner_at(&test_owner("unknown"), now, PidLiveness::Indeterminate);
    assert!(!invalid.is_stale());
    assert_eq!(invalid.advisory, LockStalenessAdvisory::InvalidStartedAt);

    let future = classify_lock_owner_at(
        &test_owner("2026-04-17T00:06:00Z"),
        now,
        PidLiveness::Running,
    );
    assert!(!future.is_stale());
    assert_eq!(future.advisory, LockStalenessAdvisory::FutureStartedAt);
}

#[test]
fn lock_error_explains_pid_reuse_review_policy() {
    let now = crate::timeutil::parse_rfc3339("2026-04-17T00:00:00Z").unwrap();
    let owner = test_owner("2026-04-09T00:00:00Z");
    let staleness = classify_lock_owner_at(&owner, now, PidLiveness::Running);

    let message = format_lock_error(
        std::path::Path::new("/tmp/ralph-lock"),
        Some(&owner),
        staleness.is_stale(),
        false,
        Some(staleness),
    );

    assert!(message.contains("PID REUSE REVIEW"));
    assert!(message.contains("Ralph does not auto-clear it"));
    assert!(message.contains("verify the PID, command, and timestamp"));
}