Skip to main content

decy_oracle/
config.rs

1//! Oracle configuration
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6/// Configuration for the decy oracle
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(default)]
9pub struct OracleConfig {
10    /// Path to the patterns file (.apr format)
11    pub patterns_path: PathBuf,
12
13    /// Confidence threshold for suggestions (0.0-1.0)
14    pub confidence_threshold: f32,
15
16    /// Maximum suggestions to return
17    pub max_suggestions: usize,
18
19    /// Enable auto-fix for high-confidence suggestions
20    pub auto_fix: bool,
21
22    /// Maximum retry attempts with oracle fixes
23    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    /// Create config from environment variables
47    ///
48    /// Looks for:
49    /// - DECY_ORACLE_PATTERNS: Path to patterns file
50    /// - DECY_ORACLE_THRESHOLD: Confidence threshold
51    /// - DECY_ORACLE_AUTO_FIX: Enable auto-fix (true/false)
52    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    /// Load config from TOML file
73    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    // ============================================================================
86    // DEFAULT CONFIGURATION TESTS
87    // ============================================================================
88
89    #[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    // ============================================================================
111    // TOML SERIALIZATION/DESERIALIZATION TESTS
112    // ============================================================================
113
114    #[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        // Test that missing fields use defaults (via #[serde(default)])
132        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); // default
138        assert!(!config.auto_fix); // default
139    }
140
141    #[test]
142    fn test_config_from_toml_empty() {
143        // Empty TOML should use all defaults
144        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        // Float may be serialized with varying precision, check for the key
161        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        // Verify we can deserialize back
166        let deserialized: OracleConfig = toml::from_str(&toml_str).unwrap();
167        assert!((deserialized.confidence_threshold - 0.95).abs() < f32::EPSILON);
168    }
169
170    // ============================================================================
171    // FROM_ENV TESTS - Use mutex to serialize access to env vars
172    // ============================================================================
173
174    // Mutex to serialize env var tests that would otherwise race.
175    // Use unwrap_or_else to recover from poisoned mutex.
176    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        // Clear relevant env vars if set
186        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        // Should use defaults when no env vars set
192        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        // Should use default when parse fails
224        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); // "yes" != "true"
261        std::env::remove_var("DECY_ORACLE_AUTO_FIX");
262    }
263
264    // ============================================================================
265    // FROM_FILE TESTS
266    // ============================================================================
267
268    #[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        // When file doesn't exist, read_to_string returns "" via unwrap_or_default
292        // Empty string parsed as TOML gives defaults
293        let config = OracleConfig::from_file(std::path::Path::new("/nonexistent/path.toml"));
294        // Should use defaults
295        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        // Don't write anything - empty file
304
305        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    // ============================================================================
320    // DEBUG AND CLONE TESTS
321    // ============================================================================
322
323    #[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    // ============================================================================
348    // DEFAULT_PATTERNS_PATH FUNCTION TESTS
349    // ============================================================================
350
351    #[test]
352    fn test_default_patterns_path_returns_path() {
353        let path = default_patterns_path();
354        // Should be a non-empty path
355        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}