1use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::Path;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(rename_all = "snake_case")]
14pub enum AgentStatusState {
15 Working,
17 Done,
19 Failed,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct AgentStatus {
26 pub spec_id: String,
28 pub status: AgentStatusState,
30 pub updated_at: String,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub error: Option<String>,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub commits: Vec<String>,
38}
39
40pub fn write_status(path: &Path, status: &AgentStatus) -> Result<()> {
54 let json =
56 serde_json::to_string_pretty(status).context("Failed to serialize AgentStatus to JSON")?;
57
58 let temp_path = path.with_extension("tmp");
60 fs::write(&temp_path, json).context(format!(
61 "Failed to write status to temporary file: {:?}",
62 temp_path
63 ))?;
64
65 fs::rename(&temp_path, path).context(format!(
67 "Failed to rename temporary status file to: {:?}",
68 path
69 ))?;
70
71 Ok(())
72}
73
74pub fn read_status(path: &Path) -> Result<AgentStatus> {
91 if !path.exists() {
93 anyhow::bail!("Status file not found at {:?}", path);
94 }
95
96 let contents =
98 fs::read_to_string(path).context(format!("Failed to read status file: {:?}", path))?;
99
100 let status: AgentStatus = serde_json::from_str(&contents)
102 .context(format!("Failed to parse status file as JSON: {:?}", path))?;
103
104 Ok(status)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use std::fs;
111 use tempfile::TempDir;
112
113 #[test]
114 fn test_serialization_round_trip() {
115 let status = AgentStatus {
116 spec_id: "2026-02-03-test".to_string(),
117 status: AgentStatusState::Done,
118 updated_at: "2026-02-03T10:00:00Z".to_string(),
119 error: None,
120 commits: vec!["abc123".to_string()],
121 };
122
123 let json = serde_json::to_string(&status).unwrap();
124 let deserialized: AgentStatus = serde_json::from_str(&json).unwrap();
125
126 assert_eq!(deserialized.spec_id, "2026-02-03-test");
127 assert_eq!(deserialized.status, AgentStatusState::Done);
128 assert_eq!(deserialized.updated_at, "2026-02-03T10:00:00Z");
129 assert_eq!(deserialized.error, None);
130 assert_eq!(deserialized.commits, vec!["abc123"]);
131 }
132
133 #[test]
134 fn test_write_status_atomic() {
135 let temp_dir = TempDir::new().unwrap();
136 let status_path = temp_dir.path().join(".chant-status.json");
137
138 let status = AgentStatus {
139 spec_id: "2026-02-03-test".to_string(),
140 status: AgentStatusState::Working,
141 updated_at: "2026-02-03T10:00:00Z".to_string(),
142 error: None,
143 commits: vec![],
144 };
145
146 write_status(&status_path, &status).unwrap();
147
148 assert!(status_path.exists());
150
151 let temp_path = status_path.with_extension("tmp");
153 assert!(!temp_path.exists());
154
155 let read_back = read_status(&status_path).unwrap();
157 assert_eq!(read_back.spec_id, status.spec_id);
158 assert_eq!(read_back.status, status.status);
159 }
160
161 #[test]
162 fn test_read_status_missing_file() {
163 let temp_dir = TempDir::new().unwrap();
164 let status_path = temp_dir.path().join("nonexistent.json");
165
166 let result = read_status(&status_path);
167 assert!(result.is_err());
168 let err_msg = result.unwrap_err().to_string();
169 assert!(err_msg.contains("not found"));
170 }
171
172 #[test]
173 fn test_read_status_corrupt_json() {
174 let temp_dir = TempDir::new().unwrap();
175 let status_path = temp_dir.path().join("corrupt.json");
176
177 fs::write(&status_path, "{ invalid json }").unwrap();
179
180 let result = read_status(&status_path);
181 assert!(result.is_err());
182 let err_msg = result.unwrap_err().to_string();
183 assert!(err_msg.contains("parse"));
184 }
185
186 #[test]
187 fn test_status_with_error() {
188 let status = AgentStatus {
189 spec_id: "2026-02-03-test".to_string(),
190 status: AgentStatusState::Failed,
191 updated_at: "2026-02-03T10:00:00Z".to_string(),
192 error: Some("Build failed".to_string()),
193 commits: vec![],
194 };
195
196 let json = serde_json::to_string(&status).unwrap();
197 let deserialized: AgentStatus = serde_json::from_str(&json).unwrap();
198
199 assert_eq!(deserialized.status, AgentStatusState::Failed);
200 assert_eq!(deserialized.error, Some("Build failed".to_string()));
201 }
202
203 #[test]
204 fn test_status_multiple_commits() {
205 let status = AgentStatus {
206 spec_id: "2026-02-03-test".to_string(),
207 status: AgentStatusState::Done,
208 updated_at: "2026-02-03T10:00:00Z".to_string(),
209 error: None,
210 commits: vec![
211 "abc123".to_string(),
212 "def456".to_string(),
213 "ghi789".to_string(),
214 ],
215 };
216
217 let json = serde_json::to_string(&status).unwrap();
218 let deserialized: AgentStatus = serde_json::from_str(&json).unwrap();
219
220 assert_eq!(deserialized.commits.len(), 3);
221 assert_eq!(deserialized.commits[0], "abc123");
222 assert_eq!(deserialized.commits[2], "ghi789");
223 }
224}