Skip to main content

ralph/session/
validation.rs

1//! Session validation and classification helpers.
2//!
3//! Responsibilities:
4//! - Validate persisted sessions against queue state and timeout policy.
5//! - Return explicit recovery classifications for callers.
6//!
7//! Not handled here:
8//! - Session persistence.
9//! - Interactive recovery prompts.
10//!
11//! Invariants/assumptions:
12//! - Sessions are resumable only when the task still exists and is `Doing`.
13//! - Timeout checks use `last_updated_at` when it parses successfully.
14
15use std::path::Path;
16
17use crate::contracts::{QueueFile, SessionState, TaskStatus};
18use crate::timeutil;
19
20use super::persistence::load_session;
21
22#[allow(clippy::large_enum_variant)]
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum SessionValidationResult {
25    Valid(SessionState),
26    NoSession,
27    Stale { reason: String },
28    Timeout { hours: u64, session: SessionState },
29}
30
31pub fn validate_session_with_now(
32    session: &SessionState,
33    queue: &QueueFile,
34    timeout_hours: Option<u64>,
35    now: time::OffsetDateTime,
36) -> SessionValidationResult {
37    let task = match queue
38        .tasks
39        .iter()
40        .find(|task| task.id.trim() == session.task_id)
41    {
42        Some(task) => task,
43        None => {
44            return SessionValidationResult::Stale {
45                reason: format!("Task {} no longer exists in queue", session.task_id),
46            };
47        }
48    };
49
50    if task.status != TaskStatus::Doing {
51        return SessionValidationResult::Stale {
52            reason: format!(
53                "Task {} is not in Doing status (current: {})",
54                session.task_id, task.status
55            ),
56        };
57    }
58
59    if let Some(timeout) = timeout_hours
60        && let Ok(session_time) = timeutil::parse_rfc3339(&session.last_updated_at)
61        && now > session_time
62    {
63        let elapsed = now - session_time;
64        let hours = elapsed.whole_hours() as u64;
65        if hours >= timeout {
66            return SessionValidationResult::Timeout {
67                hours,
68                session: session.clone(),
69            };
70        }
71    }
72
73    SessionValidationResult::Valid(session.clone())
74}
75
76pub fn validate_session(
77    session: &SessionState,
78    queue: &QueueFile,
79    timeout_hours: Option<u64>,
80) -> SessionValidationResult {
81    validate_session_with_now(
82        session,
83        queue,
84        timeout_hours,
85        time::OffsetDateTime::now_utc(),
86    )
87}
88
89pub fn check_session(
90    cache_dir: &Path,
91    queue: &QueueFile,
92    timeout_hours: Option<u64>,
93) -> anyhow::Result<SessionValidationResult> {
94    let session = match load_session(cache_dir)? {
95        Some(session) => session,
96        None => return Ok(SessionValidationResult::NoSession),
97    };
98
99    Ok(validate_session(&session, queue, timeout_hours))
100}