1use crate::config::ResolvedConfig;
2use crate::env_value;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6fn find_config_path(cwd: &Path) -> Option<PathBuf> {
7 let mut current_dir = cwd.to_path_buf();
8 loop {
9 let potential = current_dir.join("sniff.config.toml");
10 if potential.is_file() {
11 return Some(potential);
12 }
13 if !current_dir.pop() {
14 break;
15 }
16 }
17 None
18}
19
20pub fn resolve_config(cwd: &Path) -> Result<ResolvedConfig, String> {
21 let mut config = ResolvedConfig::default();
22
23 if let Some(config_path) = find_config_path(cwd) {
24 let content = fs::read_to_string(&config_path)
25 .map_err(|err| format!("failed to read config {}: {}", config_path.display(), err))?;
26 config = toml::from_str::<ResolvedConfig>(&content)
27 .map_err(|err| format!("failed to parse config {}: {}", config_path.display(), err))?;
28 }
29
30 if let Some(model) = env_value::read("SNIFF_MODEL") {
31 config.model = model;
32 }
33
34 if let Some(endpoint) = env_value::read("SNIFF_ENDPOINT") {
35 config.llm.endpoint = endpoint;
36 }
37
38 Ok(config)
39}
40
41#[cfg(test)]
42mod tests {
43 use super::resolve_config;
44 use std::fs;
45 use std::time::{SystemTime, UNIX_EPOCH};
46
47 #[test]
48 fn invalid_config_fails_instead_of_using_defaults() {
49 let unique = SystemTime::now()
50 .duration_since(UNIX_EPOCH)
51 .unwrap()
52 .as_nanos();
53 let root = std::env::temp_dir().join(format!("sniff-invalid-config-{unique}"));
54 fs::create_dir_all(&root).unwrap();
55 fs::write(root.join("sniff.config.toml"), "[thresholds\ninvalid").unwrap();
56
57 let err = resolve_config(&root).expect_err("invalid config should fail explicitly");
58 assert!(err.contains("failed to parse config"), "{err}");
59
60 let _ = fs::remove_dir_all(&root);
61 }
62}