Skip to main content

oxicode/store/
todo_state.rs

1//! Session-scoped todo state.
2//!
3//! `oxicode_agent::tools::TodoStateProvider`의 구현. 에이전트의 `todo` 도구와
4//! TUI sticky panel 사이의 단일 진실 소스. `Arc<RwLock<Vec<TodoPhase>>>`로
5//! 보유하고, clone은 cheap (Arc 공유).
6
7use 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/// Session 단위 todo 상태.
15///
16/// `TodoStateProvider` 특성 구현 — `todo` 도구와 sticky panel이 공유.
17/// TUI 채널로 갱신 알림을 보내려면 `notifier: Option<Box<dyn Fn>>`를 추가.
18#[derive(Debug)]
19pub struct TodoState {
20    phases: RwLock<Vec<TodoPhase>>,
21}
22
23impl TodoState {
24    /// 새 빈 상태 생성.
25    pub fn new() -> Self {
26        Self {
27            phases: RwLock::new(Vec::new()),
28        }
29    }
30
31    /// 초기 phase들을 지정하여 생성.
32    pub fn with_phases(phases: Vec<TodoPhase>) -> Self {
33        Self {
34            phases: RwLock::new(phases),
35        }
36    }
37
38    /// 현재 phase 스냅샷 (TUI 매 프레임 호출).
39    pub fn get_phases(&self) -> Vec<TodoPhase> {
40        self.phases.read().clone()
41    }
42
43    /// ops 적용. `apply_ops` 헬퍼를 위임.
44    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 — async 호출 전.
48        drop(phases);
49        result
50    }
51
52    /// 전체 삭제 (테스트/리셋용).
53    #[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            // RwLock 가드는 동기 acquire → await 전에 drop
80            let mut phases = self.phases.write();
81            Ok(oxicode_agent::tools::todo::apply_ops(&mut phases, &ops))
82        })
83    }
84}
85
86/// `Arc<TodoState>`를 그대로 `Arc<dyn TodoStateProvider>`로 변환.
87pub fn provider_from_state(state: Arc<TodoState>) -> Arc<dyn TodoStateProvider> {
88    state as Arc<dyn TodoStateProvider>
89}
90
91// ── Tests ────────────────────────────────────────────────────────────
92
93#[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        // tokio runtime 없이 apply_ops 동기 부분만 검증.
110        let phases = provider.get_phases();
111        assert!(phases.is_empty());
112
113        // ops 적용 후 get_phases가 갱신됨을 검증.
114        // (비동기 trait method 호출은 tokio test 필요 — 간단히 동기 .apply() 사용)
115        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        // 같은 Arc를 공유하는 두 provider가 같은 상태를 본다.
129        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(); // Arc::strong_count 증가 확인은 보이지 않지만 호출 가능
134        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}