1use crate::errors::{CoreError, CoreResult};
6use std::path::Path;
7
8pub enum StateAction {
10 AddDecision {
12 text: String,
14 },
15 AddBlocker {
17 text: String,
19 },
20 AddNote {
22 text: String,
24 },
25 SetFocus {
27 text: String,
29 },
30 AddQuestion {
32 text: String,
34 },
35}
36
37pub fn read_state(ito_path: &Path) -> CoreResult<String> {
39 let state_path = ito_path.join("planning").join("STATE.md");
40 ito_common::io::read_to_string(&state_path)
41 .map_err(|e| CoreError::io("reading STATE.md", std::io::Error::other(e)))
42}
43
44pub fn update_state(ito_path: &Path, action: StateAction) -> CoreResult<()> {
46 let state_path = ito_path.join("planning").join("STATE.md");
47 let contents = ito_common::io::read_to_string(&state_path)
48 .map_err(|e| CoreError::io("reading STATE.md", std::io::Error::other(e)))?;
49 let date = crate::time::now_date();
50
51 let updated = match action {
52 StateAction::AddDecision { ref text } => {
53 ito_domain::state::add_decision(&contents, &date, text)
54 }
55 StateAction::AddBlocker { ref text } => {
56 ito_domain::state::add_blocker(&contents, &date, text)
57 }
58 StateAction::AddNote { ref text } => {
59 let time = crate::time::now_time();
60 ito_domain::state::add_note(&contents, &date, &time, text)
61 }
62 StateAction::SetFocus { ref text } => ito_domain::state::set_focus(&contents, &date, text),
63 StateAction::AddQuestion { ref text } => {
64 ito_domain::state::add_question(&contents, &date, text)
65 }
66 };
67
68 let updated = updated.map_err(CoreError::validation)?;
69
70 ito_common::io::write(&state_path, updated.as_bytes())
71 .map_err(|e| CoreError::io("writing STATE.md", std::io::Error::other(e)))
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 fn write_file(path: &std::path::Path, contents: &str) {
80 if let Some(parent) = path.parent() {
81 std::fs::create_dir_all(parent).expect("create parent dirs");
82 }
83 std::fs::write(path, contents).expect("write fixture");
84 }
85
86 fn minimal_state_md(date: &str) -> String {
88 format!(
89 "# Project State\n\n\
90 Last Updated: {date}\n\n\
91 ## Current Focus\n\
92 [placeholder]\n\n\
93 ## Recent Decisions\n\
94 - {date}: Project initialized\n\n\
95 ## Open Questions\n\
96 - [ ] placeholder\n\n\
97 ## Blockers\n\
98 [None currently]\n\n\
99 ## Session Notes\n\
100 ### {date} - Initial Setup\n\
101 - Completed: init\n"
102 )
103 }
104
105 #[test]
106 fn read_state_returns_error_for_missing_file() {
107 let tmp = tempfile::tempdir().expect("tempdir");
108 let ito_path = tmp.path();
109 let result = read_state(ito_path);
112 assert!(result.is_err(), "read_state should fail for missing file");
113 let err = result.unwrap_err();
114 let msg = err.to_string();
115 assert!(
116 msg.contains("STATE.md"),
117 "error should mention STATE.md, got: {msg}"
118 );
119 }
120
121 #[test]
122 fn read_state_returns_contents_for_existing_file() {
123 let tmp = tempfile::tempdir().expect("tempdir");
124 let ito_path = tmp.path();
125 let contents = minimal_state_md("2025-01-01");
126 write_file(&ito_path.join("planning").join("STATE.md"), &contents);
127
128 let result = read_state(ito_path).expect("read_state should succeed");
129 assert_eq!(result, contents);
130 }
131
132 #[test]
133 fn update_state_add_decision() {
134 let tmp = tempfile::tempdir().expect("tempdir");
135 let ito_path = tmp.path();
136 write_file(
137 &ito_path.join("planning").join("STATE.md"),
138 &minimal_state_md("2025-01-01"),
139 );
140
141 update_state(
142 ito_path,
143 StateAction::AddDecision {
144 text: "Use Rust".to_string(),
145 },
146 )
147 .expect("add decision should succeed");
148
149 let updated =
150 std::fs::read_to_string(ito_path.join("planning").join("STATE.md")).expect("read back");
151 assert!(
152 updated.contains("Use Rust"),
153 "decision text should appear in STATE.md"
154 );
155 }
156
157 #[test]
158 fn update_state_add_blocker() {
159 let tmp = tempfile::tempdir().expect("tempdir");
160 let ito_path = tmp.path();
161 write_file(
162 &ito_path.join("planning").join("STATE.md"),
163 &minimal_state_md("2025-01-01"),
164 );
165
166 update_state(
167 ito_path,
168 StateAction::AddBlocker {
169 text: "Waiting on API access".to_string(),
170 },
171 )
172 .expect("add blocker should succeed");
173
174 let updated =
175 std::fs::read_to_string(ito_path.join("planning").join("STATE.md")).expect("read back");
176 assert!(
177 updated.contains("Waiting on API access"),
178 "blocker text should appear in STATE.md"
179 );
180 assert!(
182 !updated.contains("[None currently]"),
183 "placeholder should be replaced"
184 );
185 }
186
187 #[test]
188 fn update_state_set_focus() {
189 let tmp = tempfile::tempdir().expect("tempdir");
190 let ito_path = tmp.path();
191 write_file(
192 &ito_path.join("planning").join("STATE.md"),
193 &minimal_state_md("2025-01-01"),
194 );
195
196 update_state(
197 ito_path,
198 StateAction::SetFocus {
199 text: "Implement auth module".to_string(),
200 },
201 )
202 .expect("set focus should succeed");
203
204 let updated =
205 std::fs::read_to_string(ito_path.join("planning").join("STATE.md")).expect("read back");
206 assert!(
207 updated.contains("Implement auth module"),
208 "focus text should appear in STATE.md"
209 );
210 }
211
212 #[test]
213 fn update_state_add_question() {
214 let tmp = tempfile::tempdir().expect("tempdir");
215 let ito_path = tmp.path();
216 write_file(
217 &ito_path.join("planning").join("STATE.md"),
218 &minimal_state_md("2025-01-01"),
219 );
220
221 update_state(
222 ito_path,
223 StateAction::AddQuestion {
224 text: "Should we use gRPC?".to_string(),
225 },
226 )
227 .expect("add question should succeed");
228
229 let updated =
230 std::fs::read_to_string(ito_path.join("planning").join("STATE.md")).expect("read back");
231 assert!(
232 updated.contains("Should we use gRPC?"),
233 "question text should appear in STATE.md"
234 );
235 }
236
237 #[test]
238 fn update_state_add_note() {
239 let tmp = tempfile::tempdir().expect("tempdir");
240 let ito_path = tmp.path();
241 write_file(
242 &ito_path.join("planning").join("STATE.md"),
243 &minimal_state_md("2025-01-01"),
244 );
245
246 update_state(
247 ito_path,
248 StateAction::AddNote {
249 text: "Reviewed the design".to_string(),
250 },
251 )
252 .expect("add note should succeed");
253
254 let updated =
255 std::fs::read_to_string(ito_path.join("planning").join("STATE.md")).expect("read back");
256 assert!(
257 updated.contains("Reviewed the design"),
258 "note text should appear in STATE.md"
259 );
260 }
261
262 #[test]
263 fn update_state_returns_error_for_missing_file() {
264 let tmp = tempfile::tempdir().expect("tempdir");
265 let ito_path = tmp.path();
266 let result = update_state(
269 ito_path,
270 StateAction::AddDecision {
271 text: "test".to_string(),
272 },
273 );
274 assert!(result.is_err(), "update_state should fail for missing file");
275 }
276}