1use std::path::Path;
19
20use serde::Deserialize;
21
22use crate::embeddings::Provider;
23use crate::index::CTX_DIR;
24
25pub const CONFIG_FILE: &str = "config.toml";
27
28#[derive(Debug, Clone, Default, Deserialize)]
31#[serde(default)]
32pub struct CtxConfig {
33 pub embedding: EmbeddingConfig,
35}
36
37#[derive(Debug, Clone, Default, Deserialize)]
39#[serde(default)]
40pub struct EmbeddingConfig {
41 pub provider: Option<Provider>,
43 pub model: Option<String>,
46 pub host: Option<String>,
48}
49
50impl CtxConfig {
51 pub fn load(root: &Path) -> Self {
54 Self::load_file(&root.join(CTX_DIR).join(CONFIG_FILE))
55 }
56
57 pub fn load_file(path: &Path) -> Self {
59 let text = match std::fs::read_to_string(path) {
60 Ok(text) => text,
61 Err(_) => return Self::default(), };
63 match toml::from_str(&text) {
64 Ok(config) => config,
65 Err(e) => {
66 eprintln!("Warning: ignoring malformed {} ({e})", path.display());
67 Self::default()
68 }
69 }
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use std::io::Write;
77
78 fn write_temp(contents: &str) -> tempfile::NamedTempFile {
79 let mut f = tempfile::NamedTempFile::new().unwrap();
80 f.write_all(contents.as_bytes()).unwrap();
81 f
82 }
83
84 #[test]
85 fn missing_file_is_default() {
86 let cfg = CtxConfig::load_file(Path::new("/nonexistent/.ctx/config.toml"));
87 assert!(cfg.embedding.provider.is_none());
88 assert!(cfg.embedding.model.is_none());
89 }
90
91 #[test]
92 fn parses_embedding_section() {
93 let f = write_temp(
94 r#"
95[embedding]
96provider = "ollama"
97model = "qwen3-embedding:8b"
98"#,
99 );
100 let cfg = CtxConfig::load_file(f.path());
101 assert_eq!(cfg.embedding.provider, Some(Provider::Ollama));
102 assert_eq!(cfg.embedding.model.as_deref(), Some("qwen3-embedding:8b"));
103 assert!(cfg.embedding.host.is_none());
104 }
105
106 #[test]
107 fn unknown_keys_ignored() {
108 let f = write_temp(
109 r#"
110[embedding]
111provider = "openai"
112
113[future_section]
114whatever = true
115"#,
116 );
117 let cfg = CtxConfig::load_file(f.path());
118 assert_eq!(cfg.embedding.provider, Some(Provider::Openai));
119 }
120
121 #[test]
122 fn malformed_file_is_default() {
123 let f = write_temp("this is not valid toml : : :");
124 let cfg = CtxConfig::load_file(f.path());
125 assert!(cfg.embedding.provider.is_none());
126 }
127
128 #[test]
129 fn config_provides_resolution_default() {
130 assert_eq!(
132 Provider::resolve(Some(Provider::Local), false, Some(Provider::Ollama)),
133 Provider::Local
134 );
135 assert_eq!(
136 Provider::resolve(None, false, Some(Provider::Ollama)),
137 Provider::Ollama
138 );
139 assert_eq!(Provider::resolve(None, false, None), Provider::Local);
140 assert_eq!(
142 Provider::resolve(None, true, Some(Provider::Ollama)),
143 Provider::Openai
144 );
145 }
146}