1use std::time::Duration;
2
3macro_rules! trace_warn {
5 ($($arg:tt)*) => {
6 #[cfg(feature = "tracing")]
7 tracing::warn!($($arg)*);
8 };
9}
10
11const DEFAULT_CLI_PATH: &str = "claude";
13
14#[derive(Debug, Clone, Default, PartialEq)]
29#[non_exhaustive]
30pub enum Preset {
31 #[default]
37 Normal,
38
39 Minimal,
44
45 Bare,
48
49 Custom(Vec<String>),
52}
53
54#[derive(Debug, Clone, Default)]
56#[non_exhaustive]
57pub struct ClaudeConfig {
58 pub preset: Preset,
61 pub cli_path: Option<String>,
71 pub model: Option<String>,
73 pub system_prompt: Option<String>,
75 pub append_system_prompt: Option<String>,
77 pub max_turns: Option<u32>,
79 pub timeout: Option<Duration>,
81 pub stream_idle_timeout: Option<Duration>,
85 pub fallback_model: Option<String>,
87 pub effort: Option<String>,
89 pub max_budget_usd: Option<f64>,
91 pub allowed_tools: Vec<String>,
93 pub disallowed_tools: Vec<String>,
95 pub tools: Option<String>,
97 pub mcp_config: Vec<String>,
99 pub setting_sources: Option<String>,
101 pub settings: Option<String>,
103 pub json_schema: Option<String>,
105 pub include_partial_messages: Option<bool>,
107 pub include_hook_events: Option<bool>,
109 pub permission_mode: Option<String>,
111 pub dangerously_skip_permissions: Option<bool>,
113 pub add_dir: Vec<String>,
115 pub file: Vec<String>,
117 pub resume: Option<String>,
119 pub session_id: Option<String>,
121 pub bare: Option<bool>,
123 pub no_session_persistence: Option<bool>,
125 pub disable_slash_commands: Option<bool>,
127 pub strict_mcp_config: Option<bool>,
129 pub extra_args: Vec<String>,
134}
135
136impl ClaudeConfig {
137 #[must_use]
139 pub fn cli_path_or_default(&self) -> &str {
140 self.cli_path.as_deref().unwrap_or(DEFAULT_CLI_PATH)
141 }
142
143 #[must_use]
145 pub fn builder() -> ClaudeConfigBuilder {
146 ClaudeConfigBuilder::default()
147 }
148
149 #[must_use]
151 pub fn to_builder(&self) -> ClaudeConfigBuilder {
152 ClaudeConfigBuilder {
153 preset: self.preset.clone(),
154 cli_path: self.cli_path.clone(),
155 model: self.model.clone(),
156 system_prompt: self.system_prompt.clone(),
157 append_system_prompt: self.append_system_prompt.clone(),
158 max_turns: self.max_turns,
159 timeout: self.timeout,
160 stream_idle_timeout: self.stream_idle_timeout,
161 fallback_model: self.fallback_model.clone(),
162 effort: self.effort.clone(),
163 max_budget_usd: self.max_budget_usd,
164 allowed_tools: self.allowed_tools.clone(),
165 disallowed_tools: self.disallowed_tools.clone(),
166 tools: self.tools.clone(),
167 mcp_config: self.mcp_config.clone(),
168 setting_sources: self.setting_sources.clone(),
169 settings: self.settings.clone(),
170 json_schema: self.json_schema.clone(),
171 include_partial_messages: self.include_partial_messages,
172 include_hook_events: self.include_hook_events,
173 permission_mode: self.permission_mode.clone(),
174 dangerously_skip_permissions: self.dangerously_skip_permissions,
175 add_dir: self.add_dir.clone(),
176 file: self.file.clone(),
177 resume: self.resume.clone(),
178 session_id: self.session_id.clone(),
179 bare: self.bare,
180 no_session_persistence: self.no_session_persistence,
181 disable_slash_commands: self.disable_slash_commands,
182 strict_mcp_config: self.strict_mcp_config,
183 extra_args: self.extra_args.clone(),
184 }
185 }
186
187 fn base_args(&self) -> Vec<String> {
196 let mut args = self.preset_args();
197 self.push_builder_attrs(&mut args);
198 args
199 }
200
201 fn preset_args(&self) -> Vec<String> {
203 match &self.preset {
204 Preset::Normal => self.normal_preset_args(),
205 Preset::Minimal => self.minimal_preset_args(),
206 Preset::Bare => Vec::new(),
207 Preset::Custom(custom_args) => self.filtered_custom_args(custom_args),
208 }
209 }
210
211 fn normal_preset_args(&self) -> Vec<String> {
213 let mut args = vec!["--print".into()];
214
215 if self.no_session_persistence != Some(false) {
217 args.push("--no-session-persistence".into());
218 }
219
220 args.push("--setting-sources".into());
222 args.push(self.setting_sources.clone().unwrap_or_default());
223
224 if self.strict_mcp_config != Some(false) {
226 args.push("--strict-mcp-config".into());
227 }
228
229 if self.mcp_config.is_empty() {
231 args.push("--mcp-config".into());
232 args.push(r#"{"mcpServers":{}}"#.into());
233 } else {
234 for cfg in &self.mcp_config {
235 args.push("--mcp-config".into());
236 args.push(cfg.clone());
237 }
238 }
239
240 args.push("--tools".into());
242 args.push(self.tools.clone().unwrap_or_default());
243
244 if self.disable_slash_commands != Some(false) {
246 args.push("--disable-slash-commands".into());
247 }
248
249 args
250 }
251
252 fn minimal_preset_args(&self) -> Vec<String> {
254 vec!["--print".into()]
255 }
256
257 fn filtered_custom_args(&self, custom_args: &[String]) -> Vec<String> {
260 custom_args
261 .iter()
262 .filter(|arg| {
263 let s = arg.as_str();
264 !((self.no_session_persistence == Some(false) && s == "--no-session-persistence")
265 || (self.strict_mcp_config == Some(false) && s == "--strict-mcp-config")
266 || (self.disable_slash_commands == Some(false)
267 && s == "--disable-slash-commands"))
268 })
269 .cloned()
270 .collect()
271 }
272
273 fn push_builder_attrs(&self, args: &mut Vec<String>) {
279 let is_normal = matches!(self.preset, Preset::Normal);
280
281 if !is_normal {
285 if self.no_session_persistence == Some(true) {
286 args.push("--no-session-persistence".into());
287 }
288
289 if let Some(ref val) = self.setting_sources {
290 args.push("--setting-sources".into());
291 args.push(val.clone());
292 }
293
294 if self.strict_mcp_config == Some(true) {
295 args.push("--strict-mcp-config".into());
296 }
297
298 if !self.mcp_config.is_empty() {
299 for cfg in &self.mcp_config {
300 args.push("--mcp-config".into());
301 args.push(cfg.clone());
302 }
303 }
304
305 if let Some(ref val) = self.tools {
306 args.push("--tools".into());
307 args.push(val.clone());
308 }
309
310 if self.disable_slash_commands == Some(true) {
311 args.push("--disable-slash-commands".into());
312 }
313 }
314
315 if is_normal {
317 args.push("--system-prompt".into());
318 args.push(self.system_prompt.clone().unwrap_or_default());
319 } else if let Some(ref val) = self.system_prompt {
320 args.push("--system-prompt".into());
321 args.push(val.clone());
322 }
323
324 if let Some(ref val) = self.append_system_prompt {
325 args.push("--append-system-prompt".into());
326 args.push(val.clone());
327 }
328
329 if let Some(ref val) = self.model {
330 args.push("--model".into());
331 args.push(val.clone());
332 }
333
334 if let Some(ref val) = self.fallback_model {
335 args.push("--fallback-model".into());
336 args.push(val.clone());
337 }
338
339 if let Some(ref val) = self.effort {
340 args.push("--effort".into());
341 args.push(val.clone());
342 }
343
344 if let Some(max_turns) = self.max_turns {
345 args.push("--max-turns".into());
346 args.push(max_turns.to_string());
347 }
348
349 if let Some(budget) = self.max_budget_usd {
350 args.push("--max-budget-usd".into());
351 args.push(budget.to_string());
352 }
353
354 if !self.allowed_tools.is_empty() {
355 args.push("--allowedTools".into());
356 args.extend(self.allowed_tools.iter().cloned());
357 }
358
359 if !self.disallowed_tools.is_empty() {
360 args.push("--disallowedTools".into());
361 args.extend(self.disallowed_tools.iter().cloned());
362 }
363
364 if let Some(ref val) = self.settings {
365 args.push("--settings".into());
366 args.push(val.clone());
367 }
368
369 if let Some(ref val) = self.json_schema {
370 args.push("--json-schema".into());
371 args.push(val.clone());
372 }
373
374 if self.include_hook_events == Some(true) {
375 args.push("--include-hook-events".into());
376 }
377
378 if let Some(ref val) = self.permission_mode {
379 args.push("--permission-mode".into());
380 args.push(val.clone());
381 }
382
383 if self.dangerously_skip_permissions == Some(true) {
384 args.push("--dangerously-skip-permissions".into());
385 }
386
387 if !self.add_dir.is_empty() {
388 args.push("--add-dir".into());
389 args.extend(self.add_dir.iter().cloned());
390 }
391
392 if !self.file.is_empty() {
393 args.push("--file".into());
394 args.extend(self.file.iter().cloned());
395 }
396
397 if let Some(ref val) = self.resume {
398 args.push("--resume".into());
399 args.push(val.clone());
400 }
401
402 if let Some(ref val) = self.session_id {
403 args.push("--session-id".into());
404 args.push(val.clone());
405 }
406
407 if self.bare == Some(true) {
408 args.push("--bare".into());
409 }
410 }
411
412 #[must_use]
416 pub fn to_args(&self, prompt: &str) -> Vec<String> {
417 let mut args = self.base_args();
418 args.push("--output-format".into());
419 args.push("json".into());
420 args.extend(self.extra_args.iter().cloned());
421 self.warn_if_no_print(&args);
422 args.push(prompt.into());
423 args
424 }
425
426 #[must_use]
431 pub fn to_stream_args(&self, prompt: &str) -> Vec<String> {
432 let mut args = self.base_args();
433 args.push("--output-format".into());
434 args.push("stream-json".into());
435 args.push("--verbose".into());
436 if self.include_partial_messages == Some(true) {
437 args.push("--include-partial-messages".into());
438 }
439 args.extend(self.extra_args.iter().cloned());
440 self.warn_if_no_print(&args);
441 args.push(prompt.into());
442 args
443 }
444
445 fn warn_if_no_print(&self, args: &[String]) {
447 if !args.iter().any(|a| a == "--print" || a == "-p") {
448 trace_warn!(
449 "args do not contain --print; the CLI may start in interactive mode and hang"
450 );
451 }
452 }
453}
454
455#[derive(Debug, Clone, Default)]
457pub struct ClaudeConfigBuilder {
458 preset: Preset,
459 cli_path: Option<String>,
460 model: Option<String>,
461 system_prompt: Option<String>,
462 append_system_prompt: Option<String>,
463 max_turns: Option<u32>,
464 timeout: Option<Duration>,
465 stream_idle_timeout: Option<Duration>,
466 fallback_model: Option<String>,
467 effort: Option<String>,
468 max_budget_usd: Option<f64>,
469 allowed_tools: Vec<String>,
470 disallowed_tools: Vec<String>,
471 tools: Option<String>,
472 mcp_config: Vec<String>,
473 setting_sources: Option<String>,
474 settings: Option<String>,
475 json_schema: Option<String>,
476 include_partial_messages: Option<bool>,
477 include_hook_events: Option<bool>,
478 permission_mode: Option<String>,
479 dangerously_skip_permissions: Option<bool>,
480 add_dir: Vec<String>,
481 file: Vec<String>,
482 resume: Option<String>,
483 session_id: Option<String>,
484 bare: Option<bool>,
485 no_session_persistence: Option<bool>,
486 disable_slash_commands: Option<bool>,
487 strict_mcp_config: Option<bool>,
488 extra_args: Vec<String>,
489}
490
491impl ClaudeConfigBuilder {
492 #[must_use]
496 pub fn preset(mut self, preset: Preset) -> Self {
497 self.preset = preset;
498 self
499 }
500
501 #[must_use]
505 pub fn cli_path(mut self, path: impl Into<String>) -> Self {
506 self.cli_path = Some(path.into());
507 self
508 }
509
510 #[must_use]
512 pub fn model(mut self, model: impl Into<String>) -> Self {
513 self.model = Some(model.into());
514 self
515 }
516
517 #[must_use]
519 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
520 self.system_prompt = Some(prompt.into());
521 self
522 }
523
524 #[must_use]
526 pub fn append_system_prompt(mut self, prompt: impl Into<String>) -> Self {
527 self.append_system_prompt = Some(prompt.into());
528 self
529 }
530
531 #[must_use]
533 pub fn max_turns(mut self, max_turns: u32) -> Self {
534 self.max_turns = Some(max_turns);
535 self
536 }
537
538 #[must_use]
540 pub fn timeout(mut self, timeout: Duration) -> Self {
541 self.timeout = Some(timeout);
542 self
543 }
544
545 #[must_use]
550 pub fn stream_idle_timeout(mut self, timeout: Duration) -> Self {
551 self.stream_idle_timeout = Some(timeout);
552 self
553 }
554
555 #[must_use]
557 pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
558 self.fallback_model = Some(model.into());
559 self
560 }
561
562 #[must_use]
564 pub fn effort(mut self, effort: impl Into<String>) -> Self {
565 self.effort = Some(effort.into());
566 self
567 }
568
569 #[must_use]
571 pub fn max_budget_usd(mut self, budget: f64) -> Self {
572 self.max_budget_usd = Some(budget);
573 self
574 }
575
576 #[must_use]
578 pub fn allowed_tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
579 self.allowed_tools = tools.into_iter().map(Into::into).collect();
580 self
581 }
582
583 #[must_use]
585 pub fn add_allowed_tool(mut self, tool: impl Into<String>) -> Self {
586 self.allowed_tools.push(tool.into());
587 self
588 }
589
590 #[must_use]
592 pub fn disallowed_tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
593 self.disallowed_tools = tools.into_iter().map(Into::into).collect();
594 self
595 }
596
597 #[must_use]
599 pub fn add_disallowed_tool(mut self, tool: impl Into<String>) -> Self {
600 self.disallowed_tools.push(tool.into());
601 self
602 }
603
604 #[must_use]
606 pub fn tools(mut self, tools: impl Into<String>) -> Self {
607 self.tools = Some(tools.into());
608 self
609 }
610
611 #[must_use]
613 pub fn mcp_configs(mut self, configs: impl IntoIterator<Item = impl Into<String>>) -> Self {
614 self.mcp_config = configs.into_iter().map(Into::into).collect();
615 self
616 }
617
618 #[must_use]
620 pub fn add_mcp_config(mut self, config: impl Into<String>) -> Self {
621 self.mcp_config.push(config.into());
622 self
623 }
624
625 #[must_use]
627 pub fn setting_sources(mut self, sources: impl Into<String>) -> Self {
628 self.setting_sources = Some(sources.into());
629 self
630 }
631
632 #[must_use]
634 pub fn settings(mut self, settings: impl Into<String>) -> Self {
635 self.settings = Some(settings.into());
636 self
637 }
638
639 #[must_use]
641 pub fn json_schema(mut self, schema: impl Into<String>) -> Self {
642 self.json_schema = Some(schema.into());
643 self
644 }
645
646 #[must_use]
648 pub fn include_partial_messages(mut self, enabled: bool) -> Self {
649 self.include_partial_messages = Some(enabled);
650 self
651 }
652
653 #[must_use]
655 pub fn include_hook_events(mut self, enabled: bool) -> Self {
656 self.include_hook_events = Some(enabled);
657 self
658 }
659
660 #[must_use]
662 pub fn permission_mode(mut self, mode: impl Into<String>) -> Self {
663 self.permission_mode = Some(mode.into());
664 self
665 }
666
667 #[must_use]
669 pub fn dangerously_skip_permissions(mut self, enabled: bool) -> Self {
670 self.dangerously_skip_permissions = Some(enabled);
671 self
672 }
673
674 #[must_use]
676 pub fn add_dirs(mut self, dirs: impl IntoIterator<Item = impl Into<String>>) -> Self {
677 self.add_dir = dirs.into_iter().map(Into::into).collect();
678 self
679 }
680
681 #[must_use]
683 pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
684 self.add_dir.push(dir.into());
685 self
686 }
687
688 #[must_use]
690 pub fn files(mut self, files: impl IntoIterator<Item = impl Into<String>>) -> Self {
691 self.file = files.into_iter().map(Into::into).collect();
692 self
693 }
694
695 #[must_use]
697 pub fn file(mut self, file: impl Into<String>) -> Self {
698 self.file.push(file.into());
699 self
700 }
701
702 #[must_use]
704 pub fn resume(mut self, session_id: impl Into<String>) -> Self {
705 self.resume = Some(session_id.into());
706 self
707 }
708
709 #[must_use]
711 pub fn session_id(mut self, id: impl Into<String>) -> Self {
712 self.session_id = Some(id.into());
713 self
714 }
715
716 #[must_use]
718 pub fn bare(mut self, enabled: bool) -> Self {
719 self.bare = Some(enabled);
720 self
721 }
722
723 #[must_use]
726 pub fn no_session_persistence(mut self, enabled: bool) -> Self {
727 self.no_session_persistence = Some(enabled);
728 self
729 }
730
731 #[must_use]
734 pub fn disable_slash_commands(mut self, enabled: bool) -> Self {
735 self.disable_slash_commands = Some(enabled);
736 self
737 }
738
739 #[must_use]
742 pub fn strict_mcp_config(mut self, enabled: bool) -> Self {
743 self.strict_mcp_config = Some(enabled);
744 self
745 }
746
747 #[must_use]
752 pub fn extra_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
753 self.extra_args = args.into_iter().map(Into::into).collect();
754 self
755 }
756
757 #[must_use]
759 pub fn add_extra_arg(mut self, arg: impl Into<String>) -> Self {
760 self.extra_args.push(arg.into());
761 self
762 }
763
764 #[must_use]
766 pub fn build(self) -> ClaudeConfig {
767 ClaudeConfig {
768 preset: self.preset,
769 cli_path: self.cli_path,
770 model: self.model,
771 system_prompt: self.system_prompt,
772 append_system_prompt: self.append_system_prompt,
773 max_turns: self.max_turns,
774 timeout: self.timeout,
775 stream_idle_timeout: self.stream_idle_timeout,
776 fallback_model: self.fallback_model,
777 effort: self.effort,
778 max_budget_usd: self.max_budget_usd,
779 allowed_tools: self.allowed_tools,
780 disallowed_tools: self.disallowed_tools,
781 tools: self.tools,
782 mcp_config: self.mcp_config,
783 setting_sources: self.setting_sources,
784 settings: self.settings,
785 json_schema: self.json_schema,
786 include_partial_messages: self.include_partial_messages,
787 include_hook_events: self.include_hook_events,
788 permission_mode: self.permission_mode,
789 dangerously_skip_permissions: self.dangerously_skip_permissions,
790 add_dir: self.add_dir,
791 file: self.file,
792 resume: self.resume,
793 session_id: self.session_id,
794 bare: self.bare,
795 no_session_persistence: self.no_session_persistence,
796 disable_slash_commands: self.disable_slash_commands,
797 strict_mcp_config: self.strict_mcp_config,
798 extra_args: self.extra_args,
799 }
800 }
801}
802
803pub mod effort {
805 pub const LOW: &str = "low";
807 pub const MEDIUM: &str = "medium";
809 pub const HIGH: &str = "high";
811 pub const MAX: &str = "max";
813}
814
815pub mod permission_mode {
817 pub const DEFAULT: &str = "default";
819 pub const ACCEPT_EDITS: &str = "acceptEdits";
821 pub const AUTO: &str = "auto";
823 pub const BYPASS_PERMISSIONS: &str = "bypassPermissions";
825 pub const DONT_ASK: &str = "dontAsk";
827 pub const PLAN: &str = "plan";
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834
835 #[test]
836 fn default_config() {
837 let config = ClaudeConfig::default();
838 assert!(config.cli_path.is_none());
839 assert!(config.model.is_none());
840 assert!(config.system_prompt.is_none());
841 assert!(config.max_turns.is_none());
842 assert!(config.timeout.is_none());
843 }
844
845 #[test]
846 fn cli_path_or_default_returns_claude_when_none() {
847 let config = ClaudeConfig::default();
848 assert_eq!(config.cli_path_or_default(), "claude");
849 }
850
851 #[test]
852 fn cli_path_or_default_returns_custom_path() {
853 let config = ClaudeConfig::builder()
854 .cli_path("/usr/local/bin/claude-v2")
855 .build();
856 assert_eq!(config.cli_path_or_default(), "/usr/local/bin/claude-v2");
857 }
858
859 #[test]
860 fn builder_sets_cli_path() {
861 let config = ClaudeConfig::builder().cli_path("/opt/claude").build();
862 assert_eq!(config.cli_path.as_deref(), Some("/opt/claude"));
863 }
864
865 #[test]
866 fn builder_sets_stream_idle_timeout() {
867 let config = ClaudeConfig::builder()
868 .stream_idle_timeout(Duration::from_secs(60))
869 .build();
870 assert_eq!(config.stream_idle_timeout, Some(Duration::from_secs(60)));
871 }
872
873 #[test]
874 fn default_stream_idle_timeout_is_none() {
875 let config = ClaudeConfig::default();
876 assert!(config.stream_idle_timeout.is_none());
877 }
878
879 #[test]
880 fn builder_sets_all_fields() {
881 let config = ClaudeConfig::builder()
882 .model("haiku")
883 .system_prompt("You are helpful")
884 .max_turns(3)
885 .timeout(Duration::from_secs(30))
886 .build();
887
888 assert_eq!(config.model.as_deref(), Some("haiku"));
889 assert_eq!(config.system_prompt.as_deref(), Some("You are helpful"));
890 assert_eq!(config.max_turns, Some(3));
891 assert_eq!(config.timeout, Some(Duration::from_secs(30)));
892 }
893
894 #[test]
895 fn to_args_minimal() {
896 let config = ClaudeConfig::default();
897 let args = config.to_args("hello");
898
899 assert!(args.contains(&"--print".to_string()));
900 assert!(args.contains(&"json".to_string()));
901 assert!(args.contains(&"--no-session-persistence".to_string()));
902 assert!(args.contains(&"--disable-slash-commands".to_string()));
903 assert!(args.contains(&"--strict-mcp-config".to_string()));
904 let sp_idx = args.iter().position(|a| a == "--system-prompt").unwrap();
906 assert_eq!(args[sp_idx + 1], "");
907 assert!(!args.contains(&"--model".to_string()));
909 assert!(!args.contains(&"--max-turns".to_string()));
910 assert_eq!(args.last().unwrap(), "hello");
912 }
913
914 #[test]
915 fn to_args_with_options() {
916 let config = ClaudeConfig::builder()
917 .model("haiku")
918 .system_prompt("Be concise")
919 .max_turns(5)
920 .build();
921 let args = config.to_args("test prompt");
922
923 let model_idx = args.iter().position(|a| a == "--model").unwrap();
924 assert_eq!(args[model_idx + 1], "haiku");
925
926 let sp_idx = args.iter().position(|a| a == "--system-prompt").unwrap();
927 assert_eq!(args[sp_idx + 1], "Be concise");
928
929 let mt_idx = args.iter().position(|a| a == "--max-turns").unwrap();
930 assert_eq!(args[mt_idx + 1], "5");
931
932 assert_eq!(args.last().unwrap(), "test prompt");
933 }
934
935 #[test]
936 fn to_stream_args_minimal() {
937 let config = ClaudeConfig::default();
938 let args = config.to_stream_args("hello");
939
940 assert!(args.contains(&"--print".to_string()));
941 assert!(args.contains(&"stream-json".to_string()));
942 assert!(args.contains(&"--verbose".to_string()));
943 assert!(!args.contains(&"json".to_string()));
944 assert!(!args.contains(&"--include-partial-messages".to_string()));
945 assert_eq!(args.last().unwrap(), "hello");
946 }
947
948 #[test]
949 fn to_stream_args_with_partial_messages() {
950 let config = ClaudeConfig::builder()
951 .include_partial_messages(true)
952 .build();
953 let args = config.to_stream_args("hello");
954
955 assert!(args.contains(&"--include-partial-messages".to_string()));
956 }
957
958 #[test]
959 fn builder_sets_include_partial_messages() {
960 let config = ClaudeConfig::builder()
961 .include_partial_messages(true)
962 .build();
963 assert_eq!(config.include_partial_messages, Some(true));
964 }
965
966 #[test]
967 fn all_new_fields_in_builder() {
968 let config = ClaudeConfig::builder()
969 .append_system_prompt("extra context")
970 .fallback_model("haiku")
971 .effort("high")
972 .max_budget_usd(1.0)
973 .allowed_tools(["Bash", "Edit"])
974 .disallowed_tools(["Write"])
975 .tools("Bash,Edit")
976 .mcp_configs(["config.json"])
977 .setting_sources("user,project")
978 .settings("settings.json")
979 .json_schema(r#"{"type":"object"}"#)
980 .include_hook_events(true)
981 .permission_mode("auto")
982 .dangerously_skip_permissions(true)
983 .add_dirs(["/path/a"])
984 .file("spec:file.txt")
985 .resume("session-123")
986 .session_id("uuid-456")
987 .bare(true)
988 .no_session_persistence(false)
989 .disable_slash_commands(false)
990 .strict_mcp_config(false)
991 .extra_args(["--custom", "val"])
992 .build();
993
994 assert_eq!(
995 config.append_system_prompt.as_deref(),
996 Some("extra context")
997 );
998 assert_eq!(config.fallback_model.as_deref(), Some("haiku"));
999 assert_eq!(config.effort.as_deref(), Some("high"));
1000 assert_eq!(config.max_budget_usd, Some(1.0));
1001 assert_eq!(config.allowed_tools, vec!["Bash", "Edit"]);
1002 assert_eq!(config.disallowed_tools, vec!["Write"]);
1003 assert_eq!(config.tools.as_deref(), Some("Bash,Edit"));
1004 assert_eq!(config.mcp_config, vec!["config.json"]);
1005 assert_eq!(config.setting_sources.as_deref(), Some("user,project"));
1006 assert_eq!(config.settings.as_deref(), Some("settings.json"));
1007 assert_eq!(config.json_schema.as_deref(), Some(r#"{"type":"object"}"#));
1008 assert_eq!(config.include_hook_events, Some(true));
1009 assert_eq!(config.permission_mode.as_deref(), Some("auto"));
1010 assert_eq!(config.dangerously_skip_permissions, Some(true));
1011 assert_eq!(config.add_dir, vec!["/path/a"]);
1012 assert_eq!(config.file, vec!["spec:file.txt"]);
1013 assert_eq!(config.resume.as_deref(), Some("session-123"));
1014 assert_eq!(config.session_id.as_deref(), Some("uuid-456"));
1015 assert_eq!(config.bare, Some(true));
1016 assert_eq!(config.no_session_persistence, Some(false));
1017 assert_eq!(config.disable_slash_commands, Some(false));
1018 assert_eq!(config.strict_mcp_config, Some(false));
1019 assert_eq!(config.extra_args, vec!["--custom", "val"]);
1020 }
1021
1022 #[test]
1023 fn default_uses_minimal_context() {
1024 let config = ClaudeConfig::default();
1025 let args = config.to_args("test");
1026
1027 assert!(args.contains(&"--no-session-persistence".to_string()));
1028 assert!(args.contains(&"--strict-mcp-config".to_string()));
1029 assert!(args.contains(&"--disable-slash-commands".to_string()));
1030
1031 let ss_idx = args.iter().position(|a| a == "--setting-sources").unwrap();
1032 assert_eq!(args[ss_idx + 1], "");
1033
1034 let mcp_idx = args.iter().position(|a| a == "--mcp-config").unwrap();
1035 assert_eq!(args[mcp_idx + 1], r#"{"mcpServers":{}}"#);
1036
1037 let tools_idx = args.iter().position(|a| a == "--tools").unwrap();
1038 assert_eq!(args[tools_idx + 1], "");
1039 }
1040
1041 #[test]
1042 fn override_no_session_persistence_false() {
1043 let config = ClaudeConfig::builder()
1044 .no_session_persistence(false)
1045 .build();
1046 let args = config.to_args("test");
1047 assert!(!args.contains(&"--no-session-persistence".to_string()));
1048 }
1049
1050 #[test]
1051 fn override_strict_mcp_config_false() {
1052 let config = ClaudeConfig::builder().strict_mcp_config(false).build();
1053 let args = config.to_args("test");
1054 assert!(!args.contains(&"--strict-mcp-config".to_string()));
1055 }
1056
1057 #[test]
1058 fn override_disable_slash_commands_false() {
1059 let config = ClaudeConfig::builder()
1060 .disable_slash_commands(false)
1061 .build();
1062 let args = config.to_args("test");
1063 assert!(!args.contains(&"--disable-slash-commands".to_string()));
1064 }
1065
1066 #[test]
1067 fn override_tools() {
1068 let config = ClaudeConfig::builder().tools("Bash,Edit").build();
1069 let args = config.to_args("test");
1070 let idx = args.iter().position(|a| a == "--tools").unwrap();
1071 assert_eq!(args[idx + 1], "Bash,Edit");
1072 }
1073
1074 #[test]
1075 fn override_setting_sources() {
1076 let config = ClaudeConfig::builder()
1077 .setting_sources("user,project")
1078 .build();
1079 let args = config.to_args("test");
1080 let idx = args.iter().position(|a| a == "--setting-sources").unwrap();
1081 assert_eq!(args[idx + 1], "user,project");
1082 }
1083
1084 #[test]
1085 fn override_mcp_config() {
1086 let config = ClaudeConfig::builder()
1087 .mcp_configs(["path/config.json"])
1088 .build();
1089 let args = config.to_args("test");
1090 let idx = args.iter().position(|a| a == "--mcp-config").unwrap();
1091 assert_eq!(args[idx + 1], "path/config.json");
1092 assert!(!args.contains(&r#"{"mcpServers":{}}"#.to_string()));
1093 }
1094
1095 #[test]
1096 fn effort_with_constant() {
1097 let config = ClaudeConfig::builder().effort(effort::HIGH).build();
1098 let args = config.to_args("test");
1099 let idx = args.iter().position(|a| a == "--effort").unwrap();
1100 assert_eq!(args[idx + 1], "high");
1101 }
1102
1103 #[test]
1104 fn effort_with_custom_string() {
1105 let config = ClaudeConfig::builder().effort("ultra").build();
1106 let args = config.to_args("test");
1107 let idx = args.iter().position(|a| a == "--effort").unwrap();
1108 assert_eq!(args[idx + 1], "ultra");
1109 }
1110
1111 #[test]
1112 fn allowed_tools_multiple() {
1113 let config = ClaudeConfig::builder()
1114 .allowed_tools(["Bash(git:*)", "Edit", "Read"])
1115 .build();
1116 let args = config.to_args("test");
1117 let idx = args.iter().position(|a| a == "--allowedTools").unwrap();
1118 assert_eq!(args[idx + 1], "Bash(git:*)");
1119 assert_eq!(args[idx + 2], "Edit");
1120 assert_eq!(args[idx + 3], "Read");
1121 }
1122
1123 #[test]
1124 fn bare_flag() {
1125 let config = ClaudeConfig::builder().bare(true).build();
1126 let args = config.to_args("test");
1127 assert!(args.contains(&"--bare".to_string()));
1128 }
1129
1130 #[test]
1131 fn dangerously_skip_permissions_flag() {
1132 let config = ClaudeConfig::builder()
1133 .dangerously_skip_permissions(true)
1134 .build();
1135 let args = config.to_args("test");
1136 assert!(args.contains(&"--dangerously-skip-permissions".to_string()));
1137 }
1138
1139 #[test]
1140 fn resume_session() {
1141 let config = ClaudeConfig::builder().resume("session-abc").build();
1142 let args = config.to_args("test");
1143 let idx = args.iter().position(|a| a == "--resume").unwrap();
1144 assert_eq!(args[idx + 1], "session-abc");
1145 }
1146
1147 #[test]
1148 fn session_id_field() {
1149 let config = ClaudeConfig::builder()
1150 .session_id("550e8400-e29b-41d4-a716-446655440000")
1151 .build();
1152 let args = config.to_args("test");
1153 let idx = args.iter().position(|a| a == "--session-id").unwrap();
1154 assert_eq!(args[idx + 1], "550e8400-e29b-41d4-a716-446655440000");
1155 }
1156
1157 #[test]
1158 fn json_schema_field() {
1159 let schema = r#"{"type":"object","properties":{"name":{"type":"string"}}}"#;
1160 let config = ClaudeConfig::builder().json_schema(schema).build();
1161 let args = config.to_args("test");
1162 let idx = args.iter().position(|a| a == "--json-schema").unwrap();
1163 assert_eq!(args[idx + 1], schema);
1164 }
1165
1166 #[test]
1167 fn add_dir_multiple() {
1168 let config = ClaudeConfig::builder()
1169 .add_dirs(["/path/a", "/path/b"])
1170 .build();
1171 let args = config.to_args("test");
1172 let idx = args.iter().position(|a| a == "--add-dir").unwrap();
1173 assert_eq!(args[idx + 1], "/path/a");
1174 assert_eq!(args[idx + 2], "/path/b");
1175 }
1176
1177 #[test]
1178 fn file_multiple() {
1179 let config = ClaudeConfig::builder()
1180 .files(["file_abc:doc.txt", "file_def:img.png"])
1181 .build();
1182 let args = config.to_args("test");
1183 let idx = args.iter().position(|a| a == "--file").unwrap();
1184 assert_eq!(args[idx + 1], "file_abc:doc.txt");
1185 assert_eq!(args[idx + 2], "file_def:img.png");
1186 }
1187
1188 #[test]
1189 fn extra_args_before_prompt() {
1190 let config = ClaudeConfig::builder()
1191 .extra_args(["--custom-flag", "value"])
1192 .build();
1193 let args = config.to_args("my prompt");
1194 let custom_idx = args.iter().position(|a| a == "--custom-flag").unwrap();
1195 let prompt_idx = args.iter().position(|a| a == "my prompt").unwrap();
1196 assert!(custom_idx < prompt_idx);
1197 assert_eq!(args[custom_idx + 1], "value");
1198 }
1199
1200 #[test]
1201 fn extra_args_with_typed_fields() {
1202 let config = ClaudeConfig::builder()
1203 .model("sonnet")
1204 .extra_args(["--custom", "val"])
1205 .build();
1206 let args = config.to_args("test");
1207 assert!(args.contains(&"--model".to_string()));
1208 assert!(args.contains(&"sonnet".to_string()));
1209 assert!(args.contains(&"--custom".to_string()));
1210 assert!(args.contains(&"val".to_string()));
1211 }
1212
1213 #[test]
1214 fn disallowed_tools_multiple() {
1215 let config = ClaudeConfig::builder()
1216 .disallowed_tools(["Write", "Bash"])
1217 .build();
1218 let args = config.to_args("test");
1219 let idx = args.iter().position(|a| a == "--disallowedTools").unwrap();
1220 assert_eq!(args[idx + 1], "Write");
1221 assert_eq!(args[idx + 2], "Bash");
1222 }
1223
1224 #[test]
1225 fn to_builder_round_trip_fields() {
1226 let original = ClaudeConfig::builder()
1227 .cli_path("/custom/claude")
1228 .model("haiku")
1229 .system_prompt("test")
1230 .max_turns(5)
1231 .timeout(Duration::from_secs(30))
1232 .stream_idle_timeout(Duration::from_secs(45))
1233 .no_session_persistence(false)
1234 .resume("session-123")
1235 .build();
1236
1237 let rebuilt = original.to_builder().build();
1238
1239 assert_eq!(rebuilt.cli_path, original.cli_path);
1240 assert_eq!(rebuilt.model, original.model);
1241 assert_eq!(rebuilt.system_prompt, original.system_prompt);
1242 assert_eq!(rebuilt.max_turns, original.max_turns);
1243 assert_eq!(rebuilt.timeout, original.timeout);
1244 assert_eq!(rebuilt.stream_idle_timeout, original.stream_idle_timeout);
1245 assert_eq!(
1246 rebuilt.no_session_persistence,
1247 original.no_session_persistence
1248 );
1249 assert_eq!(rebuilt.resume, original.resume);
1250 }
1251
1252 #[test]
1253 fn to_builder_round_trip_args() {
1254 let config = ClaudeConfig::builder()
1255 .model("haiku")
1256 .max_turns(3)
1257 .effort("high")
1258 .allowed_tools(["Bash", "Read"])
1259 .no_session_persistence(false)
1260 .build();
1261
1262 let rebuilt = config.to_builder().build();
1263 assert_eq!(config.to_args("hi"), rebuilt.to_args("hi"));
1264 }
1265
1266 #[test]
1269 fn default_preset_is_normal() {
1270 let config = ClaudeConfig::default();
1271 assert_eq!(config.preset, Preset::Normal);
1272 }
1273
1274 #[test]
1275 fn builder_default_preset_is_normal() {
1276 let config = ClaudeConfig::builder().build();
1277 assert_eq!(config.preset, Preset::Normal);
1278 }
1279
1280 #[test]
1281 fn explicit_normal_preset_matches_default_to_args() {
1282 let default_config = ClaudeConfig::default();
1283 let explicit_config = ClaudeConfig::builder().preset(Preset::Normal).build();
1284 assert_eq!(
1285 default_config.to_args("test"),
1286 explicit_config.to_args("test")
1287 );
1288 }
1289
1290 #[test]
1291 fn explicit_normal_preset_matches_default_to_stream_args() {
1292 let default_config = ClaudeConfig::default();
1293 let explicit_config = ClaudeConfig::builder().preset(Preset::Normal).build();
1294 assert_eq!(
1295 default_config.to_stream_args("test"),
1296 explicit_config.to_stream_args("test")
1297 );
1298 }
1299
1300 #[test]
1301 fn to_builder_preserves_preset() {
1302 let config = ClaudeConfig::builder()
1303 .preset(Preset::Minimal)
1304 .model("haiku")
1305 .build();
1306 let rebuilt = config.to_builder().build();
1307 assert_eq!(rebuilt.preset, Preset::Minimal);
1308 }
1309
1310 #[test]
1311 fn minimal_preset_to_args() {
1312 let config = ClaudeConfig::builder().preset(Preset::Minimal).build();
1313 let args = config.to_args("test");
1314
1315 assert!(args.contains(&"--print".to_string()));
1316 assert!(args.contains(&"--output-format".to_string()));
1317 assert!(args.contains(&"json".to_string()));
1318 assert_eq!(args.last().unwrap(), "test");
1319
1320 assert!(!args.contains(&"--no-session-persistence".to_string()));
1322 assert!(!args.contains(&"--strict-mcp-config".to_string()));
1323 assert!(!args.contains(&"--disable-slash-commands".to_string()));
1324 assert!(!args.contains(&"--setting-sources".to_string()));
1325 assert!(!args.contains(&"--mcp-config".to_string()));
1326 assert!(!args.contains(&"--tools".to_string()));
1327 assert!(!args.contains(&"--system-prompt".to_string()));
1328 }
1329
1330 #[test]
1331 fn minimal_preset_to_stream_args() {
1332 let config = ClaudeConfig::builder().preset(Preset::Minimal).build();
1333 let args = config.to_stream_args("test");
1334
1335 assert!(args.contains(&"--print".to_string()));
1336 assert!(args.contains(&"--output-format".to_string()));
1337 assert!(args.contains(&"stream-json".to_string()));
1338 assert!(args.contains(&"--verbose".to_string()));
1339 assert_eq!(args.last().unwrap(), "test");
1340
1341 assert!(!args.contains(&"--no-session-persistence".to_string()));
1342 assert!(!args.contains(&"--system-prompt".to_string()));
1343 }
1344
1345 #[test]
1346 fn minimal_preset_with_builder_add() {
1347 let config = ClaudeConfig::builder()
1348 .preset(Preset::Minimal)
1349 .no_session_persistence(true)
1350 .model("haiku")
1351 .build();
1352 let args = config.to_args("test");
1353
1354 assert!(args.contains(&"--print".to_string()));
1355 assert!(args.contains(&"--no-session-persistence".to_string()));
1356 assert!(args.contains(&"--model".to_string()));
1357 }
1358
1359 #[test]
1360 fn minimal_preset_with_system_prompt() {
1361 let config = ClaudeConfig::builder()
1362 .preset(Preset::Minimal)
1363 .system_prompt("Be helpful")
1364 .build();
1365 let args = config.to_args("test");
1366
1367 let idx = args.iter().position(|a| a == "--system-prompt").unwrap();
1368 assert_eq!(args[idx + 1], "Be helpful");
1369 }
1370
1371 #[test]
1372 fn bare_preset_to_args() {
1373 let config = ClaudeConfig::builder().preset(Preset::Bare).build();
1374 let args = config.to_args("test");
1375
1376 assert!(args.contains(&"--output-format".to_string()));
1378 assert!(args.contains(&"json".to_string()));
1379 assert_eq!(args.last().unwrap(), "test");
1380
1381 assert!(!args.contains(&"--print".to_string()));
1383 assert!(!args.contains(&"--no-session-persistence".to_string()));
1384 assert!(!args.contains(&"--strict-mcp-config".to_string()));
1385 assert!(!args.contains(&"--disable-slash-commands".to_string()));
1386 assert!(!args.contains(&"--setting-sources".to_string()));
1387 assert!(!args.contains(&"--mcp-config".to_string()));
1388 assert!(!args.contains(&"--tools".to_string()));
1389 assert!(!args.contains(&"--system-prompt".to_string()));
1390 }
1391
1392 #[test]
1393 fn bare_preset_to_stream_args() {
1394 let config = ClaudeConfig::builder().preset(Preset::Bare).build();
1395 let args = config.to_stream_args("test");
1396
1397 assert!(args.contains(&"--output-format".to_string()));
1398 assert!(args.contains(&"stream-json".to_string()));
1399 assert!(args.contains(&"--verbose".to_string()));
1400 assert_eq!(args.last().unwrap(), "test");
1401
1402 assert!(!args.contains(&"--print".to_string()));
1403 }
1404
1405 #[test]
1406 fn bare_preset_with_extra_args() {
1407 let config = ClaudeConfig::builder()
1408 .preset(Preset::Bare)
1409 .extra_args(["--print", "--cli-mode"])
1410 .build();
1411 let args = config.to_args("test");
1412
1413 assert!(args.contains(&"--print".to_string()));
1414 assert!(args.contains(&"--cli-mode".to_string()));
1415 }
1416
1417 #[test]
1418 fn extra_args_after_format() {
1419 let config = ClaudeConfig::builder()
1420 .preset(Preset::Bare)
1421 .extra_args(["--new-flag"])
1422 .build();
1423 let args = config.to_args("test");
1424
1425 let format_idx = args.iter().position(|a| a == "--output-format").unwrap();
1427 let flag_idx = args.iter().position(|a| a == "--new-flag").unwrap();
1428 assert!(flag_idx > format_idx);
1429 }
1430
1431 #[test]
1434 fn custom_preset_to_args() {
1435 let config = ClaudeConfig::builder()
1436 .preset(Preset::Custom(vec![
1437 "--print".into(),
1438 "--no-session-persistence".into(),
1439 ]))
1440 .build();
1441 let args = config.to_args("test");
1442
1443 assert!(args.contains(&"--print".to_string()));
1444 assert!(args.contains(&"--no-session-persistence".to_string()));
1445 assert!(args.contains(&"--output-format".to_string()));
1446 assert_eq!(args.last().unwrap(), "test");
1447
1448 assert!(!args.contains(&"--strict-mcp-config".to_string()));
1450 assert!(!args.contains(&"--disable-slash-commands".to_string()));
1451 }
1452
1453 #[test]
1454 fn custom_preset_is_reusable() {
1455 let preset = Preset::Custom(vec!["--print".into(), "--no-session-persistence".into()]);
1456
1457 let config1 = ClaudeConfig::builder()
1458 .preset(preset.clone())
1459 .model("haiku")
1460 .build();
1461 let config2 = ClaudeConfig::builder()
1462 .preset(preset)
1463 .model("sonnet")
1464 .build();
1465
1466 let args1 = config1.to_args("test");
1467 let args2 = config2.to_args("test");
1468
1469 assert!(args1.contains(&"--print".to_string()));
1471 assert!(args2.contains(&"--print".to_string()));
1472 assert!(args1.contains(&"--no-session-persistence".to_string()));
1473 assert!(args2.contains(&"--no-session-persistence".to_string()));
1474 }
1475
1476 #[test]
1477 fn custom_preset_builder_override_remove() {
1478 let config = ClaudeConfig::builder()
1479 .preset(Preset::Custom(vec![
1480 "--print".into(),
1481 "--no-session-persistence".into(),
1482 ]))
1483 .no_session_persistence(false) .build();
1485 let args = config.to_args("test");
1486
1487 assert!(args.contains(&"--print".to_string()));
1488 assert!(!args.contains(&"--no-session-persistence".to_string()));
1489 }
1490
1491 #[test]
1492 fn custom_preset_builder_override_remove_strict_mcp() {
1493 let config = ClaudeConfig::builder()
1494 .preset(Preset::Custom(vec![
1495 "--print".into(),
1496 "--strict-mcp-config".into(),
1497 ]))
1498 .strict_mcp_config(false)
1499 .build();
1500 let args = config.to_args("test");
1501
1502 assert!(!args.contains(&"--strict-mcp-config".to_string()));
1503 }
1504
1505 #[test]
1506 fn custom_preset_builder_override_remove_disable_slash_commands() {
1507 let config = ClaudeConfig::builder()
1508 .preset(Preset::Custom(vec![
1509 "--print".into(),
1510 "--disable-slash-commands".into(),
1511 ]))
1512 .disable_slash_commands(false)
1513 .build();
1514 let args = config.to_args("test");
1515
1516 assert!(!args.contains(&"--disable-slash-commands".to_string()));
1517 }
1518
1519 #[test]
1522 fn priority_extra_args_appended_last() {
1523 let config = ClaudeConfig::builder()
1524 .preset(Preset::Normal)
1525 .extra_args(["--new-flag"])
1526 .build();
1527 let args = config.to_args("test");
1528
1529 let prompt_idx = args.iter().position(|a| a == "test").unwrap();
1530 let flag_idx = args.iter().position(|a| a == "--new-flag").unwrap();
1531 assert!(flag_idx < prompt_idx);
1532 assert!(flag_idx > 0); }
1534
1535 #[test]
1536 fn priority_extra_args_overrides_format() {
1537 let config = ClaudeConfig::builder()
1538 .preset(Preset::Normal)
1539 .extra_args(["--output-format", "new"])
1540 .build();
1541 let args = config.to_args("test");
1542
1543 let format_positions: Vec<_> = args
1545 .iter()
1546 .enumerate()
1547 .filter(|(_, a)| a.as_str() == "--output-format")
1548 .map(|(i, _)| i)
1549 .collect();
1550 assert_eq!(format_positions.len(), 2);
1551 assert!(format_positions[1] > format_positions[0]);
1553 }
1554
1555 #[test]
1556 fn priority_full_stack() {
1557 let config = ClaudeConfig::builder()
1558 .preset(Preset::Minimal)
1559 .model("haiku")
1560 .extra_args(["--model", "sonnet"])
1561 .build();
1562 let args = config.to_args("test");
1563
1564 let model_positions: Vec<_> = args
1566 .iter()
1567 .enumerate()
1568 .filter(|(_, a)| a.as_str() == "--model")
1569 .map(|(i, _)| i)
1570 .collect();
1571 assert_eq!(model_positions.len(), 2);
1572 assert_eq!(args[model_positions[0] + 1], "haiku");
1574 assert_eq!(args[model_positions[1] + 1], "sonnet");
1575 }
1576
1577 #[test]
1580 fn bool_flag_none_follows_normal_preset() {
1581 let config = ClaudeConfig::builder().preset(Preset::Normal).build();
1583 let args = config.to_args("test");
1584 assert!(args.contains(&"--no-session-persistence".to_string()));
1585 assert!(args.contains(&"--strict-mcp-config".to_string()));
1586 assert!(args.contains(&"--disable-slash-commands".to_string()));
1587 }
1588
1589 #[test]
1590 fn bool_flag_none_follows_minimal_preset() {
1591 let config = ClaudeConfig::builder().preset(Preset::Minimal).build();
1593 let args = config.to_args("test");
1594 assert!(!args.contains(&"--no-session-persistence".to_string()));
1595 assert!(!args.contains(&"--strict-mcp-config".to_string()));
1596 assert!(!args.contains(&"--disable-slash-commands".to_string()));
1597 }
1598
1599 #[test]
1600 fn bool_flag_true_adds_to_minimal() {
1601 let config = ClaudeConfig::builder()
1602 .preset(Preset::Minimal)
1603 .no_session_persistence(true)
1604 .strict_mcp_config(true)
1605 .disable_slash_commands(true)
1606 .build();
1607 let args = config.to_args("test");
1608 assert!(args.contains(&"--no-session-persistence".to_string()));
1609 assert!(args.contains(&"--strict-mcp-config".to_string()));
1610 assert!(args.contains(&"--disable-slash-commands".to_string()));
1611 }
1612
1613 #[test]
1614 fn bool_flag_false_removes_from_normal() {
1615 let config = ClaudeConfig::builder()
1616 .preset(Preset::Normal)
1617 .no_session_persistence(false)
1618 .strict_mcp_config(false)
1619 .disable_slash_commands(false)
1620 .build();
1621 let args = config.to_args("test");
1622 assert!(!args.contains(&"--no-session-persistence".to_string()));
1623 assert!(!args.contains(&"--strict-mcp-config".to_string()));
1624 assert!(!args.contains(&"--disable-slash-commands".to_string()));
1625 }
1626
1627 #[test]
1630 fn normal_preset_contains_print() {
1631 let config = ClaudeConfig::builder().preset(Preset::Normal).build();
1632 let args = config.to_args("test");
1633 assert!(args.contains(&"--print".to_string()));
1634 }
1635
1636 #[test]
1637 fn minimal_preset_contains_print() {
1638 let config = ClaudeConfig::builder().preset(Preset::Minimal).build();
1639 let args = config.to_args("test");
1640 assert!(args.contains(&"--print".to_string()));
1641 }
1642
1643 #[test]
1644 fn bare_preset_no_print() {
1645 let config = ClaudeConfig::builder().preset(Preset::Bare).build();
1646 let args = config.to_args("test");
1647 assert!(!args.contains(&"--print".to_string()));
1648 }
1649
1650 #[test]
1651 fn bare_preset_with_print_in_extra_args() {
1652 let config = ClaudeConfig::builder()
1653 .preset(Preset::Bare)
1654 .extra_args(["-p"])
1655 .build();
1656 let args = config.to_args("test");
1657 assert!(args.contains(&"-p".to_string()));
1658 }
1659
1660 #[test]
1661 fn custom_preset_without_print() {
1662 let config = ClaudeConfig::builder()
1663 .preset(Preset::Custom(vec!["--no-session-persistence".into()]))
1664 .build();
1665 let args = config.to_args("test");
1666 assert!(!args.contains(&"--print".to_string()));
1667 }
1668
1669 #[cfg(feature = "tracing")]
1670 #[tracing_test::traced_test]
1671 #[test]
1672 fn warn_if_no_print_fires_for_bare_preset() {
1673 let config = ClaudeConfig::builder().preset(Preset::Bare).build();
1674 let _args = config.to_args("test");
1675 assert!(logs_contain("args do not contain --print"));
1676 }
1677
1678 #[cfg(feature = "tracing")]
1679 #[tracing_test::traced_test]
1680 #[test]
1681 fn warn_if_no_print_fires_for_bare_preset_stream() {
1682 let config = ClaudeConfig::builder().preset(Preset::Bare).build();
1683 let _args = config.to_stream_args("test");
1684 assert!(logs_contain("args do not contain --print"));
1685 }
1686
1687 #[cfg(feature = "tracing")]
1688 #[tracing_test::traced_test]
1689 #[test]
1690 fn warn_if_no_print_fires_for_custom_preset_without_print() {
1691 let config = ClaudeConfig::builder()
1692 .preset(Preset::Custom(vec!["--no-session-persistence".into()]))
1693 .build();
1694 let _args = config.to_args("test");
1695 assert!(logs_contain("args do not contain --print"));
1696 }
1697
1698 #[cfg(feature = "tracing")]
1699 #[tracing_test::traced_test]
1700 #[test]
1701 fn warn_if_no_print_does_not_fire_for_normal_preset() {
1702 let config = ClaudeConfig::builder().preset(Preset::Normal).build();
1703 let _args = config.to_args("test");
1704 assert!(!logs_contain("args do not contain --print"));
1705 }
1706
1707 #[cfg(feature = "tracing")]
1708 #[tracing_test::traced_test]
1709 #[test]
1710 fn warn_if_no_print_does_not_fire_for_minimal_preset() {
1711 let config = ClaudeConfig::builder().preset(Preset::Minimal).build();
1712 let _args = config.to_args("test");
1713 assert!(!logs_contain("args do not contain --print"));
1714 }
1715
1716 #[cfg(feature = "tracing")]
1717 #[tracing_test::traced_test]
1718 #[test]
1719 fn warn_if_no_print_does_not_fire_for_custom_preset_with_print() {
1720 let config = ClaudeConfig::builder()
1721 .preset(Preset::Custom(vec!["--print".into()]))
1722 .build();
1723 let _args = config.to_args("test");
1724 assert!(!logs_contain("args do not contain --print"));
1725 }
1726}