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