1pub 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
27mod backup;
29mod id;
30mod json_repair;
31mod loader;
32mod save;
33
34pub 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
48pub 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#[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}