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 set_phases_sync(&self, phases: Vec<TodoPhase>) {
71 *self.phases.write() = phases;
72 }
73
74 fn apply_ops<'a>(
75 &'a self,
76 ops: Vec<TodoOp>,
77 ) -> Pin<Box<dyn Future<Output = Result<TodoUpdateResult, ToolError>> + Send + 'a>> {
78 Box::pin(async move {
79 let mut phases = self.phases.write();
81 Ok(oxicode_agent::tools::todo::apply_ops(&mut phases, &ops))
82 })
83 }
84}
85
86pub fn provider_from_state(state: Arc<TodoState>) -> Arc<dyn TodoStateProvider> {
88 state as Arc<dyn TodoStateProvider>
89}
90
91#[cfg(test)]
94mod tests {
95 use super::*;
96 use oxicode_agent::tools::todo::TodoItem;
97
98 #[test]
99 fn empty_state() {
100 let state = TodoState::new();
101 assert!(state.get_phases().is_empty());
102 }
103
104 #[test]
105 fn apply_ops_via_provider() {
106 let state = TodoState::new();
107 let provider: Arc<dyn TodoStateProvider> = provider_from_state(Arc::new(state));
108
109 let phases = provider.get_phases();
111 assert!(phases.is_empty());
112
113 let state_arc = Arc::new(TodoState::new());
116 let result = state_arc.apply(vec![TodoOp::Init {
117 list: None,
118 items: Some(vec!["task1".into(), "task2".into()]),
119 }]);
120 assert!(result.errors.is_empty());
121 let phases = state_arc.get_phases();
122 assert_eq!(phases.len(), 1);
123 assert_eq!(phases[0].tasks.len(), 2);
124 }
125
126 #[test]
127 fn provider_share_state() {
128 let state = Arc::new(TodoState::with_phases(vec![]));
130 let p1: Arc<dyn TodoStateProvider> = state.clone();
131 let p2: Arc<dyn TodoStateProvider> = state.clone();
132
133 let _ = p1.get_phases(); let _ = p2.get_phases();
135 assert!(state.get_phases().is_empty());
136 }
137
138 #[test]
139 fn with_phases_initial() {
140 let initial = vec![TodoPhase {
141 name: "P".into(),
142 tasks: vec![TodoItem {
143 content: "t".into(),
144 status: oxicode_agent::tools::todo::TodoStatus::Pending,
145 notes: None,
146 block_reason: None,
147 }],
148 }];
149 let state = TodoState::with_phases(initial);
150 assert_eq!(state.get_phases().len(), 1);
151 assert_eq!(state.get_phases()[0].name, "P");
152 }
153}