1use std::collections::BTreeMap;
2use std::path::PathBuf;
3use std::sync::OnceLock;
4use crate::tools::shell::config::ShellConfig;
5
6static PROFILE_NAME: OnceLock<Option<String>> = OnceLock::new();
7static PROVIDER_KEYS: OnceLock<BTreeMap<String, String>> = OnceLock::new();
8
9pub fn get_provider_keys() -> BTreeMap<String, String> {
13 PROVIDER_KEYS.get().cloned().unwrap_or_default()
14}
15
16pub fn get_profile() -> Option<String> {
19 PROFILE_NAME.get_or_init(|| std::env::var("SYNAPS_PROFILE").ok()).clone()
20}
21
22pub fn set_profile(name: Option<String>) {
26 let _ = PROFILE_NAME.set(name);
27}
28
29pub fn base_dir() -> PathBuf {
30 if let Ok(path) = std::env::var("SYNAPS_BASE_DIR") {
31 return PathBuf::from(path);
32 }
33 let home = std::env::var("HOME")
34 .or_else(|_| std::env::var("USERPROFILE"))
35 .unwrap_or_else(|_| ".".to_string());
36 PathBuf::from(home).join(".synaps-cli")
37}
38
39#[doc(hidden)]
41pub fn set_base_dir_for_tests(path: PathBuf) {
42 std::env::set_var("SYNAPS_BASE_DIR", path);
43}
44
45pub fn resolve_read_path(filename: &str) -> PathBuf {
47 let base = base_dir();
48
49 if let Some(profile) = get_profile() {
50 let profile_path = base.join(&profile).join(filename);
51 if profile_path.exists() {
52 return profile_path;
53 }
54 }
55
56 base.join(filename)
57}
58
59pub fn resolve_read_path_extended(path: &str) -> PathBuf {
61 let base = base_dir();
62
63 if let Some(profile) = get_profile() {
64 let profile_path = base.join(&profile).join(path);
65 if profile_path.exists() {
66 return profile_path;
67 }
68 }
69
70 base.join(path)
71}
72
73pub fn resolve_write_path(filename: &str) -> PathBuf {
75 let mut base = base_dir();
76
77 if let Some(profile) = get_profile() {
78 base.push(profile);
79 }
80
81 let _ = std::fs::create_dir_all(&base);
82 base.join(filename)
83}
84
85pub fn get_active_config_dir() -> PathBuf {
87 let mut base = base_dir();
88 if let Some(profile) = get_profile() {
89 base.push(profile);
90 }
91 base
92}
93
94#[derive(Debug, Clone, Default)]
96pub struct ServerConfig {
97 pub allowed_origins: Vec<String>,
99 pub token: Option<String>,
102 pub auto_approve_confirms: bool,
104 pub max_message_size: Option<usize>,
107}
108
109#[derive(Debug, Clone)]
111pub struct SynapsConfig {
112 pub model: Option<String>,
113 pub thinking_budget: Option<u32>,
114 pub context_window: Option<u64>, pub compaction_model: Option<String>, pub max_tool_output: usize, pub bash_timeout: u64, pub bash_max_timeout: u64, pub subagent_timeout: u64, pub api_retries: u32, pub theme: Option<String>,
122 pub agent_name: Option<String>,
123 pub disabled_plugins: Vec<String>,
124 pub favorite_models: Vec<String>,
125 pub disabled_skills: Vec<String>,
126 pub shell: ShellConfig,
127 pub server: ServerConfig,
128 pub provider_keys: BTreeMap<String, String>,
129 pub keybinds: std::collections::HashMap<String, String>,
130}
131
132impl Default for SynapsConfig {
133 fn default() -> Self {
134 Self {
135 model: None,
136 thinking_budget: None,
137 context_window: None,
138 compaction_model: None,
139 max_tool_output: 30000,
140 bash_timeout: 30,
141 bash_max_timeout: 300,
142 subagent_timeout: 300,
143 api_retries: 3,
144 theme: None,
145 agent_name: None,
146 disabled_plugins: Vec::new(),
147 favorite_models: Vec::new(),
148 disabled_skills: Vec::new(),
149 shell: ShellConfig::default(),
150 server: ServerConfig::default(),
151 provider_keys: BTreeMap::new(),
152 keybinds: std::collections::HashMap::new(),
153 }
154 }
155}
156
157
158fn parse_thinking_budget(val: &str) -> Option<u32> {
159 match val {
160 "low" => Some(2048),
161 "medium" => Some(4096),
162 "high" => Some(16384),
163 "xhigh" => Some(32768),
164 "adaptive" => Some(0), _ => val.parse::<u32>().ok(),
166 }
167}
168
169fn parse_comma_list(val: &str) -> Vec<String> {
170 val.split(',')
171 .map(|s| s.trim().to_string())
172 .filter(|s| !s.is_empty())
173 .collect()
174}
175
176fn write_comma_list(key: &str, values: &[String]) -> std::io::Result<()> {
177 write_config_value(key, &values.join(", "))
178}
179
180fn parse_shell_config_key(shell_config: &mut ShellConfig, key: &str, val: &str) {
182 match key {
183 "shell.max_sessions" => {
184 if let Ok(sessions) = val.parse::<usize>() {
185 shell_config.max_sessions = sessions;
186 } else {
187 eprintln!("Warning: invalid value for shell.max_sessions: '{}', using default", val);
188 }
189 }
190 "shell.idle_timeout" => {
191 if let Ok(timeout) = val.parse::<u64>() {
192 shell_config.idle_timeout = std::time::Duration::from_secs(timeout);
193 } else {
194 eprintln!("Warning: invalid value for shell.idle_timeout: '{}', using default", val);
195 }
196 }
197 "shell.readiness_timeout_ms" => {
198 if let Ok(timeout) = val.parse::<u64>() {
199 shell_config.readiness_timeout_ms = timeout;
200 } else {
201 eprintln!("Warning: invalid value for shell.readiness_timeout_ms: '{}', using default", val);
202 }
203 }
204 "shell.max_readiness_timeout_ms" => {
205 if let Ok(timeout) = val.parse::<u64>() {
206 shell_config.max_readiness_timeout_ms = timeout;
207 } else {
208 eprintln!("Warning: invalid value for shell.max_readiness_timeout_ms: '{}', using default", val);
209 }
210 }
211 "shell.default_rows" => {
212 if let Ok(rows) = val.parse::<u16>() {
213 shell_config.default_rows = rows;
214 } else {
215 eprintln!("Warning: invalid value for shell.default_rows: '{}', using default", val);
216 }
217 }
218 "shell.default_cols" => {
219 if let Ok(cols) = val.parse::<u16>() {
220 shell_config.default_cols = cols;
221 } else {
222 eprintln!("Warning: invalid value for shell.default_cols: '{}', using default", val);
223 }
224 }
225 "shell.readiness_strategy" => {
226 let val_lower = val.to_lowercase();
227 match val_lower.as_str() {
228 "timeout" | "prompt" | "hybrid" => {
229 shell_config.readiness_strategy = val.to_string();
230 }
231 _ => {
232 eprintln!("Warning: invalid value for shell.readiness_strategy: '{}', using default", val);
233 }
234 }
235 }
236 "shell.max_output" => {
237 if let Ok(max_output) = val.parse::<usize>() {
238 shell_config.max_output = max_output;
239 } else {
240 eprintln!("Warning: invalid value for shell.max_output: '{}', using default", val);
241 }
242 }
243 _ => {
244 }
246 }
247}
248
249fn parse_server_config_key(server_config: &mut ServerConfig, key: &str, val: &str) {
251 match key {
252 "server.allowed_origins" => {
253 server_config.allowed_origins = parse_comma_list(val);
254 }
255 "server.token" => {
256 if !val.is_empty() {
257 server_config.token = Some(val.to_string());
258 }
259 }
260 "server.auto_approve_confirms" => {
261 server_config.auto_approve_confirms = matches!(val, "true" | "1" | "yes");
262 }
263 "server.max_message_size" => {
264 if let Ok(size) = val.parse::<usize>() {
265 server_config.max_message_size = Some(size);
266 } else {
267 eprintln!("Warning: invalid value for server.max_message_size: '{}', ignored", val);
268 }
269 }
270 _ => {
271 }
273 }
274}
275
276pub fn load_config() -> SynapsConfig {
279 let path = resolve_read_path("config");
280 let mut config = SynapsConfig::default();
281
282 let Ok(content) = std::fs::read_to_string(&path) else {
283 return config;
284 };
285
286 for line in content.lines() {
287 let line = line.trim();
288 if line.is_empty() || line.starts_with('#') { continue; }
289 let Some((key, val)) = line.split_once('=') else { continue };
290 let key = key.trim();
291 let val = val.trim();
292 match key {
293 "model" => config.model = Some(val.to_string()),
294 "thinking" => config.thinking_budget = parse_thinking_budget(val),
295 "compaction_model" => config.compaction_model = Some(val.to_string()),
296 "context_window" => {
297 let parsed = match val {
298 "200k" | "200K" => Some(200_000),
299 "1m" | "1M" => Some(1_000_000),
300 _ => val.parse::<u64>().ok(),
301 };
302 config.context_window = parsed;
303 }
304 "max_tool_output" => {
305 if let Ok(size) = val.parse::<usize>() {
306 config.max_tool_output = size;
307 }
308 }
309 "bash_timeout" => {
310 if let Ok(timeout) = val.parse::<u64>() {
311 config.bash_timeout = timeout;
312 }
313 }
314 "bash_max_timeout" => {
315 if let Ok(timeout) = val.parse::<u64>() {
316 config.bash_max_timeout = timeout;
317 }
318 }
319 "subagent_timeout" => {
320 if let Ok(timeout) = val.parse::<u64>() {
321 config.subagent_timeout = timeout;
322 }
323 }
324 "api_retries" => {
325 if let Ok(retries) = val.parse::<u32>() {
326 config.api_retries = retries;
327 }
328 }
329 "theme" => config.theme = Some(val.to_string()),
330 "agent_name" => config.agent_name = Some(val.to_string()),
331 "disabled_plugins" => {
332 config.disabled_plugins = parse_comma_list(val);
333 }
334 "favorite_models" => {
335 config.favorite_models = parse_comma_list(val);
336 }
337 "disabled_skills" => {
338 config.disabled_skills = parse_comma_list(val);
339 }
340 _ => {
341 if key.starts_with("shell.") {
343 parse_shell_config_key(&mut config.shell, key, val);
344 } else if key.starts_with("server.") {
345 parse_server_config_key(&mut config.server, key, val);
346 } else if let Some(provider_key) = key.strip_prefix("provider.") {
347 config.provider_keys.insert(provider_key.to_string(), val.to_string());
348 } else if let Some(keybind_key) = key.strip_prefix("keybind.") {
349 config.keybinds.insert(keybind_key.to_string(), val.to_string());
350 }
351 }
353 }
354 }
355
356 if config.server.max_message_size.is_none() {
359 if let Some(ctx_tokens) = config.context_window {
360 config.server.max_message_size = Some((ctx_tokens as usize) * 4);
361 }
362 }
363
364 let _ = PROVIDER_KEYS.set(config.provider_keys.clone());
367
368 config
369}
370
371pub fn read_config_value(key: &str) -> Option<String> {
373 let path = resolve_read_path("config");
374 let content = std::fs::read_to_string(&path).ok()?;
375 for line in content.lines() {
376 let line = line.trim();
377 if line.is_empty() || line.starts_with('#') { continue; }
378 let Some((k, v)) = line.split_once('=') else { continue };
379 if k.trim() == key.trim() {
380 return Some(v.trim().to_string());
381 }
382 }
383 None
384}
385
386pub fn write_config_value(key: &str, value: &str) -> std::io::Result<()> {
390 let path = resolve_write_path("config");
391 let existing = std::fs::read_to_string(&path).unwrap_or_default();
392
393 let key_trimmed = key.trim();
394 let replacement = format!("{} = {}", key_trimmed, value);
395
396 let mut found = false;
397 let mut new_lines: Vec<String> = existing.lines().map(|line| {
398 if found { return line.to_string(); }
399 let t = line.trim_start();
400 if t.starts_with('#') || t.is_empty() { return line.to_string(); }
401 if let Some((k, _)) = t.split_once('=') {
402 if k.trim() == key_trimmed {
403 found = true;
404 return replacement.clone();
405 }
406 }
407 line.to_string()
408 }).collect();
409
410 if !found {
411 new_lines.push(replacement);
412 }
413
414 let mut out = new_lines.join("\n");
415 if !out.ends_with('\n') { out.push('\n'); }
416
417 let tmp = path.with_extension("tmp");
418 std::fs::write(&tmp, out)?;
419 #[cfg(unix)]
421 {
422 use std::os::unix::fs::PermissionsExt;
423 std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o600))?;
424 }
425 std::fs::rename(&tmp, &path)?;
426 Ok(())
427}
428
429pub fn add_favorite_model(id: &str) -> std::io::Result<()> {
431 let trimmed = id.trim();
432 if trimmed.is_empty() {
433 return Ok(());
434 }
435 let mut values = load_config().favorite_models;
436 if !values.iter().any(|v| v == trimmed) {
437 values.push(trimmed.to_string());
438 values.sort();
439 }
440 write_comma_list("favorite_models", &values)
441}
442
443pub fn remove_favorite_model(id: &str) -> std::io::Result<()> {
445 let mut values = load_config().favorite_models;
446 values.retain(|v| v != id.trim());
447 write_comma_list("favorite_models", &values)
448}
449
450pub fn is_favorite_model(id: &str) -> bool {
452 load_config().favorite_models.iter().any(|v| v == id.trim())
453}
454
455pub fn resolve_system_prompt(explicit: Option<&str>) -> String {
458 const DEFAULT_PROMPT: &str = "You are a helpful AI agent running in a terminal. \
459 You have access to bash, read, and write tools. \
460 Be concise and direct. Use tools when the user asks you to interact with the filesystem or run commands.";
461
462 if let Some(val) = explicit {
463 let path = std::path::Path::new(val);
464 if path.exists() && path.is_file() {
465 return std::fs::read_to_string(path).unwrap_or_else(|_| val.to_string());
466 }
467 return val.to_string();
468 }
469
470 let system_path = resolve_read_path("system.md");
471 if system_path.exists() {
472 return std::fs::read_to_string(&system_path).unwrap_or_default();
473 }
474
475 DEFAULT_PROMPT.to_string()
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use serial_test::serial;
482
483 #[test]
484 fn test_parse_thinking_budget() {
485 assert_eq!(parse_thinking_budget("low"), Some(2048));
486 assert_eq!(parse_thinking_budget("medium"), Some(4096));
487 assert_eq!(parse_thinking_budget("high"), Some(16384));
488 assert_eq!(parse_thinking_budget("xhigh"), Some(32768));
489 assert_eq!(parse_thinking_budget("8192"), Some(8192));
490 assert_eq!(parse_thinking_budget("invalid"), None);
491 }
492
493 #[test]
494 fn test_base_dir() {
495 let path = base_dir();
496 assert!(path.to_string_lossy().ends_with(".synaps-cli"));
497 }
498
499 #[test]
500 fn test_resolve_system_prompt_explicit() {
501 let result = resolve_system_prompt(Some("test prompt"));
502 assert_eq!(result, "test prompt");
503 }
504
505 #[test]
506 fn test_resolve_system_prompt_none() {
507 let result = resolve_system_prompt(None);
508 assert!(result.contains("helpful AI agent"));
509 }
510
511 #[test]
516 fn test_synaps_config_default() {
517 let config = SynapsConfig::default();
518 assert_eq!(config.model, None);
519 assert_eq!(config.thinking_budget, None);
520 assert_eq!(config.max_tool_output, 30000);
521 assert_eq!(config.bash_timeout, 30);
522 assert_eq!(config.bash_max_timeout, 300);
523 assert_eq!(config.subagent_timeout, 300);
524 assert_eq!(config.api_retries, 3);
525 assert_eq!(config.theme, None);
526 assert!(config.disabled_plugins.is_empty());
527 assert!(config.favorite_models.is_empty());
528 assert!(config.disabled_skills.is_empty());
529 assert_eq!(config.shell.max_sessions, 5);
530 assert_eq!(config.shell.idle_timeout.as_secs(), 600);
531 assert!(config.server.allowed_origins.is_empty());
533 assert_eq!(config.server.token, None);
534 assert!(!config.server.auto_approve_confirms);
535 assert_eq!(config.server.max_message_size, None);
536 }
537
538 #[test]
539 #[serial]
540 fn test_load_config_server_keys() {
541 let home = make_test_home("server-keys");
542 let cfg = home.join(".synaps-cli/config");
543 std::fs::write(&cfg, "\
544server.allowed_origins = http://localhost:3000, http://localhost:5193\n\
545server.token = my-secret-token\n\
546server.auto_approve_confirms = true\n\
547server.max_message_size = 65536\n\
548context_window = 200k\n\
549").unwrap();
550
551 with_home(&home, || {
552 let config = load_config();
553 assert_eq!(config.server.allowed_origins, vec![
554 "http://localhost:3000".to_string(),
555 "http://localhost:5193".to_string(),
556 ]);
557 assert_eq!(config.server.token, Some("my-secret-token".to_string()));
558 assert!(config.server.auto_approve_confirms);
559 assert_eq!(config.server.max_message_size, Some(65536));
561 });
562
563 let _ = std::fs::remove_dir_all(&home);
564 }
565
566 #[test]
567 #[serial]
568 fn test_server_max_message_size_derived_from_context_window() {
569 let home = make_test_home("server-derive");
570 let cfg = home.join(".synaps-cli/config");
571 std::fs::write(&cfg, "context_window = 200k\n").unwrap();
572
573 with_home(&home, || {
574 let config = load_config();
575 assert_eq!(config.server.max_message_size, Some(800_000));
577 });
578
579 let _ = std::fs::remove_dir_all(&home);
580 }
581
582 fn make_test_home(subdir: &str) -> std::path::PathBuf {
583 let dir = std::path::PathBuf::from(format!("/tmp/synaps-write-test-{}", subdir));
584 let _ = std::fs::remove_dir_all(&dir);
585 std::fs::create_dir_all(dir.join(".synaps-cli")).unwrap();
586 dir
587 }
588
589 fn with_home<F: FnOnce()>(home: &std::path::Path, f: F) {
590 let original = std::env::var("HOME").ok();
591 std::env::set_var("HOME", home);
592 f();
593 if let Some(h) = original {
594 std::env::set_var("HOME", h);
595 } else {
596 std::env::remove_var("HOME");
597 }
598 }
599
600 #[test]
601 #[serial]
602 fn write_config_value_replaces_existing_key() {
603 let home = make_test_home("replace");
604 let cfg = home.join(".synaps-cli/config");
605 std::fs::write(&cfg, "model = claude-opus-4-6\nthinking = low\n").unwrap();
606
607 with_home(&home, || {
608 write_config_value("model", "claude-sonnet-4-6").unwrap();
609 });
610
611 let contents = std::fs::read_to_string(&cfg).unwrap();
612 assert!(contents.contains("model = claude-sonnet-4-6"));
613 assert!(contents.contains("thinking = low"));
614 let _ = std::fs::remove_dir_all(&home);
615 }
616
617 #[test]
618 #[serial]
619 fn write_config_value_appends_when_missing() {
620 let home = make_test_home("append");
621 let cfg = home.join(".synaps-cli/config");
622 std::fs::write(&cfg, "model = claude-opus-4-6\n").unwrap();
623
624 with_home(&home, || {
625 write_config_value("theme", "dracula").unwrap();
626 });
627
628 let contents = std::fs::read_to_string(&cfg).unwrap();
629 assert!(contents.contains("model = claude-opus-4-6"));
630 assert!(contents.contains("theme = dracula"));
631 let _ = std::fs::remove_dir_all(&home);
632 }
633
634 #[test]
635 #[serial]
636 fn write_config_value_preserves_comments() {
637 let home = make_test_home("comments");
638 let cfg = home.join(".synaps-cli/config");
639 std::fs::write(&cfg, "# user comment\nmodel = claude-opus-4-6\n# another\n").unwrap();
640
641 with_home(&home, || {
642 write_config_value("model", "claude-sonnet-4-6").unwrap();
643 });
644
645 let contents = std::fs::read_to_string(&cfg).unwrap();
646 assert!(contents.contains("# user comment"));
647 assert!(contents.contains("# another"));
648 assert!(contents.contains("model = claude-sonnet-4-6"));
649 let _ = std::fs::remove_dir_all(&home);
650 }
651
652 #[test]
653 #[serial]
654 fn write_config_value_preserves_unknown_keys() {
655 let home = make_test_home("unknown");
656 let cfg = home.join(".synaps-cli/config");
657 std::fs::write(&cfg, "custom_thing = 42\nmodel = claude-opus-4-6\n").unwrap();
658
659 with_home(&home, || {
660 write_config_value("model", "claude-sonnet-4-6").unwrap();
661 });
662
663 let contents = std::fs::read_to_string(&cfg).unwrap();
664 assert!(contents.contains("custom_thing = 42"));
665 let _ = std::fs::remove_dir_all(&home);
666 }
667
668 #[test]
669 #[serial]
670 fn write_config_value_creates_file_if_absent() {
671 let home = make_test_home("create");
672 let cfg = home.join(".synaps-cli/config");
673 assert!(!cfg.exists());
674
675 with_home(&home, || {
676 write_config_value("model", "claude-sonnet-4-6").unwrap();
677 });
678
679 let contents = std::fs::read_to_string(&cfg).unwrap();
680 assert!(contents.contains("model = claude-sonnet-4-6"));
681 let _ = std::fs::remove_dir_all(&home);
682 }
683
684 #[test]
685 #[serial]
686 fn load_config_parses_theme_key() {
687 let dir = std::path::PathBuf::from("/tmp/synaps-config-test-theme/.synaps-cli");
688 let _ = std::fs::create_dir_all(&dir);
689 std::fs::write(dir.join("config"), "theme = dracula\n").unwrap();
690
691 let original_home = std::env::var("HOME").ok();
692 std::env::set_var("HOME", "/tmp/synaps-config-test-theme");
693
694 let config = load_config();
695
696 if let Some(home) = original_home {
697 std::env::set_var("HOME", home);
698 } else {
699 std::env::remove_var("HOME");
700 }
701 let _ = std::fs::remove_dir_all("/tmp/synaps-config-test-theme");
702
703 assert_eq!(config.theme.as_deref(), Some("dracula"));
704 }
705
706 #[test]
707 #[serial]
708 fn test_load_config_disable_lists() {
709 let test_dir = std::path::PathBuf::from("/tmp/synaps-config-test-disable-lists/.synaps-cli");
710 let _ = std::fs::create_dir_all(&test_dir);
711 let config_path = test_dir.join("config");
712
713 let config_content = r#"
714# Test config with disable lists
715favorite_models = claude/claude-opus-4-7, groq/llama-3.3-70b-versatile
716
717disabled_plugins = foo, bar
718disabled_skills = baz, plug:qual
719"#;
720 std::fs::write(&config_path, config_content).unwrap();
721
722 let original_home = std::env::var("HOME").ok();
723 std::env::set_var("HOME", "/tmp/synaps-config-test-disable-lists");
724
725 let config = load_config();
726
727 if let Some(home) = original_home {
728 std::env::set_var("HOME", home);
729 } else {
730 std::env::remove_var("HOME");
731 }
732
733 let _ = std::fs::remove_dir_all("/tmp/synaps-config-test-disable-lists");
734
735 assert_eq!(config.disabled_plugins, vec!["foo".to_string(), "bar".to_string()]);
736 assert_eq!(config.favorite_models, vec![
737 "claude/claude-opus-4-7".to_string(),
738 "groq/llama-3.3-70b-versatile".to_string(),
739 ]);
740 assert_eq!(config.disabled_skills, vec!["baz".to_string(), "plug:qual".to_string()]);
741 }
742
743 #[test]
744 #[serial]
745 fn favorite_model_helpers_round_trip_through_config_file() {
746 let home = make_test_home("favorite-models");
747 let cfg = home.join(".synaps-cli/config");
748 std::fs::write(&cfg, "model = claude-opus-4-7\n").unwrap();
749
750 with_home(&home, || {
751 add_favorite_model("groq/llama-3.3-70b-versatile").unwrap();
752 add_favorite_model("claude/claude-opus-4-7").unwrap();
753 add_favorite_model("groq/llama-3.3-70b-versatile").unwrap();
754 assert!(is_favorite_model("groq/llama-3.3-70b-versatile"));
755 remove_favorite_model("groq/llama-3.3-70b-versatile").unwrap();
756 assert!(!is_favorite_model("groq/llama-3.3-70b-versatile"));
757 assert!(is_favorite_model("claude/claude-opus-4-7"));
758 });
759
760 let contents = std::fs::read_to_string(&cfg).unwrap();
761 assert!(contents.contains("model = claude-opus-4-7"));
762 assert!(contents.contains("favorite_models = claude/claude-opus-4-7"));
763 let _ = std::fs::remove_dir_all(&home);
764 }
765
766 #[test]
767 #[serial]
768 fn test_load_config_new_keys() {
769 let test_dir = std::path::PathBuf::from("/tmp/synaps-config-test-new-keys/.synaps-cli");
771 let _ = std::fs::create_dir_all(&test_dir);
772 let config_path = test_dir.join("config");
773
774 let config_content = r#"
775# Test config with new keys
776model = claude-haiku
777thinking = medium
778max_tool_output = 50000
779bash_timeout = 45
780bash_max_timeout = 600
781subagent_timeout = 120
782api_retries = 5
783"#;
784 std::fs::write(&config_path, config_content).unwrap();
785
786 let original_home = std::env::var("HOME").ok();
788 std::env::set_var("HOME", "/tmp/synaps-config-test-new-keys");
789
790 let config = load_config();
791
792 if let Some(home) = original_home {
794 std::env::set_var("HOME", home);
795 } else {
796 std::env::remove_var("HOME");
797 }
798
799 let _ = std::fs::remove_dir_all("/tmp/synaps-config-test-new-keys");
801
802 assert_eq!(config.model, Some("claude-haiku".to_string()));
803 assert_eq!(config.thinking_budget, Some(4096)); assert_eq!(config.max_tool_output, 50000);
805 assert_eq!(config.bash_timeout, 45);
806 assert_eq!(config.bash_max_timeout, 600);
807 assert_eq!(config.subagent_timeout, 120);
808 assert_eq!(config.api_retries, 5);
809 }
810}