agentmux_launcher/
config.rs1use std::path::Path;
34
35use serde::Deserialize;
36
37pub const DEFAULT_SAGA_RETENTION_DAYS: i64 = 7;
39
40#[derive(Debug, Default, Deserialize)]
43struct LauncherConfig {
44 #[serde(default)]
45 saga: SagaConfig,
46}
47
48#[derive(Debug, Default, Deserialize)]
49struct SagaConfig {
50 #[serde(default)]
51 launcher: SagaLauncherConfig,
52}
53
54#[derive(Debug, Default, Deserialize)]
55struct SagaLauncherConfig {
56 retention_days: Option<i64>,
61}
62
63pub fn load_saga_retention_days(
70 user_home_dir: &Path,
71 mut log_warn: impl FnMut(&str),
72) -> i64 {
73 let path = user_home_dir.join("config.toml");
74 let raw = match std::fs::read_to_string(&path) {
75 Ok(s) => s,
76 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
77 return DEFAULT_SAGA_RETENTION_DAYS;
80 }
81 Err(e) => {
82 log_warn(&format!(
83 "[config] failed to read {}: {} — using default retention {} days",
84 path.display(),
85 e,
86 DEFAULT_SAGA_RETENTION_DAYS
87 ));
88 return DEFAULT_SAGA_RETENTION_DAYS;
89 }
90 };
91 match toml::from_str::<LauncherConfig>(&raw) {
92 Ok(cfg) => match cfg.saga.launcher.retention_days {
93 Some(d) if d > 0 => d,
94 Some(d) => {
95 log_warn(&format!(
96 "[config] [saga.launcher] retention_days = {} is non-positive; using default {} days",
97 d, DEFAULT_SAGA_RETENTION_DAYS
98 ));
99 DEFAULT_SAGA_RETENTION_DAYS
100 }
101 None => DEFAULT_SAGA_RETENTION_DAYS,
102 },
103 Err(e) => {
104 log_warn(&format!(
105 "[config] failed to parse {}: {} — using default retention {} days",
106 path.display(),
107 e,
108 DEFAULT_SAGA_RETENTION_DAYS
109 ));
110 DEFAULT_SAGA_RETENTION_DAYS
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use std::cell::RefCell;
119 use std::fs;
120 use tempfile::TempDir;
121
122 fn run(home: &Path) -> (i64, Vec<String>) {
127 let warnings: RefCell<Vec<String>> = RefCell::new(Vec::new());
128 let days = load_saga_retention_days(home, |w| warnings.borrow_mut().push(w.to_string()));
129 (days, warnings.into_inner())
130 }
131
132 #[test]
133 fn missing_file_returns_default_no_warning() {
134 let dir = TempDir::new().unwrap();
135 let (days, warns) = run(dir.path());
136 assert_eq!(days, DEFAULT_SAGA_RETENTION_DAYS);
137 assert!(warns.is_empty(), "missing file is silent, got: {warns:?}");
138 }
139
140 #[test]
141 fn valid_config_returns_configured_value() {
142 let dir = TempDir::new().unwrap();
143 fs::write(
144 dir.path().join("config.toml"),
145 "[saga.launcher]\nretention_days = 14\n",
146 )
147 .unwrap();
148 let (days, warns) = run(dir.path());
149 assert_eq!(days, 14);
150 assert!(warns.is_empty());
151 }
152
153 #[test]
154 fn malformed_toml_logs_warning_and_returns_default() {
155 let dir = TempDir::new().unwrap();
156 fs::write(dir.path().join("config.toml"), "not = toml = bro\n").unwrap();
157 let (days, warns) = run(dir.path());
158 assert_eq!(days, DEFAULT_SAGA_RETENTION_DAYS);
159 assert_eq!(warns.len(), 1);
160 assert!(warns[0].contains("failed to parse"), "got: {}", warns[0]);
161 }
162
163 #[test]
164 fn missing_section_returns_default_no_warning() {
165 let dir = TempDir::new().unwrap();
167 fs::write(
168 dir.path().join("config.toml"),
169 "[some_other_section]\nfoo = 1\n",
170 )
171 .unwrap();
172 let (days, warns) = run(dir.path());
173 assert_eq!(days, DEFAULT_SAGA_RETENTION_DAYS);
174 assert!(warns.is_empty());
175 }
176
177 #[test]
178 fn non_positive_retention_logs_warning_and_returns_default() {
179 let dir = TempDir::new().unwrap();
180 fs::write(
181 dir.path().join("config.toml"),
182 "[saga.launcher]\nretention_days = 0\n",
183 )
184 .unwrap();
185 let (days, warns) = run(dir.path());
186 assert_eq!(days, DEFAULT_SAGA_RETENTION_DAYS);
187 assert_eq!(warns.len(), 1);
188 assert!(warns[0].contains("non-positive"));
189
190 let dir = TempDir::new().unwrap();
191 fs::write(
192 dir.path().join("config.toml"),
193 "[saga.launcher]\nretention_days = -3\n",
194 )
195 .unwrap();
196 let (days, warns) = run(dir.path());
197 assert_eq!(days, DEFAULT_SAGA_RETENTION_DAYS);
198 assert_eq!(warns.len(), 1);
199 }
200
201}