1use std::time::Duration;
2
3const DEFAULT_CLI_PATH: &str = "claude";
5
6#[derive(Debug, Clone, Default)]
8#[non_exhaustive]
9pub struct ClaudeConfig {
10 pub cli_path: Option<String>,
20 pub model: Option<String>,
22 pub system_prompt: Option<String>,
24 pub append_system_prompt: Option<String>,
26 pub max_turns: Option<u32>,
28 pub timeout: Option<Duration>,
30 pub stream_idle_timeout: Option<Duration>,
34 pub fallback_model: Option<String>,
36 pub effort: Option<String>,
38 pub max_budget_usd: Option<f64>,
40 pub allowed_tools: Vec<String>,
42 pub disallowed_tools: Vec<String>,
44 pub tools: Option<String>,
46 pub mcp_config: Vec<String>,
48 pub setting_sources: Option<String>,
50 pub settings: Option<String>,
52 pub json_schema: Option<String>,
54 pub include_partial_messages: Option<bool>,
56 pub include_hook_events: Option<bool>,
58 pub permission_mode: Option<String>,
60 pub dangerously_skip_permissions: Option<bool>,
62 pub add_dir: Vec<String>,
64 pub file: Vec<String>,
66 pub resume: Option<String>,
68 pub session_id: Option<String>,
70 pub bare: Option<bool>,
72 pub no_session_persistence: Option<bool>,
74 pub disable_slash_commands: Option<bool>,
76 pub strict_mcp_config: Option<bool>,
78 pub extra_args: Vec<String>,
83}
84
85impl ClaudeConfig {
86 #[must_use]
88 pub fn cli_path_or_default(&self) -> &str {
89 self.cli_path.as_deref().unwrap_or(DEFAULT_CLI_PATH)
90 }
91
92 #[must_use]
94 pub fn builder() -> ClaudeConfigBuilder {
95 ClaudeConfigBuilder::default()
96 }
97
98 #[must_use]
100 pub fn to_builder(&self) -> ClaudeConfigBuilder {
101 ClaudeConfigBuilder {
102 cli_path: self.cli_path.clone(),
103 model: self.model.clone(),
104 system_prompt: self.system_prompt.clone(),
105 append_system_prompt: self.append_system_prompt.clone(),
106 max_turns: self.max_turns,
107 timeout: self.timeout,
108 stream_idle_timeout: self.stream_idle_timeout,
109 fallback_model: self.fallback_model.clone(),
110 effort: self.effort.clone(),
111 max_budget_usd: self.max_budget_usd,
112 allowed_tools: self.allowed_tools.clone(),
113 disallowed_tools: self.disallowed_tools.clone(),
114 tools: self.tools.clone(),
115 mcp_config: self.mcp_config.clone(),
116 setting_sources: self.setting_sources.clone(),
117 settings: self.settings.clone(),
118 json_schema: self.json_schema.clone(),
119 include_partial_messages: self.include_partial_messages,
120 include_hook_events: self.include_hook_events,
121 permission_mode: self.permission_mode.clone(),
122 dangerously_skip_permissions: self.dangerously_skip_permissions,
123 add_dir: self.add_dir.clone(),
124 file: self.file.clone(),
125 resume: self.resume.clone(),
126 session_id: self.session_id.clone(),
127 bare: self.bare,
128 no_session_persistence: self.no_session_persistence,
129 disable_slash_commands: self.disable_slash_commands,
130 strict_mcp_config: self.strict_mcp_config,
131 extra_args: self.extra_args.clone(),
132 }
133 }
134
135 fn base_args(&self) -> Vec<String> {
137 let mut args = vec!["--print".into()];
138
139 if self.no_session_persistence != Some(false) {
143 args.push("--no-session-persistence".into());
144 }
145
146 args.push("--setting-sources".into());
148 args.push(self.setting_sources.clone().unwrap_or_default());
149
150 if self.strict_mcp_config != Some(false) {
152 args.push("--strict-mcp-config".into());
153 }
154
155 if self.mcp_config.is_empty() {
157 args.push("--mcp-config".into());
158 args.push(r#"{"mcpServers":{}}"#.into());
159 } else {
160 for cfg in &self.mcp_config {
161 args.push("--mcp-config".into());
162 args.push(cfg.clone());
163 }
164 }
165
166 args.push("--tools".into());
168 args.push(self.tools.clone().unwrap_or_default());
169
170 if self.disable_slash_commands != Some(false) {
172 args.push("--disable-slash-commands".into());
173 }
174
175 args.push("--system-prompt".into());
178 args.push(self.system_prompt.clone().unwrap_or_default());
179
180 if let Some(ref val) = self.append_system_prompt {
181 args.push("--append-system-prompt".into());
182 args.push(val.clone());
183 }
184
185 if let Some(ref val) = self.model {
186 args.push("--model".into());
187 args.push(val.clone());
188 }
189
190 if let Some(ref val) = self.fallback_model {
191 args.push("--fallback-model".into());
192 args.push(val.clone());
193 }
194
195 if let Some(ref val) = self.effort {
196 args.push("--effort".into());
197 args.push(val.clone());
198 }
199
200 if let Some(max_turns) = self.max_turns {
201 args.push("--max-turns".into());
202 args.push(max_turns.to_string());
203 }
204
205 if let Some(budget) = self.max_budget_usd {
206 args.push("--max-budget-usd".into());
207 args.push(budget.to_string());
208 }
209
210 if !self.allowed_tools.is_empty() {
211 args.push("--allowedTools".into());
212 args.extend(self.allowed_tools.iter().cloned());
213 }
214
215 if !self.disallowed_tools.is_empty() {
216 args.push("--disallowedTools".into());
217 args.extend(self.disallowed_tools.iter().cloned());
218 }
219
220 if let Some(ref val) = self.settings {
221 args.push("--settings".into());
222 args.push(val.clone());
223 }
224
225 if let Some(ref val) = self.json_schema {
226 args.push("--json-schema".into());
227 args.push(val.clone());
228 }
229
230 if self.include_hook_events == Some(true) {
231 args.push("--include-hook-events".into());
232 }
233
234 if let Some(ref val) = self.permission_mode {
235 args.push("--permission-mode".into());
236 args.push(val.clone());
237 }
238
239 if self.dangerously_skip_permissions == Some(true) {
240 args.push("--dangerously-skip-permissions".into());
241 }
242
243 if !self.add_dir.is_empty() {
244 args.push("--add-dir".into());
245 args.extend(self.add_dir.iter().cloned());
246 }
247
248 if !self.file.is_empty() {
249 args.push("--file".into());
250 args.extend(self.file.iter().cloned());
251 }
252
253 if let Some(ref val) = self.resume {
254 args.push("--resume".into());
255 args.push(val.clone());
256 }
257
258 if let Some(ref val) = self.session_id {
259 args.push("--session-id".into());
260 args.push(val.clone());
261 }
262
263 if self.bare == Some(true) {
264 args.push("--bare".into());
265 }
266
267 args.extend(self.extra_args.iter().cloned());
269
270 args
271 }
272
273 #[must_use]
277 pub fn to_args(&self, prompt: &str) -> Vec<String> {
278 let mut args = self.base_args();
279 args.push("--output-format".into());
280 args.push("json".into());
281 args.push(prompt.into());
282 args
283 }
284
285 #[must_use]
290 pub fn to_stream_args(&self, prompt: &str) -> Vec<String> {
291 let mut args = self.base_args();
292 args.push("--output-format".into());
293 args.push("stream-json".into());
294 args.push("--verbose".into());
295
296 if self.include_partial_messages == Some(true) {
297 args.push("--include-partial-messages".into());
298 }
299
300 args.push(prompt.into());
301 args
302 }
303}
304
305#[derive(Debug, Clone, Default)]
307pub struct ClaudeConfigBuilder {
308 cli_path: Option<String>,
309 model: Option<String>,
310 system_prompt: Option<String>,
311 append_system_prompt: Option<String>,
312 max_turns: Option<u32>,
313 timeout: Option<Duration>,
314 stream_idle_timeout: Option<Duration>,
315 fallback_model: Option<String>,
316 effort: Option<String>,
317 max_budget_usd: Option<f64>,
318 allowed_tools: Vec<String>,
319 disallowed_tools: Vec<String>,
320 tools: Option<String>,
321 mcp_config: Vec<String>,
322 setting_sources: Option<String>,
323 settings: Option<String>,
324 json_schema: Option<String>,
325 include_partial_messages: Option<bool>,
326 include_hook_events: Option<bool>,
327 permission_mode: Option<String>,
328 dangerously_skip_permissions: Option<bool>,
329 add_dir: Vec<String>,
330 file: Vec<String>,
331 resume: Option<String>,
332 session_id: Option<String>,
333 bare: Option<bool>,
334 no_session_persistence: Option<bool>,
335 disable_slash_commands: Option<bool>,
336 strict_mcp_config: Option<bool>,
337 extra_args: Vec<String>,
338}
339
340impl ClaudeConfigBuilder {
341 #[must_use]
345 pub fn cli_path(mut self, path: impl Into<String>) -> Self {
346 self.cli_path = Some(path.into());
347 self
348 }
349
350 #[must_use]
352 pub fn model(mut self, model: impl Into<String>) -> Self {
353 self.model = Some(model.into());
354 self
355 }
356
357 #[must_use]
359 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
360 self.system_prompt = Some(prompt.into());
361 self
362 }
363
364 #[must_use]
366 pub fn append_system_prompt(mut self, prompt: impl Into<String>) -> Self {
367 self.append_system_prompt = Some(prompt.into());
368 self
369 }
370
371 #[must_use]
373 pub fn max_turns(mut self, max_turns: u32) -> Self {
374 self.max_turns = Some(max_turns);
375 self
376 }
377
378 #[must_use]
380 pub fn timeout(mut self, timeout: Duration) -> Self {
381 self.timeout = Some(timeout);
382 self
383 }
384
385 #[must_use]
390 pub fn stream_idle_timeout(mut self, timeout: Duration) -> Self {
391 self.stream_idle_timeout = Some(timeout);
392 self
393 }
394
395 #[must_use]
397 pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
398 self.fallback_model = Some(model.into());
399 self
400 }
401
402 #[must_use]
404 pub fn effort(mut self, effort: impl Into<String>) -> Self {
405 self.effort = Some(effort.into());
406 self
407 }
408
409 #[must_use]
411 pub fn max_budget_usd(mut self, budget: f64) -> Self {
412 self.max_budget_usd = Some(budget);
413 self
414 }
415
416 #[must_use]
418 pub fn allowed_tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
419 self.allowed_tools = tools.into_iter().map(Into::into).collect();
420 self
421 }
422
423 #[must_use]
425 pub fn add_allowed_tool(mut self, tool: impl Into<String>) -> Self {
426 self.allowed_tools.push(tool.into());
427 self
428 }
429
430 #[must_use]
432 pub fn disallowed_tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
433 self.disallowed_tools = tools.into_iter().map(Into::into).collect();
434 self
435 }
436
437 #[must_use]
439 pub fn add_disallowed_tool(mut self, tool: impl Into<String>) -> Self {
440 self.disallowed_tools.push(tool.into());
441 self
442 }
443
444 #[must_use]
446 pub fn tools(mut self, tools: impl Into<String>) -> Self {
447 self.tools = Some(tools.into());
448 self
449 }
450
451 #[must_use]
453 pub fn mcp_configs(mut self, configs: impl IntoIterator<Item = impl Into<String>>) -> Self {
454 self.mcp_config = configs.into_iter().map(Into::into).collect();
455 self
456 }
457
458 #[must_use]
460 pub fn add_mcp_config(mut self, config: impl Into<String>) -> Self {
461 self.mcp_config.push(config.into());
462 self
463 }
464
465 #[must_use]
467 pub fn setting_sources(mut self, sources: impl Into<String>) -> Self {
468 self.setting_sources = Some(sources.into());
469 self
470 }
471
472 #[must_use]
474 pub fn settings(mut self, settings: impl Into<String>) -> Self {
475 self.settings = Some(settings.into());
476 self
477 }
478
479 #[must_use]
481 pub fn json_schema(mut self, schema: impl Into<String>) -> Self {
482 self.json_schema = Some(schema.into());
483 self
484 }
485
486 #[must_use]
488 pub fn include_partial_messages(mut self, enabled: bool) -> Self {
489 self.include_partial_messages = Some(enabled);
490 self
491 }
492
493 #[must_use]
495 pub fn include_hook_events(mut self, enabled: bool) -> Self {
496 self.include_hook_events = Some(enabled);
497 self
498 }
499
500 #[must_use]
502 pub fn permission_mode(mut self, mode: impl Into<String>) -> Self {
503 self.permission_mode = Some(mode.into());
504 self
505 }
506
507 #[must_use]
509 pub fn dangerously_skip_permissions(mut self, enabled: bool) -> Self {
510 self.dangerously_skip_permissions = Some(enabled);
511 self
512 }
513
514 #[must_use]
516 pub fn add_dirs(mut self, dirs: impl IntoIterator<Item = impl Into<String>>) -> Self {
517 self.add_dir = dirs.into_iter().map(Into::into).collect();
518 self
519 }
520
521 #[must_use]
523 pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
524 self.add_dir.push(dir.into());
525 self
526 }
527
528 #[must_use]
530 pub fn files(mut self, files: impl IntoIterator<Item = impl Into<String>>) -> Self {
531 self.file = files.into_iter().map(Into::into).collect();
532 self
533 }
534
535 #[must_use]
537 pub fn file(mut self, file: impl Into<String>) -> Self {
538 self.file.push(file.into());
539 self
540 }
541
542 #[must_use]
544 pub fn resume(mut self, session_id: impl Into<String>) -> Self {
545 self.resume = Some(session_id.into());
546 self
547 }
548
549 #[must_use]
551 pub fn session_id(mut self, id: impl Into<String>) -> Self {
552 self.session_id = Some(id.into());
553 self
554 }
555
556 #[must_use]
558 pub fn bare(mut self, enabled: bool) -> Self {
559 self.bare = Some(enabled);
560 self
561 }
562
563 #[must_use]
566 pub fn no_session_persistence(mut self, enabled: bool) -> Self {
567 self.no_session_persistence = Some(enabled);
568 self
569 }
570
571 #[must_use]
574 pub fn disable_slash_commands(mut self, enabled: bool) -> Self {
575 self.disable_slash_commands = Some(enabled);
576 self
577 }
578
579 #[must_use]
582 pub fn strict_mcp_config(mut self, enabled: bool) -> Self {
583 self.strict_mcp_config = Some(enabled);
584 self
585 }
586
587 #[must_use]
592 pub fn extra_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
593 self.extra_args = args.into_iter().map(Into::into).collect();
594 self
595 }
596
597 #[must_use]
599 pub fn add_extra_arg(mut self, arg: impl Into<String>) -> Self {
600 self.extra_args.push(arg.into());
601 self
602 }
603
604 #[must_use]
606 pub fn build(self) -> ClaudeConfig {
607 ClaudeConfig {
608 cli_path: self.cli_path,
609 model: self.model,
610 system_prompt: self.system_prompt,
611 append_system_prompt: self.append_system_prompt,
612 max_turns: self.max_turns,
613 timeout: self.timeout,
614 stream_idle_timeout: self.stream_idle_timeout,
615 fallback_model: self.fallback_model,
616 effort: self.effort,
617 max_budget_usd: self.max_budget_usd,
618 allowed_tools: self.allowed_tools,
619 disallowed_tools: self.disallowed_tools,
620 tools: self.tools,
621 mcp_config: self.mcp_config,
622 setting_sources: self.setting_sources,
623 settings: self.settings,
624 json_schema: self.json_schema,
625 include_partial_messages: self.include_partial_messages,
626 include_hook_events: self.include_hook_events,
627 permission_mode: self.permission_mode,
628 dangerously_skip_permissions: self.dangerously_skip_permissions,
629 add_dir: self.add_dir,
630 file: self.file,
631 resume: self.resume,
632 session_id: self.session_id,
633 bare: self.bare,
634 no_session_persistence: self.no_session_persistence,
635 disable_slash_commands: self.disable_slash_commands,
636 strict_mcp_config: self.strict_mcp_config,
637 extra_args: self.extra_args,
638 }
639 }
640}
641
642pub mod effort {
644 pub const LOW: &str = "low";
646 pub const MEDIUM: &str = "medium";
648 pub const HIGH: &str = "high";
650 pub const MAX: &str = "max";
652}
653
654pub mod permission_mode {
656 pub const DEFAULT: &str = "default";
658 pub const ACCEPT_EDITS: &str = "acceptEdits";
660 pub const AUTO: &str = "auto";
662 pub const BYPASS_PERMISSIONS: &str = "bypassPermissions";
664 pub const DONT_ASK: &str = "dontAsk";
666 pub const PLAN: &str = "plan";
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673
674 #[test]
675 fn default_config() {
676 let config = ClaudeConfig::default();
677 assert!(config.cli_path.is_none());
678 assert!(config.model.is_none());
679 assert!(config.system_prompt.is_none());
680 assert!(config.max_turns.is_none());
681 assert!(config.timeout.is_none());
682 }
683
684 #[test]
685 fn cli_path_or_default_returns_claude_when_none() {
686 let config = ClaudeConfig::default();
687 assert_eq!(config.cli_path_or_default(), "claude");
688 }
689
690 #[test]
691 fn cli_path_or_default_returns_custom_path() {
692 let config = ClaudeConfig::builder()
693 .cli_path("/usr/local/bin/claude-v2")
694 .build();
695 assert_eq!(config.cli_path_or_default(), "/usr/local/bin/claude-v2");
696 }
697
698 #[test]
699 fn builder_sets_cli_path() {
700 let config = ClaudeConfig::builder().cli_path("/opt/claude").build();
701 assert_eq!(config.cli_path.as_deref(), Some("/opt/claude"));
702 }
703
704 #[test]
705 fn builder_sets_stream_idle_timeout() {
706 let config = ClaudeConfig::builder()
707 .stream_idle_timeout(Duration::from_secs(60))
708 .build();
709 assert_eq!(config.stream_idle_timeout, Some(Duration::from_secs(60)));
710 }
711
712 #[test]
713 fn default_stream_idle_timeout_is_none() {
714 let config = ClaudeConfig::default();
715 assert!(config.stream_idle_timeout.is_none());
716 }
717
718 #[test]
719 fn builder_sets_all_fields() {
720 let config = ClaudeConfig::builder()
721 .model("haiku")
722 .system_prompt("You are helpful")
723 .max_turns(3)
724 .timeout(Duration::from_secs(30))
725 .build();
726
727 assert_eq!(config.model.as_deref(), Some("haiku"));
728 assert_eq!(config.system_prompt.as_deref(), Some("You are helpful"));
729 assert_eq!(config.max_turns, Some(3));
730 assert_eq!(config.timeout, Some(Duration::from_secs(30)));
731 }
732
733 #[test]
734 fn to_args_minimal() {
735 let config = ClaudeConfig::default();
736 let args = config.to_args("hello");
737
738 assert!(args.contains(&"--print".to_string()));
739 assert!(args.contains(&"json".to_string()));
740 assert!(args.contains(&"--no-session-persistence".to_string()));
741 assert!(args.contains(&"--disable-slash-commands".to_string()));
742 assert!(args.contains(&"--strict-mcp-config".to_string()));
743 let sp_idx = args.iter().position(|a| a == "--system-prompt").unwrap();
745 assert_eq!(args[sp_idx + 1], "");
746 assert!(!args.contains(&"--model".to_string()));
748 assert!(!args.contains(&"--max-turns".to_string()));
749 assert_eq!(args.last().unwrap(), "hello");
751 }
752
753 #[test]
754 fn to_args_with_options() {
755 let config = ClaudeConfig::builder()
756 .model("haiku")
757 .system_prompt("Be concise")
758 .max_turns(5)
759 .build();
760 let args = config.to_args("test prompt");
761
762 let model_idx = args.iter().position(|a| a == "--model").unwrap();
763 assert_eq!(args[model_idx + 1], "haiku");
764
765 let sp_idx = args.iter().position(|a| a == "--system-prompt").unwrap();
766 assert_eq!(args[sp_idx + 1], "Be concise");
767
768 let mt_idx = args.iter().position(|a| a == "--max-turns").unwrap();
769 assert_eq!(args[mt_idx + 1], "5");
770
771 assert_eq!(args.last().unwrap(), "test prompt");
772 }
773
774 #[test]
775 fn to_stream_args_minimal() {
776 let config = ClaudeConfig::default();
777 let args = config.to_stream_args("hello");
778
779 assert!(args.contains(&"--print".to_string()));
780 assert!(args.contains(&"stream-json".to_string()));
781 assert!(args.contains(&"--verbose".to_string()));
782 assert!(!args.contains(&"json".to_string()));
783 assert!(!args.contains(&"--include-partial-messages".to_string()));
784 assert_eq!(args.last().unwrap(), "hello");
785 }
786
787 #[test]
788 fn to_stream_args_with_partial_messages() {
789 let config = ClaudeConfig::builder()
790 .include_partial_messages(true)
791 .build();
792 let args = config.to_stream_args("hello");
793
794 assert!(args.contains(&"--include-partial-messages".to_string()));
795 }
796
797 #[test]
798 fn builder_sets_include_partial_messages() {
799 let config = ClaudeConfig::builder()
800 .include_partial_messages(true)
801 .build();
802 assert_eq!(config.include_partial_messages, Some(true));
803 }
804
805 #[test]
806 fn all_new_fields_in_builder() {
807 let config = ClaudeConfig::builder()
808 .append_system_prompt("extra context")
809 .fallback_model("haiku")
810 .effort("high")
811 .max_budget_usd(1.0)
812 .allowed_tools(["Bash", "Edit"])
813 .disallowed_tools(["Write"])
814 .tools("Bash,Edit")
815 .mcp_configs(["config.json"])
816 .setting_sources("user,project")
817 .settings("settings.json")
818 .json_schema(r#"{"type":"object"}"#)
819 .include_hook_events(true)
820 .permission_mode("auto")
821 .dangerously_skip_permissions(true)
822 .add_dirs(["/path/a"])
823 .file("spec:file.txt")
824 .resume("session-123")
825 .session_id("uuid-456")
826 .bare(true)
827 .no_session_persistence(false)
828 .disable_slash_commands(false)
829 .strict_mcp_config(false)
830 .extra_args(["--custom", "val"])
831 .build();
832
833 assert_eq!(
834 config.append_system_prompt.as_deref(),
835 Some("extra context")
836 );
837 assert_eq!(config.fallback_model.as_deref(), Some("haiku"));
838 assert_eq!(config.effort.as_deref(), Some("high"));
839 assert_eq!(config.max_budget_usd, Some(1.0));
840 assert_eq!(config.allowed_tools, vec!["Bash", "Edit"]);
841 assert_eq!(config.disallowed_tools, vec!["Write"]);
842 assert_eq!(config.tools.as_deref(), Some("Bash,Edit"));
843 assert_eq!(config.mcp_config, vec!["config.json"]);
844 assert_eq!(config.setting_sources.as_deref(), Some("user,project"));
845 assert_eq!(config.settings.as_deref(), Some("settings.json"));
846 assert_eq!(config.json_schema.as_deref(), Some(r#"{"type":"object"}"#));
847 assert_eq!(config.include_hook_events, Some(true));
848 assert_eq!(config.permission_mode.as_deref(), Some("auto"));
849 assert_eq!(config.dangerously_skip_permissions, Some(true));
850 assert_eq!(config.add_dir, vec!["/path/a"]);
851 assert_eq!(config.file, vec!["spec:file.txt"]);
852 assert_eq!(config.resume.as_deref(), Some("session-123"));
853 assert_eq!(config.session_id.as_deref(), Some("uuid-456"));
854 assert_eq!(config.bare, Some(true));
855 assert_eq!(config.no_session_persistence, Some(false));
856 assert_eq!(config.disable_slash_commands, Some(false));
857 assert_eq!(config.strict_mcp_config, Some(false));
858 assert_eq!(config.extra_args, vec!["--custom", "val"]);
859 }
860
861 #[test]
862 fn default_uses_minimal_context() {
863 let config = ClaudeConfig::default();
864 let args = config.to_args("test");
865
866 assert!(args.contains(&"--no-session-persistence".to_string()));
867 assert!(args.contains(&"--strict-mcp-config".to_string()));
868 assert!(args.contains(&"--disable-slash-commands".to_string()));
869
870 let ss_idx = args.iter().position(|a| a == "--setting-sources").unwrap();
871 assert_eq!(args[ss_idx + 1], "");
872
873 let mcp_idx = args.iter().position(|a| a == "--mcp-config").unwrap();
874 assert_eq!(args[mcp_idx + 1], r#"{"mcpServers":{}}"#);
875
876 let tools_idx = args.iter().position(|a| a == "--tools").unwrap();
877 assert_eq!(args[tools_idx + 1], "");
878 }
879
880 #[test]
881 fn override_no_session_persistence_false() {
882 let config = ClaudeConfig::builder()
883 .no_session_persistence(false)
884 .build();
885 let args = config.to_args("test");
886 assert!(!args.contains(&"--no-session-persistence".to_string()));
887 }
888
889 #[test]
890 fn override_strict_mcp_config_false() {
891 let config = ClaudeConfig::builder().strict_mcp_config(false).build();
892 let args = config.to_args("test");
893 assert!(!args.contains(&"--strict-mcp-config".to_string()));
894 }
895
896 #[test]
897 fn override_disable_slash_commands_false() {
898 let config = ClaudeConfig::builder()
899 .disable_slash_commands(false)
900 .build();
901 let args = config.to_args("test");
902 assert!(!args.contains(&"--disable-slash-commands".to_string()));
903 }
904
905 #[test]
906 fn override_tools() {
907 let config = ClaudeConfig::builder().tools("Bash,Edit").build();
908 let args = config.to_args("test");
909 let idx = args.iter().position(|a| a == "--tools").unwrap();
910 assert_eq!(args[idx + 1], "Bash,Edit");
911 }
912
913 #[test]
914 fn override_setting_sources() {
915 let config = ClaudeConfig::builder()
916 .setting_sources("user,project")
917 .build();
918 let args = config.to_args("test");
919 let idx = args.iter().position(|a| a == "--setting-sources").unwrap();
920 assert_eq!(args[idx + 1], "user,project");
921 }
922
923 #[test]
924 fn override_mcp_config() {
925 let config = ClaudeConfig::builder()
926 .mcp_configs(["path/config.json"])
927 .build();
928 let args = config.to_args("test");
929 let idx = args.iter().position(|a| a == "--mcp-config").unwrap();
930 assert_eq!(args[idx + 1], "path/config.json");
931 assert!(!args.contains(&r#"{"mcpServers":{}}"#.to_string()));
932 }
933
934 #[test]
935 fn effort_with_constant() {
936 let config = ClaudeConfig::builder().effort(effort::HIGH).build();
937 let args = config.to_args("test");
938 let idx = args.iter().position(|a| a == "--effort").unwrap();
939 assert_eq!(args[idx + 1], "high");
940 }
941
942 #[test]
943 fn effort_with_custom_string() {
944 let config = ClaudeConfig::builder().effort("ultra").build();
945 let args = config.to_args("test");
946 let idx = args.iter().position(|a| a == "--effort").unwrap();
947 assert_eq!(args[idx + 1], "ultra");
948 }
949
950 #[test]
951 fn allowed_tools_multiple() {
952 let config = ClaudeConfig::builder()
953 .allowed_tools(["Bash(git:*)", "Edit", "Read"])
954 .build();
955 let args = config.to_args("test");
956 let idx = args.iter().position(|a| a == "--allowedTools").unwrap();
957 assert_eq!(args[idx + 1], "Bash(git:*)");
958 assert_eq!(args[idx + 2], "Edit");
959 assert_eq!(args[idx + 3], "Read");
960 }
961
962 #[test]
963 fn bare_flag() {
964 let config = ClaudeConfig::builder().bare(true).build();
965 let args = config.to_args("test");
966 assert!(args.contains(&"--bare".to_string()));
967 }
968
969 #[test]
970 fn dangerously_skip_permissions_flag() {
971 let config = ClaudeConfig::builder()
972 .dangerously_skip_permissions(true)
973 .build();
974 let args = config.to_args("test");
975 assert!(args.contains(&"--dangerously-skip-permissions".to_string()));
976 }
977
978 #[test]
979 fn resume_session() {
980 let config = ClaudeConfig::builder().resume("session-abc").build();
981 let args = config.to_args("test");
982 let idx = args.iter().position(|a| a == "--resume").unwrap();
983 assert_eq!(args[idx + 1], "session-abc");
984 }
985
986 #[test]
987 fn session_id_field() {
988 let config = ClaudeConfig::builder()
989 .session_id("550e8400-e29b-41d4-a716-446655440000")
990 .build();
991 let args = config.to_args("test");
992 let idx = args.iter().position(|a| a == "--session-id").unwrap();
993 assert_eq!(args[idx + 1], "550e8400-e29b-41d4-a716-446655440000");
994 }
995
996 #[test]
997 fn json_schema_field() {
998 let schema = r#"{"type":"object","properties":{"name":{"type":"string"}}}"#;
999 let config = ClaudeConfig::builder().json_schema(schema).build();
1000 let args = config.to_args("test");
1001 let idx = args.iter().position(|a| a == "--json-schema").unwrap();
1002 assert_eq!(args[idx + 1], schema);
1003 }
1004
1005 #[test]
1006 fn add_dir_multiple() {
1007 let config = ClaudeConfig::builder()
1008 .add_dirs(["/path/a", "/path/b"])
1009 .build();
1010 let args = config.to_args("test");
1011 let idx = args.iter().position(|a| a == "--add-dir").unwrap();
1012 assert_eq!(args[idx + 1], "/path/a");
1013 assert_eq!(args[idx + 2], "/path/b");
1014 }
1015
1016 #[test]
1017 fn file_multiple() {
1018 let config = ClaudeConfig::builder()
1019 .files(["file_abc:doc.txt", "file_def:img.png"])
1020 .build();
1021 let args = config.to_args("test");
1022 let idx = args.iter().position(|a| a == "--file").unwrap();
1023 assert_eq!(args[idx + 1], "file_abc:doc.txt");
1024 assert_eq!(args[idx + 2], "file_def:img.png");
1025 }
1026
1027 #[test]
1028 fn extra_args_before_prompt() {
1029 let config = ClaudeConfig::builder()
1030 .extra_args(["--custom-flag", "value"])
1031 .build();
1032 let args = config.to_args("my prompt");
1033 let custom_idx = args.iter().position(|a| a == "--custom-flag").unwrap();
1034 let prompt_idx = args.iter().position(|a| a == "my prompt").unwrap();
1035 assert!(custom_idx < prompt_idx);
1036 assert_eq!(args[custom_idx + 1], "value");
1037 }
1038
1039 #[test]
1040 fn extra_args_with_typed_fields() {
1041 let config = ClaudeConfig::builder()
1042 .model("sonnet")
1043 .extra_args(["--custom", "val"])
1044 .build();
1045 let args = config.to_args("test");
1046 assert!(args.contains(&"--model".to_string()));
1047 assert!(args.contains(&"sonnet".to_string()));
1048 assert!(args.contains(&"--custom".to_string()));
1049 assert!(args.contains(&"val".to_string()));
1050 }
1051
1052 #[test]
1053 fn disallowed_tools_multiple() {
1054 let config = ClaudeConfig::builder()
1055 .disallowed_tools(["Write", "Bash"])
1056 .build();
1057 let args = config.to_args("test");
1058 let idx = args.iter().position(|a| a == "--disallowedTools").unwrap();
1059 assert_eq!(args[idx + 1], "Write");
1060 assert_eq!(args[idx + 2], "Bash");
1061 }
1062
1063 #[test]
1064 fn to_builder_round_trip_fields() {
1065 let original = ClaudeConfig::builder()
1066 .cli_path("/custom/claude")
1067 .model("haiku")
1068 .system_prompt("test")
1069 .max_turns(5)
1070 .timeout(Duration::from_secs(30))
1071 .stream_idle_timeout(Duration::from_secs(45))
1072 .no_session_persistence(false)
1073 .resume("session-123")
1074 .build();
1075
1076 let rebuilt = original.to_builder().build();
1077
1078 assert_eq!(rebuilt.cli_path, original.cli_path);
1079 assert_eq!(rebuilt.model, original.model);
1080 assert_eq!(rebuilt.system_prompt, original.system_prompt);
1081 assert_eq!(rebuilt.max_turns, original.max_turns);
1082 assert_eq!(rebuilt.timeout, original.timeout);
1083 assert_eq!(rebuilt.stream_idle_timeout, original.stream_idle_timeout);
1084 assert_eq!(
1085 rebuilt.no_session_persistence,
1086 original.no_session_persistence
1087 );
1088 assert_eq!(rebuilt.resume, original.resume);
1089 }
1090
1091 #[test]
1092 fn to_builder_round_trip_args() {
1093 let config = ClaudeConfig::builder()
1094 .model("haiku")
1095 .max_turns(3)
1096 .effort("high")
1097 .allowed_tools(["Bash", "Read"])
1098 .no_session_persistence(false)
1099 .build();
1100
1101 let rebuilt = config.to_builder().build();
1102 assert_eq!(config.to_args("hi"), rebuilt.to_args("hi"));
1103 }
1104}