1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(default)]
9pub struct OracleConfig {
10 pub patterns_path: PathBuf,
12
13 pub confidence_threshold: f32,
15
16 pub max_suggestions: usize,
18
19 pub auto_fix: bool,
21
22 pub max_retries: usize,
24}
25
26impl Default for OracleConfig {
27 fn default() -> Self {
28 Self {
29 patterns_path: default_patterns_path(),
30 confidence_threshold: 0.7_f32,
31 max_suggestions: 5,
32 auto_fix: false,
33 max_retries: 3,
34 }
35 }
36}
37
38fn default_patterns_path() -> PathBuf {
39 dirs::home_dir()
40 .unwrap_or_else(|| PathBuf::from("."))
41 .join(".decy")
42 .join("decision_patterns.apr")
43}
44
45impl OracleConfig {
46 pub fn from_env() -> Self {
53 let mut config = Self::default();
54
55 if let Ok(path) = std::env::var("DECY_ORACLE_PATTERNS") {
56 config.patterns_path = PathBuf::from(path);
57 }
58
59 if let Ok(threshold) = std::env::var("DECY_ORACLE_THRESHOLD") {
60 if let Ok(t) = threshold.parse() {
61 config.confidence_threshold = t;
62 }
63 }
64
65 if let Ok(auto_fix) = std::env::var("DECY_ORACLE_AUTO_FIX") {
66 config.auto_fix = auto_fix.to_lowercase() == "true";
67 }
68
69 config
70 }
71
72 pub fn from_file(path: &std::path::Path) -> Result<Self, toml::de::Error> {
74 let content = std::fs::read_to_string(path).unwrap_or_default();
75 toml::from_str(&content)
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use std::io::Write;
83 use tempfile::NamedTempFile;
84
85 #[test]
90 fn test_config_default() {
91 let config = OracleConfig::default();
92 assert!((config.confidence_threshold - 0.7_f32).abs() < f32::EPSILON);
93 assert_eq!(config.max_suggestions, 5);
94 assert!(!config.auto_fix);
95 assert_eq!(config.max_retries, 3);
96 }
97
98 #[test]
99 fn test_config_default_patterns_path_ends_with_apr() {
100 let config = OracleConfig::default();
101 assert!(config.patterns_path.to_string_lossy().ends_with(".apr"));
102 }
103
104 #[test]
105 fn test_config_default_patterns_path_in_decy_dir() {
106 let config = OracleConfig::default();
107 assert!(config.patterns_path.to_string_lossy().contains(".decy"));
108 }
109
110 #[test]
115 fn test_config_from_toml() {
116 let toml = r#"
117confidence_threshold = 0.85
118max_suggestions = 10
119auto_fix = true
120max_retries = 5
121"#;
122 let config: OracleConfig = toml::from_str(toml).unwrap();
123 assert!((config.confidence_threshold - 0.85_f32).abs() < f32::EPSILON);
124 assert_eq!(config.max_suggestions, 10);
125 assert!(config.auto_fix);
126 assert_eq!(config.max_retries, 5);
127 }
128
129 #[test]
130 fn test_config_from_toml_partial() {
131 let toml = r#"
133confidence_threshold = 0.9
134"#;
135 let config: OracleConfig = toml::from_str(toml).unwrap();
136 assert!((config.confidence_threshold - 0.9_f32).abs() < f32::EPSILON);
137 assert_eq!(config.max_suggestions, 5); assert!(!config.auto_fix); }
140
141 #[test]
142 fn test_config_from_toml_empty() {
143 let toml = "";
145 let config: OracleConfig = toml::from_str(toml).unwrap();
146 assert!((config.confidence_threshold - 0.7_f32).abs() < f32::EPSILON);
147 assert_eq!(config.max_suggestions, 5);
148 }
149
150 #[test]
151 fn test_config_to_toml() {
152 let config = OracleConfig {
153 patterns_path: PathBuf::from("/custom/path.apr"),
154 confidence_threshold: 0.95,
155 max_suggestions: 20,
156 auto_fix: true,
157 max_retries: 10,
158 };
159 let toml_str = toml::to_string(&config).unwrap();
160 assert!(toml_str.contains("confidence_threshold"));
162 assert!(toml_str.contains("max_suggestions = 20"));
163 assert!(toml_str.contains("auto_fix = true"));
164 assert!(toml_str.contains("max_retries = 10"));
165 let deserialized: OracleConfig = toml::from_str(&toml_str).unwrap();
167 assert!((deserialized.confidence_threshold - 0.95).abs() < f32::EPSILON);
168 }
169
170 static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
177
178 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
179 ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner())
180 }
181
182 #[test]
183 fn test_from_env_default_when_no_env_vars() {
184 let _lock = lock_env();
185 std::env::remove_var("DECY_ORACLE_PATTERNS");
187 std::env::remove_var("DECY_ORACLE_THRESHOLD");
188 std::env::remove_var("DECY_ORACLE_AUTO_FIX");
189
190 let config = OracleConfig::from_env();
191 assert!((config.confidence_threshold - 0.7_f32).abs() < f32::EPSILON);
193 assert!(!config.auto_fix);
194 }
195
196 #[test]
197 fn test_from_env_patterns_path() {
198 let _lock = lock_env();
199 std::env::set_var("DECY_ORACLE_PATTERNS", "/custom/test/path.apr");
200 let config = OracleConfig::from_env();
201 assert_eq!(config.patterns_path, PathBuf::from("/custom/test/path.apr"));
202 std::env::remove_var("DECY_ORACLE_PATTERNS");
203 }
204
205 #[test]
206 fn test_from_env_threshold() {
207 let _lock = lock_env();
208 std::env::set_var("DECY_ORACLE_THRESHOLD", "0.85");
209 let config = OracleConfig::from_env();
210 assert!(
211 (config.confidence_threshold - 0.85_f32).abs() < 0.01,
212 "Expected ~0.85, got {}",
213 config.confidence_threshold
214 );
215 std::env::remove_var("DECY_ORACLE_THRESHOLD");
216 }
217
218 #[test]
219 fn test_from_env_threshold_invalid_uses_default() {
220 let _lock = lock_env();
221 std::env::set_var("DECY_ORACLE_THRESHOLD", "not_a_number");
222 let config = OracleConfig::from_env();
223 assert!((config.confidence_threshold - 0.7_f32).abs() < f32::EPSILON);
225 std::env::remove_var("DECY_ORACLE_THRESHOLD");
226 }
227
228 #[test]
229 fn test_from_env_auto_fix_true() {
230 let _lock = lock_env();
231 std::env::set_var("DECY_ORACLE_AUTO_FIX", "true");
232 let config = OracleConfig::from_env();
233 assert!(config.auto_fix);
234 std::env::remove_var("DECY_ORACLE_AUTO_FIX");
235 }
236
237 #[test]
238 fn test_from_env_auto_fix_true_uppercase() {
239 let _lock = lock_env();
240 std::env::set_var("DECY_ORACLE_AUTO_FIX", "TRUE");
241 let config = OracleConfig::from_env();
242 assert!(config.auto_fix);
243 std::env::remove_var("DECY_ORACLE_AUTO_FIX");
244 }
245
246 #[test]
247 fn test_from_env_auto_fix_false() {
248 let _lock = lock_env();
249 std::env::set_var("DECY_ORACLE_AUTO_FIX", "false");
250 let config = OracleConfig::from_env();
251 assert!(!config.auto_fix);
252 std::env::remove_var("DECY_ORACLE_AUTO_FIX");
253 }
254
255 #[test]
256 fn test_from_env_auto_fix_any_other_value_is_false() {
257 let _lock = lock_env();
258 std::env::set_var("DECY_ORACLE_AUTO_FIX", "yes");
259 let config = OracleConfig::from_env();
260 assert!(!config.auto_fix); std::env::remove_var("DECY_ORACLE_AUTO_FIX");
262 }
263
264 #[test]
269 fn test_from_file_valid_toml() {
270 let mut file = NamedTempFile::new().unwrap();
271 writeln!(
272 file,
273 r#"
274confidence_threshold = 0.8
275max_suggestions = 15
276auto_fix = true
277max_retries = 7
278"#
279 )
280 .unwrap();
281
282 let config = OracleConfig::from_file(file.path()).unwrap();
283 assert!((config.confidence_threshold - 0.8_f32).abs() < f32::EPSILON);
284 assert_eq!(config.max_suggestions, 15);
285 assert!(config.auto_fix);
286 assert_eq!(config.max_retries, 7);
287 }
288
289 #[test]
290 fn test_from_file_nonexistent_uses_defaults() {
291 let config = OracleConfig::from_file(std::path::Path::new("/nonexistent/path.toml"));
294 assert!(config.is_ok());
296 let config = config.unwrap();
297 assert!((config.confidence_threshold - 0.7_f32).abs() < f32::EPSILON);
298 }
299
300 #[test]
301 fn test_from_file_empty_file_uses_defaults() {
302 let file = NamedTempFile::new().unwrap();
303 let config = OracleConfig::from_file(file.path()).unwrap();
306 assert!((config.confidence_threshold - 0.7_f32).abs() < f32::EPSILON);
307 assert_eq!(config.max_suggestions, 5);
308 }
309
310 #[test]
311 fn test_from_file_invalid_toml_returns_error() {
312 let mut file = NamedTempFile::new().unwrap();
313 writeln!(file, "invalid = [toml").unwrap();
314
315 let result = OracleConfig::from_file(file.path());
316 assert!(result.is_err());
317 }
318
319 #[test]
324 fn test_config_debug() {
325 let config = OracleConfig::default();
326 let debug_str = format!("{:?}", config);
327 assert!(debug_str.contains("OracleConfig"));
328 assert!(debug_str.contains("confidence_threshold"));
329 }
330
331 #[test]
332 fn test_config_clone() {
333 let config = OracleConfig {
334 patterns_path: PathBuf::from("/test/path.apr"),
335 confidence_threshold: 0.9,
336 max_suggestions: 10,
337 auto_fix: true,
338 max_retries: 5,
339 };
340 let cloned = config.clone();
341 assert_eq!(config.patterns_path, cloned.patterns_path);
342 assert!((config.confidence_threshold - cloned.confidence_threshold).abs() < f32::EPSILON);
343 assert_eq!(config.max_suggestions, cloned.max_suggestions);
344 assert_eq!(config.auto_fix, cloned.auto_fix);
345 }
346
347 #[test]
352 fn test_default_patterns_path_returns_path() {
353 let path = default_patterns_path();
354 assert!(!path.as_os_str().is_empty());
356 }
357
358 #[test]
359 fn test_default_patterns_path_file_extension() {
360 let path = default_patterns_path();
361 assert_eq!(path.extension().unwrap(), "apr");
362 }
363
364 #[test]
365 fn test_default_patterns_path_filename() {
366 let path = default_patterns_path();
367 assert_eq!(path.file_name().unwrap(), "decision_patterns.apr");
368 }
369}