1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(default)]
13#[derive(Default)]
14pub struct Config {
15 pub api: ApiConfig,
16 pub permissions: PermissionsConfig,
17 pub ui: UiConfig,
18 #[serde(default)]
20 pub features: FeaturesConfig,
21 #[serde(default)]
23 pub mcp_servers: std::collections::HashMap<String, McpServerEntry>,
24 #[serde(default)]
26 pub hooks: Vec<HookDefinition>,
27 #[serde(default)]
29 pub security: SecurityConfig,
30}
31
32#[derive(Debug, Clone, Default, Serialize, Deserialize)]
34#[serde(default)]
35pub struct SecurityConfig {
36 #[serde(default)]
38 pub additional_directories: Vec<String>,
39 #[serde(default)]
41 pub mcp_server_allowlist: Vec<String>,
42 #[serde(default)]
44 pub mcp_server_denylist: Vec<String>,
45 #[serde(default)]
47 pub disable_bypass_permissions: bool,
48 #[serde(default)]
50 pub env_allowlist: Vec<String>,
51 #[serde(default)]
53 pub disable_skill_shell_execution: bool,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct McpServerEntry {
59 pub command: Option<String>,
61 #[serde(default)]
63 pub args: Vec<String>,
64 pub url: Option<String>,
66 #[serde(default)]
68 pub env: std::collections::HashMap<String, String>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(default)]
78pub struct ApiConfig {
79 pub base_url: String,
81 pub model: String,
83 #[serde(skip_serializing)]
86 pub api_key: Option<String>,
87 pub max_output_tokens: Option<u32>,
89 pub thinking: Option<String>,
91 pub effort: Option<String>,
93 pub max_cost_usd: Option<f64>,
95 pub timeout_secs: u64,
97 pub max_retries: u32,
99}
100
101impl Default for ApiConfig {
102 fn default() -> Self {
103 let api_key = std::env::var("AGENT_CODE_API_KEY")
105 .or_else(|_| std::env::var("ANTHROPIC_API_KEY"))
106 .or_else(|_| std::env::var("OPENAI_API_KEY"))
107 .or_else(|_| std::env::var("AZURE_OPENAI_API_KEY"))
108 .or_else(|_| std::env::var("XAI_API_KEY"))
109 .or_else(|_| std::env::var("GOOGLE_API_KEY"))
110 .or_else(|_| std::env::var("DEEPSEEK_API_KEY"))
111 .or_else(|_| std::env::var("GROQ_API_KEY"))
112 .or_else(|_| std::env::var("MISTRAL_API_KEY"))
113 .or_else(|_| std::env::var("ZHIPU_API_KEY"))
114 .or_else(|_| std::env::var("TOGETHER_API_KEY"))
115 .or_else(|_| std::env::var("OPENROUTER_API_KEY"))
116 .or_else(|_| std::env::var("COHERE_API_KEY"))
117 .or_else(|_| std::env::var("PERPLEXITY_API_KEY"))
118 .ok();
119
120 let use_bedrock = std::env::var("AGENT_CODE_USE_BEDROCK").is_ok()
123 || std::env::var("AWS_REGION").is_ok() && api_key.is_some();
124 let use_vertex = std::env::var("AGENT_CODE_USE_VERTEX").is_ok();
125 let use_azure = std::env::var("AZURE_OPENAI_ENDPOINT").is_ok()
126 || std::env::var("AZURE_OPENAI_API_KEY").is_ok();
127
128 let has_generic = std::env::var("AGENT_CODE_API_KEY").is_ok();
129 let base_url = if use_bedrock {
130 let region = std::env::var("AWS_REGION").unwrap_or_else(|_| "us-east-1".to_string());
132 format!("https://bedrock-runtime.{region}.amazonaws.com")
133 } else if use_vertex {
134 let project = std::env::var("GOOGLE_CLOUD_PROJECT").unwrap_or_default();
136 let location = std::env::var("GOOGLE_CLOUD_LOCATION")
137 .unwrap_or_else(|_| "us-central1".to_string());
138 format!(
139 "https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/publishers/anthropic/models"
140 )
141 } else if use_azure {
142 std::env::var("AZURE_OPENAI_ENDPOINT").unwrap_or_else(|_| {
144 "https://YOUR_RESOURCE.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT"
145 .to_string()
146 })
147 } else if has_generic {
148 "https://api.openai.com/v1".to_string()
150 } else if std::env::var("GOOGLE_API_KEY").is_ok() {
151 "https://generativelanguage.googleapis.com/v1beta/openai".to_string()
152 } else if std::env::var("DEEPSEEK_API_KEY").is_ok() {
153 "https://api.deepseek.com/v1".to_string()
154 } else if std::env::var("XAI_API_KEY").is_ok() {
155 "https://api.x.ai/v1".to_string()
156 } else if std::env::var("GROQ_API_KEY").is_ok() {
157 "https://api.groq.com/openai/v1".to_string()
158 } else if std::env::var("MISTRAL_API_KEY").is_ok() {
159 "https://api.mistral.ai/v1".to_string()
160 } else if std::env::var("TOGETHER_API_KEY").is_ok() {
161 "https://api.together.xyz/v1".to_string()
162 } else if std::env::var("OPENROUTER_API_KEY").is_ok() {
163 "https://openrouter.ai/api/v1".to_string()
164 } else if std::env::var("COHERE_API_KEY").is_ok() {
165 "https://api.cohere.com/v2".to_string()
166 } else if std::env::var("PERPLEXITY_API_KEY").is_ok() {
167 "https://api.perplexity.ai".to_string()
168 } else {
169 "https://api.openai.com/v1".to_string()
171 };
172
173 Self {
174 base_url,
175 model: "gpt-5.4".to_string(),
176 api_key,
177 max_output_tokens: Some(16384),
178 thinking: None,
179 effort: None,
180 max_cost_usd: None,
181 timeout_secs: 120,
182 max_retries: 3,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(default)]
190pub struct PermissionsConfig {
191 pub default_mode: PermissionMode,
193 pub rules: Vec<PermissionRule>,
195}
196
197impl Default for PermissionsConfig {
198 fn default() -> Self {
199 Self {
200 default_mode: PermissionMode::Ask,
201 rules: Vec::new(),
202 }
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum PermissionMode {
212 Allow,
214 Deny,
216 Ask,
218 AcceptEdits,
220 Plan,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct PermissionRule {
227 pub tool: String,
229 pub pattern: Option<String>,
231 pub action: PermissionMode,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(default)]
238pub struct UiConfig {
239 pub markdown: bool,
241 pub syntax_highlight: bool,
243 pub theme: String,
245 pub edit_mode: String,
247}
248
249impl Default for UiConfig {
250 fn default() -> Self {
251 Self {
252 markdown: true,
253 syntax_highlight: true,
254 theme: "dark".to_string(),
255 edit_mode: "emacs".to_string(),
256 }
257 }
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
263#[serde(default)]
264pub struct FeaturesConfig {
265 pub token_budget: bool,
267 pub commit_attribution: bool,
269 pub compaction_reminders: bool,
271 pub unattended_retry: bool,
273 pub history_snip: bool,
275 pub auto_theme: bool,
277 pub mcp_rich_output: bool,
279 pub fork_conversation: bool,
281 pub verification_agent: bool,
283 pub extract_memories: bool,
285 pub context_collapse: bool,
287 pub reactive_compact: bool,
289 pub prompt_caching: bool,
292}
293
294impl Default for FeaturesConfig {
295 fn default() -> Self {
296 Self {
297 token_budget: true,
298 commit_attribution: true,
299 compaction_reminders: true,
300 unattended_retry: true,
301 history_snip: true,
302 auto_theme: true,
303 mcp_rich_output: true,
304 fork_conversation: true,
305 verification_agent: true,
306 extract_memories: true,
307 context_collapse: true,
308 reactive_compact: true,
309 prompt_caching: true,
310 }
311 }
312}
313
314#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
318#[serde(rename_all = "snake_case")]
319pub enum HookEvent {
320 SessionStart,
321 SessionStop,
322 PreToolUse,
323 PostToolUse,
324 UserPromptSubmit,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
329#[serde(tag = "type")]
330pub enum HookAction {
331 #[serde(rename = "shell")]
333 Shell { command: String },
334 #[serde(rename = "http")]
336 Http { url: String, method: Option<String> },
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct HookDefinition {
342 pub event: HookEvent,
343 pub action: HookAction,
344 pub tool_name: Option<String>,
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
355 fn api_config_default_model() {
356 let cfg = ApiConfig::default();
357 assert_eq!(cfg.model, "gpt-5.4");
358 }
359
360 #[test]
361 fn api_config_default_timeout() {
362 let cfg = ApiConfig::default();
363 assert_eq!(cfg.timeout_secs, 120);
364 }
365
366 #[test]
367 fn api_config_default_max_retries() {
368 let cfg = ApiConfig::default();
369 assert_eq!(cfg.max_retries, 3);
370 }
371
372 #[test]
373 fn api_config_default_max_output_tokens() {
374 let cfg = ApiConfig::default();
375 assert_eq!(cfg.max_output_tokens, Some(16384));
376 }
377
378 #[test]
379 fn api_config_default_base_url_contains_scheme() {
380 let cfg = ApiConfig::default();
381 assert!(
382 cfg.base_url.starts_with("https://"),
383 "base_url should start with https://, got: {}",
384 cfg.base_url
385 );
386 }
387
388 #[test]
389 fn api_config_default_thinking_is_none() {
390 let cfg = ApiConfig::default();
391 assert!(cfg.thinking.is_none());
392 }
393
394 #[test]
395 fn api_config_default_effort_is_none() {
396 let cfg = ApiConfig::default();
397 assert!(cfg.effort.is_none());
398 }
399
400 #[test]
401 fn api_config_default_max_cost_is_none() {
402 let cfg = ApiConfig::default();
403 assert!(cfg.max_cost_usd.is_none());
404 }
405
406 #[test]
409 fn permissions_config_default_mode_is_ask() {
410 let cfg = PermissionsConfig::default();
411 assert_eq!(cfg.default_mode, PermissionMode::Ask);
412 }
413
414 #[test]
415 fn permissions_config_default_rules_empty() {
416 let cfg = PermissionsConfig::default();
417 assert!(cfg.rules.is_empty());
418 }
419
420 #[test]
423 fn ui_config_default_markdown_true() {
424 let cfg = UiConfig::default();
425 assert!(cfg.markdown);
426 }
427
428 #[test]
429 fn ui_config_default_syntax_highlight_true() {
430 let cfg = UiConfig::default();
431 assert!(cfg.syntax_highlight);
432 }
433
434 #[test]
435 fn ui_config_default_theme_dark() {
436 let cfg = UiConfig::default();
437 assert_eq!(cfg.theme, "dark");
438 }
439
440 #[test]
441 fn ui_config_default_edit_mode_emacs() {
442 let cfg = UiConfig::default();
443 assert_eq!(cfg.edit_mode, "emacs");
444 }
445
446 #[test]
449 fn features_config_default_all_true() {
450 let cfg = FeaturesConfig::default();
451 assert!(cfg.token_budget);
452 assert!(cfg.commit_attribution);
453 assert!(cfg.compaction_reminders);
454 assert!(cfg.unattended_retry);
455 assert!(cfg.history_snip);
456 assert!(cfg.auto_theme);
457 assert!(cfg.mcp_rich_output);
458 assert!(cfg.fork_conversation);
459 assert!(cfg.verification_agent);
460 assert!(cfg.extract_memories);
461 assert!(cfg.context_collapse);
462 assert!(cfg.reactive_compact);
463 }
464
465 #[test]
468 fn security_config_default_empty_vecs() {
469 let cfg = SecurityConfig::default();
470 assert!(cfg.additional_directories.is_empty());
471 assert!(cfg.mcp_server_allowlist.is_empty());
472 assert!(cfg.mcp_server_denylist.is_empty());
473 assert!(cfg.env_allowlist.is_empty());
474 }
475
476 #[test]
477 fn security_config_default_booleans_false() {
478 let cfg = SecurityConfig::default();
479 assert!(!cfg.disable_bypass_permissions);
480 assert!(!cfg.disable_skill_shell_execution);
481 }
482
483 #[test]
486 fn config_default_composes_sub_defaults() {
487 let cfg = Config::default();
488 assert_eq!(cfg.api.model, "gpt-5.4");
489 assert_eq!(cfg.permissions.default_mode, PermissionMode::Ask);
490 assert!(cfg.ui.markdown);
491 assert!(cfg.features.token_budget);
492 assert!(cfg.mcp_servers.is_empty());
493 assert!(cfg.hooks.is_empty());
494 assert!(cfg.security.additional_directories.is_empty());
495 }
496
497 #[test]
500 fn permission_mode_serde_roundtrip_allow() {
501 let json = serde_json::to_string(&PermissionMode::Allow).unwrap();
502 assert_eq!(json, "\"allow\"");
503 let back: PermissionMode = serde_json::from_str(&json).unwrap();
504 assert_eq!(back, PermissionMode::Allow);
505 }
506
507 #[test]
508 fn permission_mode_serde_roundtrip_deny() {
509 let json = serde_json::to_string(&PermissionMode::Deny).unwrap();
510 assert_eq!(json, "\"deny\"");
511 let back: PermissionMode = serde_json::from_str(&json).unwrap();
512 assert_eq!(back, PermissionMode::Deny);
513 }
514
515 #[test]
516 fn permission_mode_serde_roundtrip_ask() {
517 let json = serde_json::to_string(&PermissionMode::Ask).unwrap();
518 assert_eq!(json, "\"ask\"");
519 let back: PermissionMode = serde_json::from_str(&json).unwrap();
520 assert_eq!(back, PermissionMode::Ask);
521 }
522
523 #[test]
524 fn permission_mode_serde_roundtrip_accept_edits() {
525 let json = serde_json::to_string(&PermissionMode::AcceptEdits).unwrap();
526 assert_eq!(json, "\"accept_edits\"");
527 let back: PermissionMode = serde_json::from_str(&json).unwrap();
528 assert_eq!(back, PermissionMode::AcceptEdits);
529 }
530
531 #[test]
532 fn permission_mode_serde_roundtrip_plan() {
533 let json = serde_json::to_string(&PermissionMode::Plan).unwrap();
534 assert_eq!(json, "\"plan\"");
535 let back: PermissionMode = serde_json::from_str(&json).unwrap();
536 assert_eq!(back, PermissionMode::Plan);
537 }
538
539 #[test]
542 fn hook_event_serde_roundtrip_session_start() {
543 let json = serde_json::to_string(&HookEvent::SessionStart).unwrap();
544 assert_eq!(json, "\"session_start\"");
545 let back: HookEvent = serde_json::from_str(&json).unwrap();
546 assert_eq!(back, HookEvent::SessionStart);
547 }
548
549 #[test]
550 fn hook_event_serde_roundtrip_session_stop() {
551 let json = serde_json::to_string(&HookEvent::SessionStop).unwrap();
552 assert_eq!(json, "\"session_stop\"");
553 let back: HookEvent = serde_json::from_str(&json).unwrap();
554 assert_eq!(back, HookEvent::SessionStop);
555 }
556
557 #[test]
558 fn hook_event_serde_roundtrip_pre_tool_use() {
559 let json = serde_json::to_string(&HookEvent::PreToolUse).unwrap();
560 assert_eq!(json, "\"pre_tool_use\"");
561 let back: HookEvent = serde_json::from_str(&json).unwrap();
562 assert_eq!(back, HookEvent::PreToolUse);
563 }
564
565 #[test]
566 fn hook_event_serde_roundtrip_post_tool_use() {
567 let json = serde_json::to_string(&HookEvent::PostToolUse).unwrap();
568 assert_eq!(json, "\"post_tool_use\"");
569 let back: HookEvent = serde_json::from_str(&json).unwrap();
570 assert_eq!(back, HookEvent::PostToolUse);
571 }
572
573 #[test]
574 fn hook_event_serde_roundtrip_user_prompt_submit() {
575 let json = serde_json::to_string(&HookEvent::UserPromptSubmit).unwrap();
576 assert_eq!(json, "\"user_prompt_submit\"");
577 let back: HookEvent = serde_json::from_str(&json).unwrap();
578 assert_eq!(back, HookEvent::UserPromptSubmit);
579 }
580
581 #[test]
584 fn hook_action_serde_roundtrip_shell() {
585 let action = HookAction::Shell {
586 command: "echo hello".into(),
587 };
588 let json = serde_json::to_string(&action).unwrap();
589 assert!(json.contains("\"type\":\"shell\""));
590 assert!(json.contains("\"command\":\"echo hello\""));
591 let back: HookAction = serde_json::from_str(&json).unwrap();
592 match back {
593 HookAction::Shell { command } => assert_eq!(command, "echo hello"),
594 _ => panic!("expected Shell variant"),
595 }
596 }
597
598 #[test]
599 fn hook_action_serde_roundtrip_http() {
600 let action = HookAction::Http {
601 url: "https://example.com/hook".into(),
602 method: Some("POST".into()),
603 };
604 let json = serde_json::to_string(&action).unwrap();
605 assert!(json.contains("\"type\":\"http\""));
606 let back: HookAction = serde_json::from_str(&json).unwrap();
607 match back {
608 HookAction::Http { url, method } => {
609 assert_eq!(url, "https://example.com/hook");
610 assert_eq!(method.unwrap(), "POST");
611 }
612 _ => panic!("expected Http variant"),
613 }
614 }
615
616 #[test]
617 fn hook_action_http_method_none() {
618 let action = HookAction::Http {
619 url: "https://example.com".into(),
620 method: None,
621 };
622 let json = serde_json::to_string(&action).unwrap();
623 let back: HookAction = serde_json::from_str(&json).unwrap();
624 match back {
625 HookAction::Http { method, .. } => assert!(method.is_none()),
626 _ => panic!("expected Http variant"),
627 }
628 }
629
630 #[test]
633 fn hook_definition_serde_roundtrip() {
634 let def = HookDefinition {
635 event: HookEvent::PreToolUse,
636 action: HookAction::Shell {
637 command: "lint.sh".into(),
638 },
639 tool_name: Some("Bash".into()),
640 };
641 let json = serde_json::to_string(&def).unwrap();
642 let back: HookDefinition = serde_json::from_str(&json).unwrap();
643 assert_eq!(back.event, HookEvent::PreToolUse);
644 assert_eq!(back.tool_name, Some("Bash".into()));
645 }
646
647 #[test]
648 fn hook_definition_tool_name_none() {
649 let def = HookDefinition {
650 event: HookEvent::SessionStart,
651 action: HookAction::Shell {
652 command: "setup.sh".into(),
653 },
654 tool_name: None,
655 };
656 let json = serde_json::to_string(&def).unwrap();
657 let back: HookDefinition = serde_json::from_str(&json).unwrap();
658 assert!(back.tool_name.is_none());
659 }
660
661 #[test]
664 fn config_toml_deserialization_full() {
665 let toml_str = r#"
666[api]
667model = "test-model"
668timeout_secs = 60
669max_retries = 5
670base_url = "https://api.test.com/v1"
671
672[permissions]
673default_mode = "allow"
674
675[ui]
676markdown = false
677syntax_highlight = false
678theme = "light"
679edit_mode = "vi"
680
681[features]
682token_budget = false
683commit_attribution = false
684
685[security]
686disable_bypass_permissions = true
687additional_directories = ["/tmp"]
688"#;
689 let cfg: Config = toml::from_str(toml_str).unwrap();
690 assert_eq!(cfg.api.model, "test-model");
691 assert_eq!(cfg.api.timeout_secs, 60);
692 assert_eq!(cfg.api.max_retries, 5);
693 assert_eq!(cfg.api.base_url, "https://api.test.com/v1");
694 assert_eq!(cfg.permissions.default_mode, PermissionMode::Allow);
695 assert!(!cfg.ui.markdown);
696 assert!(!cfg.ui.syntax_highlight);
697 assert_eq!(cfg.ui.theme, "light");
698 assert_eq!(cfg.ui.edit_mode, "vi");
699 assert!(!cfg.features.token_budget);
700 assert!(!cfg.features.commit_attribution);
701 assert!(cfg.security.disable_bypass_permissions);
702 assert_eq!(cfg.security.additional_directories, vec!["/tmp"]);
703 }
704
705 #[test]
706 fn config_toml_empty_string_uses_defaults() {
707 let cfg: Config = toml::from_str("").unwrap();
708 assert_eq!(cfg.api.timeout_secs, 120);
709 assert_eq!(cfg.permissions.default_mode, PermissionMode::Ask);
710 assert!(cfg.ui.markdown);
711 }
712
713 #[test]
714 fn config_toml_partial_override() {
715 let toml_str = r#"
716[ui]
717theme = "solarized"
718"#;
719 let cfg: Config = toml::from_str(toml_str).unwrap();
720 assert_eq!(cfg.ui.theme, "solarized");
722 assert!(cfg.ui.markdown);
724 assert!(cfg.ui.syntax_highlight);
725 assert_eq!(cfg.ui.edit_mode, "emacs");
726 }
727
728 #[test]
731 fn mcp_server_entry_with_command() {
732 let json = r#"{"command": "npx mcp-server", "args": ["--port", "3000"]}"#;
733 let entry: McpServerEntry = serde_json::from_str(json).unwrap();
734 assert_eq!(entry.command, Some("npx mcp-server".into()));
735 assert_eq!(entry.args, vec!["--port", "3000"]);
736 assert!(entry.url.is_none());
737 }
738
739 #[test]
740 fn mcp_server_entry_with_url() {
741 let json = r#"{"url": "https://mcp.example.com/sse"}"#;
742 let entry: McpServerEntry = serde_json::from_str(json).unwrap();
743 assert!(entry.command.is_none());
744 assert_eq!(entry.url, Some("https://mcp.example.com/sse".into()));
745 assert!(entry.args.is_empty());
746 }
747
748 #[test]
749 fn mcp_server_entry_with_env() {
750 let json = r#"{"command": "server", "env": {"TOKEN": "abc"}}"#;
751 let entry: McpServerEntry = serde_json::from_str(json).unwrap();
752 assert_eq!(entry.env.get("TOKEN").unwrap(), "abc");
753 }
754
755 #[test]
758 fn permission_rule_serde_roundtrip_with_pattern() {
759 let rule = PermissionRule {
760 tool: "Bash".into(),
761 pattern: Some("rm -rf *".into()),
762 action: PermissionMode::Deny,
763 };
764 let json = serde_json::to_string(&rule).unwrap();
765 let back: PermissionRule = serde_json::from_str(&json).unwrap();
766 assert_eq!(back.tool, "Bash");
767 assert_eq!(back.pattern, Some("rm -rf *".into()));
768 assert_eq!(back.action, PermissionMode::Deny);
769 }
770
771 #[test]
772 fn permission_rule_serde_roundtrip_without_pattern() {
773 let rule = PermissionRule {
774 tool: "Read".into(),
775 pattern: None,
776 action: PermissionMode::Allow,
777 };
778 let json = serde_json::to_string(&rule).unwrap();
779 let back: PermissionRule = serde_json::from_str(&json).unwrap();
780 assert_eq!(back.tool, "Read");
781 assert!(back.pattern.is_none());
782 assert_eq!(back.action, PermissionMode::Allow);
783 }
784
785 #[test]
788 fn config_toml_with_hooks() {
789 let toml_str = r#"
790[[hooks]]
791event = "session_start"
792tool_name = "Bash"
793
794[hooks.action]
795type = "shell"
796command = "echo starting"
797"#;
798 let cfg: Config = toml::from_str(toml_str).unwrap();
799 assert_eq!(cfg.hooks.len(), 1);
800 assert_eq!(cfg.hooks[0].event, HookEvent::SessionStart);
801 assert_eq!(cfg.hooks[0].tool_name, Some("Bash".into()));
802 }
803
804 #[test]
807 fn config_toml_with_mcp_servers() {
808 let toml_str = r#"
809[mcp_servers.my_server]
810command = "npx my-mcp"
811args = ["--flag"]
812"#;
813 let cfg: Config = toml::from_str(toml_str).unwrap();
814 assert!(cfg.mcp_servers.contains_key("my_server"));
815 let server = &cfg.mcp_servers["my_server"];
816 assert_eq!(server.command, Some("npx my-mcp".into()));
817 assert_eq!(server.args, vec!["--flag"]);
818 }
819}