oxicode/store/
todo_state.rs1use std::pin::Pin;
8use std::sync::Arc;
9
10use oxicode_agent::tools::todo::{TodoOp, TodoPhase, TodoUpdateResult};
11use oxicode_agent::tools::{TodoStateProvider, ToolError};
12use parking_lot::RwLock;
13
14#[derive(Debug)]
19pub struct TodoState {
20 phases: RwLock<Vec<TodoPhase>>,
21}
22
23impl TodoState {
24 pub fn new() -> Self {
26 Self {
27 phases: RwLock::new(Vec::new()),
28 }
29 }
30
31 pub fn with_phases(phases: Vec<TodoPhase>) -> Self {
33 Self {
34 phases: RwLock::new(phases),
35 }
36 }
37
38 pub fn get_phases(&self) -> Vec<TodoPhase> {
40 self.phases.read().clone()
41 }
42
43 pub fn apply(&self, ops: Vec<TodoOp>) -> TodoUpdateResult {
45 let mut phases = self.phases.write();
46 let result = oxicode_agent::tools::todo::apply_ops(&mut phases, &ops);
47 drop(phases);
49 result
50 }
51
52 #[allow(dead_code)]
54 pub fn clear(&self) {
55 self.phases.write().clear();
56 }
57}
58
59impl Default for TodoState {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl TodoStateProvider for TodoState {
66 fn get_phases(&self) -> Vec<TodoPhase> {
67 TodoState::get_phases(self)
68 }
69
70 fn apply_ops<'a>(
71 &'a self,
72 ops: Vec<TodoOp>,
73 ) -> Pin<Box<dyn Future<Output = Result<TodoUpdateResult, ToolError>> + Send + 'a>> {
74 Box::pin(async move {
75 let mut phases = self.phases.write();
77 Ok(oxicode_agent::tools::todo::apply_ops(&mut phases, &ops))
78 })
79 }
80}
81
82pub fn provider_from_state(state: Arc<TodoState>) -> Arc<dyn TodoStateProvider> {
84 state as Arc<dyn TodoStateProvider>
85}
86
87#[cfg(test)]
90mod tests {
91 use super::*;
92 use oxicode_agent::tools::todo::TodoItem;
93
94 #[test]
95 fn empty_state() {
96 let state = TodoState::new();
97 assert!(state.get_phases().is_empty());
98 }
99
100 #[test]
101 fn apply_ops_via_provider() {
102 let state = TodoState::new();
103 let provider: Arc<dyn TodoStateProvider> = provider_from_state(Arc::new(state));
104
105 let phases = provider.get_phases();
107 assert!(phases.is_empty());
108
109 let state_arc = Arc::new(TodoState::new());
112 let result = state_arc.apply(vec![TodoOp::Init {
113 list: None,
114 items: Some(vec!["task1".into(), "task2".into()]),
115 }]);
116 assert!(result.errors.is_empty());
117 let phases = state_arc.get_phases();
118 assert_eq!(phases.len(), 1);
119 assert_eq!(phases[0].tasks.len(), 2);
120 }
121
122 #[test]
123 fn provider_share_state() {
124 let state = Arc::new(TodoState::with_phases(vec![]));
126 let p1: Arc<dyn TodoStateProvider> = state.clone();
127 let p2: Arc<dyn TodoStateProvider> = state.clone();
128
129 let _ = p1.get_phases(); let _ = p2.get_phases();
131 assert!(state.get_phases().is_empty());
132 }
133
134 #[test]
135 fn with_phases_initial() {
136 let initial = vec![TodoPhase {
137 name: "P".into(),
138 tasks: vec![TodoItem {
139 content: "t".into(),
140 status: oxicode_agent::tools::todo::TodoStatus::Pending,
141 notes: None,
142 block_reason: None,
143 }],
144 }];
145 let state = TodoState::with_phases(initial);
146 assert_eq!(state.get_phases().len(), 1);
147 assert_eq!(state.get_phases()[0].name, "P");
148 }
149}