Skip to main content

ralph/
queue.rs

1//! Task queue persistence, validation, and pruning.
2//!
3//! Responsibilities:
4//! - Load, save, and validate queue files in JSON format.
5//! - Provide operations for moving completed tasks and pruning history.
6//! - Own queue-level helpers such as ID generation and validation.
7//!
8//! Not handled here:
9//! - Directory lock acquisition (see `crate::lock`).
10//! - CLI parsing or user interaction.
11//! - Runner integration or external command execution.
12//!
13//! Invariants/assumptions:
14//! - Queue files conform to the schema in `crate::contracts`.
15//! - Callers hold locks when mutating queue state on disk.
16
17// Existing submodules (unchanged)
18pub mod graph;
19pub mod hierarchy;
20pub mod operations;
21pub mod prune;
22pub mod repair;
23pub mod search;
24pub mod size_check;
25pub mod validation;
26
27// New split modules
28mod backup;
29mod id;
30mod json_repair;
31mod loader;
32mod save;
33
34// Re-exports from existing submodules
35pub use graph::*;
36pub use operations::*;
37pub use prune::{PruneOptions, PruneReport, prune_done_tasks};
38pub use repair::*;
39pub use search::{
40    SearchOptions, filter_tasks, fuzzy_search_tasks, search_tasks, search_tasks_with_options,
41};
42pub use size_check::{
43    SizeCheckResult, check_queue_size, count_threshold_or_default, print_size_warning_if_needed,
44    size_threshold_or_default,
45};
46pub use validation::{ValidationWarning, log_warnings, validate_queue, validate_queue_set};
47
48// Re-exports from new modules.
49pub use backup::backup_queue;
50pub use id::next_id_across;
51pub use id::{format_id, normalize_prefix};
52pub use json_repair::attempt_json_repair;
53pub use loader::{
54    load_and_validate_queues, load_queue, load_queue_or_default, load_queue_with_repair,
55    load_queue_with_repair_and_validate, repair_and_validate_queues,
56};
57pub use save::save_queue;
58
59use crate::lock;
60use anyhow::Result;
61use std::path::Path;
62
63pub fn acquire_queue_lock(repo_root: &Path, label: &str, force: bool) -> Result<lock::DirLock> {
64    let lock_dir = lock::queue_lock_dir(repo_root);
65    lock::acquire_dir_lock(&lock_dir, label, force)
66}
67
68// Tests that exercise the facade re-exports
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::contracts::{Task, TaskStatus};
73    use std::collections::HashMap;
74
75    fn task(id: &str) -> Task {
76        Task {
77            id: id.to_string(),
78            status: TaskStatus::Todo,
79            title: "Test task".to_string(),
80            description: None,
81            priority: Default::default(),
82            tags: vec!["code".to_string()],
83            scope: vec!["crates/ralph".to_string()],
84            evidence: vec!["observed".to_string()],
85            plan: vec!["do thing".to_string()],
86            notes: vec![],
87            request: Some("test request".to_string()),
88            agent: None,
89            created_at: Some("2026-01-18T00:00:00Z".to_string()),
90            updated_at: Some("2026-01-18T00:00:00Z".to_string()),
91            completed_at: None,
92            started_at: None,
93            scheduled_start: None,
94            depends_on: vec![],
95            blocks: vec![],
96            relates_to: vec![],
97            duplicates: None,
98            custom_fields: HashMap::new(),
99            parent_id: None,
100            estimated_minutes: None,
101            actual_minutes: None,
102        }
103    }
104
105    #[test]
106    fn task_defaults_to_medium_priority() {
107        use crate::contracts::TaskPriority;
108        let task = task("RQ-0001");
109        assert_eq!(task.priority, TaskPriority::Medium);
110    }
111
112    #[test]
113    fn priority_ord_implements_correct_ordering() {
114        use crate::contracts::TaskPriority;
115        assert!(TaskPriority::Critical > TaskPriority::High);
116        assert!(TaskPriority::High > TaskPriority::Medium);
117        assert!(TaskPriority::Medium > TaskPriority::Low);
118    }
119
120    #[test]
121    fn attempt_json_repair_fixes_trailing_comma_in_array() {
122        let input = r#"{"tasks": [{"id": "RQ-0001", "tags": ["a", "b",]}]}"#;
123        let repaired = attempt_json_repair(input).expect("should repair");
124        assert!(repaired.contains("\"tags\": [\"a\", \"b\"]"));
125        assert!(!repaired.contains("\"b\","));
126    }
127
128    #[test]
129    fn attempt_json_repair_fixes_trailing_comma_in_object() {
130        let input = r#"{"tasks": [{"id": "RQ-0001", "title": "Test",}]}"#;
131        let repaired = attempt_json_repair(input).expect("should repair");
132        assert!(repaired.contains("\"title\": \"Test\"}"));
133        assert!(!repaired.contains("\"Test\","));
134    }
135
136    #[test]
137    fn attempt_json_repair_returns_none_for_valid_json() {
138        let input = r#"{"tasks": [{"id": "RQ-0001", "title": "Test"}]}"#;
139        assert!(attempt_json_repair(input).is_none());
140    }
141}