1use ralph_core::{CliConfig, HatBackend};
4use std::fmt;
5use std::io::Write;
6use tempfile::NamedTempFile;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum OutputFormat {
14 #[default]
16 Text,
17 StreamJson,
19 CopilotStreamJson,
21 PiStreamJson,
23 Acp,
25}
26
27#[derive(Debug, Clone)]
29pub struct CustomBackendError;
30
31impl fmt::Display for CustomBackendError {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 write!(f, "custom backend requires a command to be specified")
34 }
35}
36
37impl std::error::Error for CustomBackendError {}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum PromptMode {
42 Arg,
44 Stdin,
46}
47
48#[derive(Debug, Clone)]
50pub struct CliBackend {
51 pub command: String,
53 pub args: Vec<String>,
55 pub prompt_mode: PromptMode,
57 pub prompt_flag: Option<String>,
59 pub output_format: OutputFormat,
61 pub env_vars: Vec<(String, String)>,
63}
64
65impl CliBackend {
66 pub fn from_config(config: &CliConfig) -> Result<Self, CustomBackendError> {
71 let mut backend = match config.backend.as_str() {
72 "claude" => Self::claude(),
73 "kiro" => Self::kiro(),
74 "kiro-acp" => Self::kiro_acp(),
75 "gemini" => Self::gemini(),
76 "codex" => Self::codex(),
77 "amp" => Self::amp(),
78 "copilot" => Self::copilot(),
79 "opencode" => Self::opencode(),
80 "pi" => Self::pi(),
81 "roo" => Self::roo(),
82 "custom" => return Self::custom(config),
83 _ => Self::claude(), };
85
86 backend.args.extend(config.args.iter().cloned());
89 if backend.command == "codex" {
90 Self::reconcile_codex_args(&mut backend.args);
91 }
92
93 if let Some(ref cmd) = config.command {
95 backend.command = cmd.clone();
96 }
97
98 Ok(backend)
99 }
100
101 pub fn claude() -> Self {
111 Self {
112 command: "claude".to_string(),
113 args: vec![
114 "--dangerously-skip-permissions".to_string(),
115 "--verbose".to_string(),
116 "--output-format".to_string(),
117 "stream-json".to_string(),
118 "--setting-sources".to_string(),
119 "project,local".to_string(),
120 "--print".to_string(),
121 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet".to_string(),
122 ],
123 prompt_mode: PromptMode::Stdin,
124 prompt_flag: None,
125 output_format: OutputFormat::StreamJson,
126 env_vars: vec![],
127 }
128 }
129
130 pub fn claude_interactive() -> Self {
140 Self {
141 command: "claude".to_string(),
142 args: vec![
143 "--dangerously-skip-permissions".to_string(),
144 "--setting-sources".to_string(),
145 "project,local".to_string(),
146 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet".to_string(),
147 ],
148 prompt_mode: PromptMode::Arg,
149 prompt_flag: None,
150 output_format: OutputFormat::Text,
151 env_vars: vec![],
152 }
153 }
154
155 pub fn kiro() -> Self {
159 Self {
160 command: "kiro-cli".to_string(),
161 args: vec![
162 "chat".to_string(),
163 "--no-interactive".to_string(),
164 "--trust-all-tools".to_string(),
165 ],
166 prompt_mode: PromptMode::Arg,
167 prompt_flag: None,
168 output_format: OutputFormat::Text,
169 env_vars: vec![],
170 }
171 }
172
173 pub fn kiro_with_agent(agent: String, extra_args: &[String]) -> Self {
177 let mut backend = Self {
178 command: "kiro-cli".to_string(),
179 args: vec![
180 "chat".to_string(),
181 "--no-interactive".to_string(),
182 "--trust-all-tools".to_string(),
183 "--agent".to_string(),
184 agent,
185 ],
186 prompt_mode: PromptMode::Arg,
187 prompt_flag: None,
188 output_format: OutputFormat::Text,
189 env_vars: vec![],
190 };
191 backend.args.extend(extra_args.iter().cloned());
192 backend
193 }
194
195 pub fn kiro_acp() -> Self {
200 Self::kiro_acp_with_options(None, None)
201 }
202
203 pub fn kiro_acp_with_options(agent: Option<&str>, model: Option<&str>) -> Self {
205 let mut args = vec!["acp".to_string()];
206 if let Some(name) = agent {
207 args.push("--agent".to_string());
208 args.push(name.to_string());
209 }
210 if let Some(m) = model {
211 args.push("--model".to_string());
212 args.push(m.to_string());
213 }
214 Self {
215 command: "kiro-cli".to_string(),
216 args,
217 prompt_mode: PromptMode::Stdin,
218 prompt_flag: None,
219 output_format: OutputFormat::Acp,
220 env_vars: vec![],
221 }
222 }
223
224 pub fn from_name_with_args(
229 name: &str,
230 extra_args: &[String],
231 ) -> Result<Self, CustomBackendError> {
232 let mut backend = Self::from_name(name)?;
233 backend.args.extend(extra_args.iter().cloned());
234 if backend.command == "codex" {
235 Self::reconcile_codex_args(&mut backend.args);
236 }
237 Ok(backend)
238 }
239
240 pub fn from_name(name: &str) -> Result<Self, CustomBackendError> {
245 match name {
246 "claude" => Ok(Self::claude()),
247 "kiro" => Ok(Self::kiro()),
248 "kiro-acp" => Ok(Self::kiro_acp()),
249 "gemini" => Ok(Self::gemini()),
250 "codex" => Ok(Self::codex()),
251 "amp" => Ok(Self::amp()),
252 "copilot" => Ok(Self::copilot()),
253 "opencode" => Ok(Self::opencode()),
254 "pi" => Ok(Self::pi()),
255 "roo" => Ok(Self::roo()),
256 _ => Err(CustomBackendError),
257 }
258 }
259
260 pub fn from_hat_backend(hat_backend: &HatBackend) -> Result<Self, CustomBackendError> {
265 match hat_backend {
266 HatBackend::Named(name) => Self::from_name(name),
267 HatBackend::NamedWithArgs { backend_type, args } => {
268 Self::from_name_with_args(backend_type, args)
269 }
270 HatBackend::KiroAgent {
271 backend_type,
272 agent,
273 args,
274 } => {
275 if backend_type == "kiro-acp" {
276 Ok(Self::kiro_acp_with_options(Some(agent), None))
277 } else {
278 Ok(Self::kiro_with_agent(agent.clone(), args))
279 }
280 }
281 HatBackend::Custom { command, args } => Ok(Self {
282 command: command.clone(),
283 args: args.clone(),
284 prompt_mode: PromptMode::Arg,
285 prompt_flag: None,
286 output_format: OutputFormat::Text,
287 env_vars: vec![],
288 }),
289 }
290 }
291
292 pub fn gemini() -> Self {
294 Self {
295 command: "gemini".to_string(),
296 args: vec!["--yolo".to_string()],
297 prompt_mode: PromptMode::Arg,
298 prompt_flag: Some("-p".to_string()),
299 output_format: OutputFormat::Text,
300 env_vars: vec![],
301 }
302 }
303
304 pub fn codex() -> Self {
306 Self {
307 command: "codex".to_string(),
308 args: vec!["exec".to_string(), "--yolo".to_string()],
309 prompt_mode: PromptMode::Arg,
310 prompt_flag: None, output_format: OutputFormat::Text,
312 env_vars: vec![],
313 }
314 }
315
316 pub fn amp() -> Self {
318 Self {
319 command: "amp".to_string(),
320 args: vec!["--dangerously-allow-all".to_string()],
321 prompt_mode: PromptMode::Arg,
322 prompt_flag: Some("-x".to_string()),
323 output_format: OutputFormat::Text,
324 env_vars: vec![],
325 }
326 }
327
328 pub fn copilot() -> Self {
333 Self {
334 command: "copilot".to_string(),
335 args: vec![
336 "--allow-all-tools".to_string(),
337 "--output-format".to_string(),
338 "json".to_string(),
339 ],
340 prompt_mode: PromptMode::Arg,
341 prompt_flag: Some("-p".to_string()),
342 output_format: OutputFormat::CopilotStreamJson,
343 env_vars: vec![],
344 }
345 }
346
347 pub fn copilot_tui() -> Self {
353 Self {
354 command: "copilot".to_string(),
355 args: vec![], prompt_mode: PromptMode::Arg,
357 prompt_flag: None, output_format: OutputFormat::Text,
359 env_vars: vec![],
360 }
361 }
362
363 pub fn claude_interactive_teams() -> Self {
368 Self {
369 command: "claude".to_string(),
370 args: vec![
371 "--dangerously-skip-permissions".to_string(),
372 "--setting-sources".to_string(),
373 "project,local".to_string(),
374 "--disallowedTools=TodoWrite".to_string(),
375 ],
376 prompt_mode: PromptMode::Arg,
377 prompt_flag: None,
378 output_format: OutputFormat::Text,
379 env_vars: vec![(
380 "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS".to_string(),
381 "1".to_string(),
382 )],
383 }
384 }
385
386 pub fn for_interactive_prompt(backend_name: &str) -> Result<Self, CustomBackendError> {
405 match backend_name {
406 "claude" => Ok(Self::claude_interactive()),
407 "kiro" | "kiro-acp" => Ok(Self::kiro_interactive()),
411 "gemini" => Ok(Self::gemini_interactive()),
412 "codex" => Ok(Self::codex_interactive()),
413 "amp" => Ok(Self::amp_interactive()),
414 "copilot" => Ok(Self::copilot_interactive()),
415 "opencode" => Ok(Self::opencode_interactive()),
416 "pi" => Ok(Self::pi_interactive()),
417 "roo" => Ok(Self::roo_interactive()),
418 _ => Err(CustomBackendError),
419 }
420 }
421
422 pub fn kiro_interactive() -> Self {
427 Self {
428 command: "kiro-cli".to_string(),
429 args: vec!["chat".to_string(), "--trust-all-tools".to_string()],
430 prompt_mode: PromptMode::Arg,
431 prompt_flag: None,
432 output_format: OutputFormat::Text,
433 env_vars: vec![],
434 }
435 }
436
437 pub fn gemini_interactive() -> Self {
442 Self {
443 command: "gemini".to_string(),
444 args: vec!["--yolo".to_string()],
445 prompt_mode: PromptMode::Arg,
446 prompt_flag: Some("-i".to_string()), output_format: OutputFormat::Text,
448 env_vars: vec![],
449 }
450 }
451
452 pub fn codex_interactive() -> Self {
457 Self {
458 command: "codex".to_string(),
459 args: vec![], prompt_mode: PromptMode::Arg,
461 prompt_flag: None, output_format: OutputFormat::Text,
463 env_vars: vec![],
464 }
465 }
466
467 pub fn amp_interactive() -> Self {
472 Self {
473 command: "amp".to_string(),
474 args: vec![],
475 prompt_mode: PromptMode::Arg,
476 prompt_flag: Some("-x".to_string()),
477 output_format: OutputFormat::Text,
478 env_vars: vec![],
479 }
480 }
481
482 pub fn copilot_interactive() -> Self {
487 Self {
488 command: "copilot".to_string(),
489 args: vec![],
490 prompt_mode: PromptMode::Arg,
491 prompt_flag: Some("-p".to_string()),
492 output_format: OutputFormat::Text,
493 env_vars: vec![],
494 }
495 }
496
497 pub fn opencode() -> Self {
507 Self {
508 command: "opencode".to_string(),
509 args: vec!["run".to_string()],
510 prompt_mode: PromptMode::Arg,
511 prompt_flag: None, output_format: OutputFormat::Text,
513 env_vars: vec![],
514 }
515 }
516
517 pub fn opencode_tui() -> Self {
525 Self {
526 command: "opencode".to_string(),
527 args: vec!["run".to_string()],
528 prompt_mode: PromptMode::Arg,
529 prompt_flag: None, output_format: OutputFormat::Text,
531 env_vars: vec![],
532 }
533 }
534
535 pub fn opencode_interactive() -> Self {
545 Self {
546 command: "opencode".to_string(),
547 args: vec![],
548 prompt_mode: PromptMode::Arg,
549 prompt_flag: Some("--prompt".to_string()),
550 output_format: OutputFormat::Text,
551 env_vars: vec![],
552 }
553 }
554
555 pub fn pi() -> Self {
560 Self {
561 command: "pi".to_string(),
562 args: vec![
563 "-p".to_string(),
564 "--mode".to_string(),
565 "json".to_string(),
566 "--no-session".to_string(),
567 ],
568 prompt_mode: PromptMode::Arg,
569 prompt_flag: None, output_format: OutputFormat::PiStreamJson,
571 env_vars: vec![],
572 }
573 }
574
575 pub fn pi_interactive() -> Self {
580 Self {
581 command: "pi".to_string(),
582 args: vec!["--no-session".to_string()],
583 prompt_mode: PromptMode::Arg,
584 prompt_flag: None, output_format: OutputFormat::Text,
586 env_vars: vec![],
587 }
588 }
589
590 pub fn roo() -> Self {
597 Self {
598 command: "roo".to_string(),
599 args: vec!["--print".to_string(), "--ephemeral".to_string()],
600 prompt_mode: PromptMode::Arg,
601 prompt_flag: None,
602 output_format: OutputFormat::Text,
603 env_vars: vec![],
604 }
605 }
606
607 pub fn roo_interactive() -> Self {
612 Self {
613 command: "roo".to_string(),
614 args: vec![],
615 prompt_mode: PromptMode::Arg,
616 prompt_flag: None,
617 output_format: OutputFormat::Text,
618 env_vars: vec![],
619 }
620 }
621
622 pub fn custom(config: &CliConfig) -> Result<Self, CustomBackendError> {
627 let command = config.command.clone().ok_or(CustomBackendError)?;
628 let prompt_mode = if config.prompt_mode == "stdin" {
629 PromptMode::Stdin
630 } else {
631 PromptMode::Arg
632 };
633
634 Ok(Self {
635 command,
636 args: config.args.clone(),
637 prompt_mode,
638 prompt_flag: config.prompt_flag.clone(),
639 output_format: OutputFormat::Text,
640 env_vars: vec![],
641 })
642 }
643
644 fn build_roo_prompt_file(
648 args: &mut Vec<String>,
649 prompt: &str,
650 ) -> (Option<String>, Option<NamedTempFile>) {
651 match NamedTempFile::new() {
652 Ok(mut file) => {
653 if let Err(e) = file.write_all(prompt.as_bytes()) {
654 tracing::warn!("Failed to write roo prompt to temp file: {}", e);
655 args.push(prompt.to_string());
656 (None, None)
657 } else {
658 args.push("--prompt-file".to_string());
659 args.push(file.path().display().to_string());
660 (None, Some(file))
661 }
662 }
663 Err(e) => {
664 tracing::warn!("Failed to create temp file for roo: {}", e);
665 args.push(prompt.to_string());
666 (None, None)
667 }
668 }
669 }
670
671 pub fn build_command_pty(
678 &self,
679 prompt: &str,
680 ) -> (String, Vec<String>, Option<String>, Option<NamedTempFile>) {
681 if self.prompt_mode == PromptMode::Stdin {
682 let mut pty_backend = self.clone();
684 pty_backend.prompt_mode = PromptMode::Arg;
685 if pty_backend.prompt_flag.is_none() {
687 pty_backend.prompt_flag = Some("-p".to_string());
688 }
689 pty_backend.build_command(prompt, false)
690 } else {
691 self.build_command(prompt, false)
692 }
693 }
694
695 pub fn build_command(
701 &self,
702 prompt: &str,
703 interactive: bool,
704 ) -> (String, Vec<String>, Option<String>, Option<NamedTempFile>) {
705 let mut args = self.args.clone();
706
707 if interactive {
709 args = self.filter_args_for_interactive(args);
710 }
711
712 let (stdin_input, temp_file) = match self.prompt_mode {
714 PromptMode::Arg => {
715 if self.command == "roo" && args.contains(&"--print".to_string()) {
718 Self::build_roo_prompt_file(&mut args, prompt)
719 } else {
720 let (prompt_text, temp_file) = if prompt.len() > 7000 {
722 match NamedTempFile::new() {
723 Ok(mut file) => {
724 if let Err(e) = file.write_all(prompt.as_bytes()) {
725 tracing::warn!("Failed to write prompt to temp file: {}", e);
726 (prompt.to_string(), None)
727 } else {
728 let path = file.path().display().to_string();
729 (
730 format!("Please read and execute the task in {}", path),
731 Some(file),
732 )
733 }
734 }
735 Err(e) => {
736 tracing::warn!("Failed to create temp file: {}", e);
737 (prompt.to_string(), None)
738 }
739 }
740 } else {
741 (prompt.to_string(), None)
742 };
743
744 if let Some(ref flag) = self.prompt_flag {
745 args.push(flag.clone());
746 }
747 args.push(prompt_text);
748 (None, temp_file)
749 }
750 }
751 PromptMode::Stdin => (Some(prompt.to_string()), None),
752 };
753
754 tracing::debug!(
756 command = %self.command,
757 args_count = args.len(),
758 prompt_len = prompt.len(),
759 interactive = interactive,
760 uses_stdin = stdin_input.is_some(),
761 uses_temp_file = temp_file.is_some(),
762 "Built CLI command"
763 );
764 tracing::trace!(prompt = %prompt, "Full prompt content");
766
767 (self.command.clone(), args, stdin_input, temp_file)
768 }
769
770 fn filter_args_for_interactive(&self, args: Vec<String>) -> Vec<String> {
772 match self.command.as_str() {
773 "kiro-cli" => args
774 .into_iter()
775 .filter(|a| a != "--no-interactive")
776 .collect(),
777 "codex" => args.into_iter().filter(|a| a != "--full-auto").collect(),
778 "amp" => args
779 .into_iter()
780 .filter(|a| a != "--dangerously-allow-all")
781 .collect(),
782 "copilot" => args
783 .into_iter()
784 .filter(|a| a != "--allow-all-tools")
785 .collect(),
786 "claude" => args.into_iter().filter(|a| a != "--print").collect(),
787 "roo" => args
788 .into_iter()
789 .filter(|a| a != "--print" && a != "--ephemeral")
790 .collect(),
791 _ => args, }
793 }
794
795 fn reconcile_codex_args(args: &mut Vec<String>) {
796 let had_dangerous_bypass = args
797 .iter()
798 .any(|arg| arg == "--dangerously-bypass-approvals-and-sandbox");
799 if had_dangerous_bypass {
800 args.retain(|arg| arg != "--dangerously-bypass-approvals-and-sandbox");
801 if !args.iter().any(|arg| arg == "--yolo") {
802 if let Some(pos) = args.iter().position(|arg| arg == "exec") {
803 args.insert(pos + 1, "--yolo".to_string());
804 } else {
805 args.push("--yolo".to_string());
806 }
807 }
808 }
809
810 if args.iter().any(|arg| arg == "--yolo") {
811 args.retain(|arg| arg != "--full-auto");
812 let mut seen_yolo = false;
814 args.retain(|arg| {
815 if arg == "--yolo" {
816 if seen_yolo {
817 return false;
818 }
819 seen_yolo = true;
820 }
821 true
822 });
823 if !seen_yolo {
824 if let Some(pos) = args.iter().position(|arg| arg == "exec") {
825 args.insert(pos + 1, "--yolo".to_string());
826 } else {
827 args.push("--yolo".to_string());
828 }
829 }
830 }
831 }
832}
833
834#[cfg(test)]
835mod tests {
836 use super::*;
837
838 #[test]
839 fn test_claude_backend() {
840 let backend = CliBackend::claude();
841 let (cmd, args, stdin, temp) = backend.build_command("test prompt", false);
842
843 assert_eq!(cmd, "claude");
844 assert_eq!(
845 args,
846 vec![
847 "--dangerously-skip-permissions",
848 "--verbose",
849 "--output-format",
850 "stream-json",
851 "--setting-sources",
852 "project,local",
853 "--print",
854 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
855 ]
856 );
857 assert_eq!(stdin, Some("test prompt".to_string()));
858 assert!(temp.is_none());
859 assert_eq!(backend.output_format, OutputFormat::StreamJson);
860 }
861
862 #[test]
863 fn test_claude_interactive_backend() {
864 let backend = CliBackend::claude_interactive();
865 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
866
867 assert_eq!(cmd, "claude");
868 assert_eq!(
872 args,
873 vec![
874 "--dangerously-skip-permissions",
875 "--setting-sources",
876 "project,local",
877 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
878 "test prompt"
879 ]
880 );
881 assert!(stdin.is_none()); assert_eq!(backend.output_format, OutputFormat::Text);
883 assert_eq!(backend.prompt_flag, None);
884 }
885
886 #[test]
887 fn test_claude_large_prompt_uses_stdin_not_temp_file() {
888 let backend = CliBackend::claude();
889 let large_prompt = "x".repeat(7001);
890 let (cmd, args, stdin, temp) = backend.build_command(&large_prompt, false);
891
892 assert_eq!(cmd, "claude");
893 assert!(args.contains(&"--print".to_string()));
894 assert_eq!(stdin, Some(large_prompt));
895 assert!(temp.is_none());
896 }
897
898 #[test]
901 fn test_claude_build_command_pty_uses_arg_mode() {
902 let backend = CliBackend::claude();
903 let large_prompt = "x".repeat(7001);
904 let (cmd, args, stdin, temp) = backend.build_command_pty(&large_prompt);
905
906 assert_eq!(cmd, "claude");
907 assert!(args.contains(&"--print".to_string()));
909 assert!(stdin.is_none(), "PTY mode should not use stdin");
911 assert!(
913 temp.is_some(),
914 "Large prompt in PTY mode should use temp file"
915 );
916 assert!(args.iter().any(|a| a.contains("Please read and execute")));
917 }
918
919 #[test]
920 fn test_claude_build_command_pty_small_prompt_uses_arg_directly() {
921 let backend = CliBackend::claude();
922 let (cmd, args, stdin, temp) = backend.build_command_pty("small prompt");
923
924 assert_eq!(cmd, "claude");
925 assert!(args.contains(&"--print".to_string()));
926 assert!(stdin.is_none());
927 assert!(temp.is_none());
928 assert!(args.contains(&"-p".to_string()));
930 assert!(args.contains(&"small prompt".to_string()));
931 }
932
933 #[test]
934 fn test_non_claude_large_prompt() {
935 let backend = CliBackend::kiro();
936 let large_prompt = "x".repeat(7001);
937 let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
938
939 assert_eq!(cmd, "kiro-cli");
940 assert!(temp.is_some());
941 assert!(args.iter().any(|a| a.contains("Please read and execute")));
942 }
943
944 #[test]
945 fn test_kiro_backend() {
946 let backend = CliBackend::kiro();
947 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
948
949 assert_eq!(cmd, "kiro-cli");
950 assert_eq!(
951 args,
952 vec![
953 "chat",
954 "--no-interactive",
955 "--trust-all-tools",
956 "test prompt"
957 ]
958 );
959 assert!(stdin.is_none());
960 }
961
962 #[test]
963 fn test_gemini_backend() {
964 let backend = CliBackend::gemini();
965 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
966
967 assert_eq!(cmd, "gemini");
968 assert_eq!(args, vec!["--yolo", "-p", "test prompt"]);
969 assert!(stdin.is_none());
970 }
971
972 #[test]
973 fn test_codex_backend() {
974 let backend = CliBackend::codex();
975 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
976
977 assert_eq!(cmd, "codex");
978 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
979 assert!(stdin.is_none());
980 }
981
982 #[test]
983 fn test_codex_large_prompt_uses_temp_file() {
984 let backend = CliBackend::codex();
985 let large_prompt = "x".repeat(7001);
986 let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
987
988 assert_eq!(cmd, "codex");
989 assert!(temp.is_some());
990 assert!(args.iter().any(|a| a.contains("Please read and execute")));
991 }
992
993 #[test]
994 fn test_amp_backend() {
995 let backend = CliBackend::amp();
996 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
997
998 assert_eq!(cmd, "amp");
999 assert_eq!(args, vec!["--dangerously-allow-all", "-x", "test prompt"]);
1000 assert!(stdin.is_none());
1001 }
1002
1003 #[test]
1004 fn test_copilot_backend() {
1005 let backend = CliBackend::copilot();
1006 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1007
1008 assert_eq!(cmd, "copilot");
1009 assert_eq!(
1010 args,
1011 vec![
1012 "--allow-all-tools",
1013 "--output-format",
1014 "json",
1015 "-p",
1016 "test prompt"
1017 ]
1018 );
1019 assert!(stdin.is_none());
1020 assert_eq!(backend.output_format, OutputFormat::CopilotStreamJson);
1021 }
1022
1023 #[test]
1024 fn test_copilot_tui_backend() {
1025 let backend = CliBackend::copilot_tui();
1026 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1027
1028 assert_eq!(cmd, "copilot");
1029 assert_eq!(args, vec!["test prompt"]);
1031 assert!(stdin.is_none());
1032 assert_eq!(backend.output_format, OutputFormat::Text);
1033 assert_eq!(backend.prompt_flag, None);
1034 }
1035
1036 #[test]
1037 fn test_from_config() {
1038 let config = CliConfig {
1039 backend: "claude".to_string(),
1040 command: None,
1041 prompt_mode: "arg".to_string(),
1042 ..Default::default()
1043 };
1044 let backend = CliBackend::from_config(&config).unwrap();
1045
1046 assert_eq!(backend.command, "claude");
1047 assert_eq!(backend.prompt_mode, PromptMode::Stdin);
1048 assert_eq!(backend.prompt_flag, None);
1049 assert!(backend.args.contains(&"--print".to_string()));
1050 }
1051
1052 #[test]
1053 fn test_from_config_command_override() {
1054 let config = CliConfig {
1055 backend: "claude".to_string(),
1056 command: Some("my-custom-claude".to_string()),
1057 prompt_mode: "arg".to_string(),
1058 ..Default::default()
1059 };
1060 let backend = CliBackend::from_config(&config).unwrap();
1061
1062 assert_eq!(backend.command, "my-custom-claude");
1063 assert_eq!(backend.prompt_flag, None);
1064 assert_eq!(backend.prompt_mode, PromptMode::Stdin);
1065 assert!(backend.args.contains(&"--print".to_string()));
1066 assert_eq!(backend.output_format, OutputFormat::StreamJson);
1067 }
1068
1069 #[test]
1070 fn test_kiro_interactive_mode_omits_no_interactive_flag() {
1071 let backend = CliBackend::kiro();
1072 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1073
1074 assert_eq!(cmd, "kiro-cli");
1075 assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
1076 assert!(stdin.is_none());
1077 assert!(!args.contains(&"--no-interactive".to_string()));
1078 }
1079
1080 #[test]
1081 fn test_codex_interactive_mode_omits_full_auto() {
1082 let backend = CliBackend::codex();
1083 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1084
1085 assert_eq!(cmd, "codex");
1086 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
1087 assert!(stdin.is_none());
1088 assert!(!args.contains(&"--full-auto".to_string()));
1089 }
1090
1091 #[test]
1092 fn test_amp_interactive_mode_no_flags() {
1093 let backend = CliBackend::amp();
1094 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1095
1096 assert_eq!(cmd, "amp");
1097 assert_eq!(args, vec!["-x", "test prompt"]);
1098 assert!(stdin.is_none());
1099 assert!(!args.contains(&"--dangerously-allow-all".to_string()));
1100 }
1101
1102 #[test]
1103 fn test_copilot_interactive_mode_omits_allow_all_tools() {
1104 let backend = CliBackend::copilot();
1105 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1106
1107 assert_eq!(cmd, "copilot");
1108 assert_eq!(args, vec!["--output-format", "json", "-p", "test prompt"]);
1109 assert!(stdin.is_none());
1110 assert!(!args.contains(&"--allow-all-tools".to_string()));
1111 }
1112
1113 #[test]
1114 fn test_claude_interactive_mode_omits_print() {
1115 let backend = CliBackend::claude();
1116 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
1117 let (_, args_interactive, stdin_interactive, _) =
1118 backend.build_command("test prompt", true);
1119
1120 assert_eq!(cmd, "claude");
1121 assert!(args_auto.contains(&"--print".to_string()));
1122 assert!(!args_interactive.contains(&"--print".to_string()));
1123 assert_eq!(
1124 args_interactive,
1125 vec![
1126 "--dangerously-skip-permissions",
1127 "--verbose",
1128 "--output-format",
1129 "stream-json",
1130 "--setting-sources",
1131 "project,local",
1132 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
1133 ]
1134 );
1135 assert_eq!(stdin_auto, Some("test prompt".to_string()));
1136 assert_eq!(stdin_interactive, Some("test prompt".to_string()));
1137 }
1138
1139 #[test]
1140 fn test_gemini_interactive_mode_unchanged() {
1141 let backend = CliBackend::gemini();
1142 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
1143 let (_, args_interactive, stdin_interactive, _) =
1144 backend.build_command("test prompt", true);
1145
1146 assert_eq!(cmd, "gemini");
1147 assert_eq!(args_auto, args_interactive);
1148 assert_eq!(args_auto, vec!["--yolo", "-p", "test prompt"]);
1149 assert_eq!(stdin_auto, stdin_interactive);
1150 assert!(stdin_auto.is_none());
1151 }
1152
1153 #[test]
1154 fn test_custom_backend_with_prompt_flag_short() {
1155 let config = CliConfig {
1156 backend: "custom".to_string(),
1157 command: Some("my-agent".to_string()),
1158 prompt_mode: "arg".to_string(),
1159 prompt_flag: Some("-p".to_string()),
1160 ..Default::default()
1161 };
1162 let backend = CliBackend::from_config(&config).unwrap();
1163 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1164
1165 assert_eq!(cmd, "my-agent");
1166 assert_eq!(args, vec!["-p", "test prompt"]);
1167 assert!(stdin.is_none());
1168 }
1169
1170 #[test]
1171 fn test_custom_backend_with_prompt_flag_long() {
1172 let config = CliConfig {
1173 backend: "custom".to_string(),
1174 command: Some("my-agent".to_string()),
1175 prompt_mode: "arg".to_string(),
1176 prompt_flag: Some("--prompt".to_string()),
1177 ..Default::default()
1178 };
1179 let backend = CliBackend::from_config(&config).unwrap();
1180 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1181
1182 assert_eq!(cmd, "my-agent");
1183 assert_eq!(args, vec!["--prompt", "test prompt"]);
1184 assert!(stdin.is_none());
1185 }
1186
1187 #[test]
1188 fn test_custom_backend_without_prompt_flag_positional() {
1189 let config = CliConfig {
1190 backend: "custom".to_string(),
1191 command: Some("my-agent".to_string()),
1192 prompt_mode: "arg".to_string(),
1193 prompt_flag: None,
1194 ..Default::default()
1195 };
1196 let backend = CliBackend::from_config(&config).unwrap();
1197 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1198
1199 assert_eq!(cmd, "my-agent");
1200 assert_eq!(args, vec!["test prompt"]);
1201 assert!(stdin.is_none());
1202 }
1203
1204 #[test]
1205 fn test_custom_backend_without_command_returns_error() {
1206 let config = CliConfig {
1207 backend: "custom".to_string(),
1208 command: None,
1209 prompt_mode: "arg".to_string(),
1210 ..Default::default()
1211 };
1212 let result = CliBackend::from_config(&config);
1213
1214 assert!(result.is_err());
1215 let err = result.unwrap_err();
1216 assert_eq!(
1217 err.to_string(),
1218 "custom backend requires a command to be specified"
1219 );
1220 }
1221
1222 #[test]
1223 fn test_kiro_with_agent() {
1224 let backend = CliBackend::kiro_with_agent("my-agent".to_string(), &[]);
1225 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1226
1227 assert_eq!(cmd, "kiro-cli");
1228 assert_eq!(
1229 args,
1230 vec![
1231 "chat",
1232 "--no-interactive",
1233 "--trust-all-tools",
1234 "--agent",
1235 "my-agent",
1236 "test prompt"
1237 ]
1238 );
1239 assert!(stdin.is_none());
1240 }
1241
1242 #[test]
1243 fn test_kiro_with_agent_extra_args() {
1244 let extra_args = vec!["--verbose".to_string(), "--debug".to_string()];
1245 let backend = CliBackend::kiro_with_agent("my-agent".to_string(), &extra_args);
1246 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1247
1248 assert_eq!(cmd, "kiro-cli");
1249 assert_eq!(
1250 args,
1251 vec![
1252 "chat",
1253 "--no-interactive",
1254 "--trust-all-tools",
1255 "--agent",
1256 "my-agent",
1257 "--verbose",
1258 "--debug",
1259 "test prompt"
1260 ]
1261 );
1262 assert!(stdin.is_none());
1263 }
1264
1265 #[test]
1266 fn test_from_name_claude() {
1267 let backend = CliBackend::from_name("claude").unwrap();
1268 assert_eq!(backend.command, "claude");
1269 assert_eq!(backend.prompt_mode, PromptMode::Stdin);
1270 assert_eq!(backend.prompt_flag, None);
1271 assert!(backend.args.contains(&"--print".to_string()));
1272 }
1273
1274 #[test]
1275 fn test_from_name_kiro() {
1276 let backend = CliBackend::from_name("kiro").unwrap();
1277 assert_eq!(backend.command, "kiro-cli");
1278 }
1279
1280 #[test]
1281 fn test_from_name_gemini() {
1282 let backend = CliBackend::from_name("gemini").unwrap();
1283 assert_eq!(backend.command, "gemini");
1284 }
1285
1286 #[test]
1287 fn test_from_name_codex() {
1288 let backend = CliBackend::from_name("codex").unwrap();
1289 assert_eq!(backend.command, "codex");
1290 }
1291
1292 #[test]
1293 fn test_from_name_amp() {
1294 let backend = CliBackend::from_name("amp").unwrap();
1295 assert_eq!(backend.command, "amp");
1296 }
1297
1298 #[test]
1299 fn test_from_name_copilot() {
1300 let backend = CliBackend::from_name("copilot").unwrap();
1301 assert_eq!(backend.command, "copilot");
1302 assert_eq!(backend.prompt_flag, Some("-p".to_string()));
1303 }
1304
1305 #[test]
1306 fn test_from_name_invalid() {
1307 let result = CliBackend::from_name("invalid");
1308 assert!(result.is_err());
1309 }
1310
1311 #[test]
1312 fn test_from_hat_backend_named() {
1313 let hat_backend = HatBackend::Named("claude".to_string());
1314 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1315 assert_eq!(backend.command, "claude");
1316 }
1317
1318 #[test]
1319 fn test_from_hat_backend_kiro_agent() {
1320 let hat_backend = HatBackend::KiroAgent {
1321 backend_type: "kiro".to_string(),
1322 agent: "my-agent".to_string(),
1323 args: vec![],
1324 };
1325 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1326 let (cmd, args, _, _) = backend.build_command("test", false);
1327 assert_eq!(cmd, "kiro-cli");
1328 assert!(args.contains(&"--agent".to_string()));
1329 assert!(args.contains(&"my-agent".to_string()));
1330 }
1331
1332 #[test]
1333 fn test_from_hat_backend_kiro_acp_agent_uses_acp_executor() {
1334 let hat_backend = HatBackend::KiroAgent {
1335 backend_type: "kiro-acp".to_string(),
1336 agent: "my-agent".to_string(),
1337 args: vec![],
1338 };
1339 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1340 assert_eq!(backend.command, "kiro-cli");
1341 assert_eq!(backend.output_format, OutputFormat::Acp);
1342 assert!(backend.args.contains(&"acp".to_string()));
1343 assert!(backend.args.contains(&"--agent".to_string()));
1344 assert!(backend.args.contains(&"my-agent".to_string()));
1345 }
1346
1347 #[test]
1348 fn test_from_hat_backend_kiro_agent_with_args() {
1349 let hat_backend = HatBackend::KiroAgent {
1350 backend_type: "kiro".to_string(),
1351 agent: "my-agent".to_string(),
1352 args: vec!["--verbose".to_string()],
1353 };
1354 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1355 let (cmd, args, _, _) = backend.build_command("test", false);
1356 assert_eq!(cmd, "kiro-cli");
1357 assert!(args.contains(&"--agent".to_string()));
1358 assert!(args.contains(&"my-agent".to_string()));
1359 assert!(args.contains(&"--verbose".to_string()));
1360 }
1361
1362 #[test]
1363 fn test_from_hat_backend_named_with_args() {
1364 let hat_backend = HatBackend::NamedWithArgs {
1365 backend_type: "claude".to_string(),
1366 args: vec!["--model".to_string(), "claude-sonnet-4".to_string()],
1367 };
1368 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1369 assert_eq!(backend.command, "claude");
1370 assert!(backend.args.contains(&"--model".to_string()));
1371 assert!(backend.args.contains(&"claude-sonnet-4".to_string()));
1372 }
1373
1374 #[test]
1375 fn test_codex_named_with_args_dangerous_bypass_normalizes_to_yolo() {
1376 let hat_backend = HatBackend::NamedWithArgs {
1377 backend_type: "codex".to_string(),
1378 args: vec!["--dangerously-bypass-approvals-and-sandbox".to_string()],
1379 };
1380 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1381 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1382
1383 assert_eq!(cmd, "codex");
1384 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
1385 }
1386
1387 #[test]
1388 fn test_codex_named_with_args_yolo_removes_full_auto() {
1389 let hat_backend = HatBackend::NamedWithArgs {
1390 backend_type: "codex".to_string(),
1391 args: vec!["--yolo".to_string()],
1392 };
1393 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1394 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1395
1396 assert_eq!(cmd, "codex");
1397 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
1398 }
1399
1400 #[test]
1401 fn test_from_hat_backend_custom() {
1402 let hat_backend = HatBackend::Custom {
1403 command: "my-cli".to_string(),
1404 args: vec!["--flag".to_string()],
1405 };
1406 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1407 assert_eq!(backend.command, "my-cli");
1408 assert_eq!(backend.args, vec!["--flag"]);
1409 }
1410
1411 #[test]
1416 fn test_for_interactive_prompt_claude() {
1417 let backend = CliBackend::for_interactive_prompt("claude").unwrap();
1418 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1419
1420 assert_eq!(cmd, "claude");
1421 assert_eq!(
1423 args,
1424 vec![
1425 "--dangerously-skip-permissions",
1426 "--setting-sources",
1427 "project,local",
1428 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
1429 "test prompt"
1430 ]
1431 );
1432 assert!(stdin.is_none());
1433 assert_eq!(backend.prompt_flag, None);
1434 }
1435
1436 #[test]
1437 fn test_for_interactive_prompt_kiro() {
1438 let backend = CliBackend::for_interactive_prompt("kiro").unwrap();
1439 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1440
1441 assert_eq!(cmd, "kiro-cli");
1442 assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
1444 assert!(!args.contains(&"--no-interactive".to_string()));
1445 assert!(stdin.is_none());
1446 }
1447
1448 #[test]
1452 fn test_for_interactive_prompt_kiro_acp_falls_back_to_kiro_chat() {
1453 let backend = CliBackend::for_interactive_prompt("kiro-acp").unwrap();
1454 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1455
1456 assert_eq!(cmd, "kiro-cli");
1457 assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
1460 assert!(!args.contains(&"acp".to_string()));
1461 assert!(!args.contains(&"--no-interactive".to_string()));
1462 assert!(stdin.is_none());
1463 assert_eq!(backend.output_format, OutputFormat::Text);
1464 }
1465
1466 #[test]
1467 fn test_for_interactive_prompt_gemini() {
1468 let backend = CliBackend::for_interactive_prompt("gemini").unwrap();
1469 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1470
1471 assert_eq!(cmd, "gemini");
1472 assert_eq!(args, vec!["--yolo", "-i", "test prompt"]);
1474 assert_eq!(backend.prompt_flag, Some("-i".to_string()));
1475 assert!(stdin.is_none());
1476 }
1477
1478 #[test]
1479 fn test_for_interactive_prompt_codex() {
1480 let backend = CliBackend::for_interactive_prompt("codex").unwrap();
1481 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1482
1483 assert_eq!(cmd, "codex");
1484 assert_eq!(args, vec!["test prompt"]);
1486 assert!(!args.contains(&"exec".to_string()));
1487 assert!(!args.contains(&"--full-auto".to_string()));
1488 assert!(stdin.is_none());
1489 }
1490
1491 #[test]
1492 fn test_for_interactive_prompt_amp() {
1493 let backend = CliBackend::for_interactive_prompt("amp").unwrap();
1494 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1495
1496 assert_eq!(cmd, "amp");
1497 assert_eq!(args, vec!["-x", "test prompt"]);
1499 assert!(!args.contains(&"--dangerously-allow-all".to_string()));
1500 assert!(stdin.is_none());
1501 }
1502
1503 #[test]
1504 fn test_for_interactive_prompt_copilot() {
1505 let backend = CliBackend::for_interactive_prompt("copilot").unwrap();
1506 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1507
1508 assert_eq!(cmd, "copilot");
1509 assert_eq!(args, vec!["-p", "test prompt"]);
1511 assert!(!args.contains(&"--allow-all-tools".to_string()));
1512 assert!(stdin.is_none());
1513 }
1514
1515 #[test]
1516 fn test_for_interactive_prompt_invalid() {
1517 let result = CliBackend::for_interactive_prompt("invalid_backend");
1518 assert!(result.is_err());
1519 }
1520
1521 #[test]
1526 fn test_opencode_backend() {
1527 let backend = CliBackend::opencode();
1528 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1529
1530 assert_eq!(cmd, "opencode");
1531 assert_eq!(args, vec!["run", "test prompt"]);
1533 assert!(stdin.is_none());
1534 assert_eq!(backend.output_format, OutputFormat::Text);
1535 assert_eq!(backend.prompt_flag, None);
1536 }
1537
1538 #[test]
1539 fn test_opencode_tui_backend() {
1540 let backend = CliBackend::opencode_tui();
1541 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1542
1543 assert_eq!(cmd, "opencode");
1544 assert_eq!(args, vec!["run", "test prompt"]);
1546 assert!(stdin.is_none());
1547 assert_eq!(backend.output_format, OutputFormat::Text);
1548 assert_eq!(backend.prompt_flag, None);
1549 }
1550
1551 #[test]
1552 fn test_opencode_interactive_mode_unchanged() {
1553 let backend = CliBackend::opencode();
1555 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
1556 let (_, args_interactive, stdin_interactive, _) =
1557 backend.build_command("test prompt", true);
1558
1559 assert_eq!(cmd, "opencode");
1560 assert_eq!(args_auto, args_interactive);
1562 assert_eq!(args_auto, vec!["run", "test prompt"]);
1563 assert!(stdin_auto.is_none());
1564 assert!(stdin_interactive.is_none());
1565 }
1566
1567 #[test]
1568 fn test_from_name_opencode() {
1569 let backend = CliBackend::from_name("opencode").unwrap();
1570 assert_eq!(backend.command, "opencode");
1571 assert_eq!(backend.prompt_flag, None); }
1573
1574 #[test]
1575 fn test_for_interactive_prompt_opencode() {
1576 let backend = CliBackend::for_interactive_prompt("opencode").unwrap();
1577 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1578
1579 assert_eq!(cmd, "opencode");
1580 assert_eq!(args, vec!["--prompt", "test prompt"]);
1582 assert!(stdin.is_none());
1583 assert_eq!(backend.prompt_flag, Some("--prompt".to_string()));
1584 }
1585
1586 #[test]
1587 fn test_opencode_interactive_launches_tui_not_headless() {
1588 let backend = CliBackend::opencode_interactive();
1598 let (cmd, args, _, _) = backend.build_command("test prompt", true);
1599
1600 assert_eq!(cmd, "opencode");
1601 assert!(
1604 !args.contains(&"run".to_string()),
1605 "opencode_interactive() should not use 'run' subcommand. \
1606 'opencode run' is headless mode, but interactive mode needs TUI. \
1607 Expected: opencode --prompt \"test prompt\", got: opencode {}",
1608 args.join(" ")
1609 );
1610 assert!(
1612 args.contains(&"--prompt".to_string()),
1613 "opencode_interactive() should use --prompt flag for TUI mode. \
1614 Expected args to contain '--prompt', got: {:?}",
1615 args
1616 );
1617 }
1618
1619 #[test]
1624 fn test_pi_backend() {
1625 let backend = CliBackend::pi();
1626 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1627
1628 assert_eq!(cmd, "pi");
1629 assert_eq!(
1630 args,
1631 vec!["-p", "--mode", "json", "--no-session", "test prompt"]
1632 );
1633 assert!(stdin.is_none());
1634 assert_eq!(backend.output_format, OutputFormat::PiStreamJson);
1635 assert_eq!(backend.prompt_flag, None); }
1637
1638 #[test]
1639 fn test_pi_interactive_backend() {
1640 let backend = CliBackend::pi_interactive();
1641 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1642
1643 assert_eq!(cmd, "pi");
1644 assert_eq!(args, vec!["--no-session", "test prompt"]);
1646 assert!(stdin.is_none());
1647 assert_eq!(backend.output_format, OutputFormat::Text);
1648 assert_eq!(backend.prompt_flag, None);
1649 }
1650
1651 #[test]
1652 fn test_from_name_pi() {
1653 let backend = CliBackend::from_name("pi").unwrap();
1654 assert_eq!(backend.command, "pi");
1655 assert_eq!(backend.prompt_flag, None);
1656 assert_eq!(backend.output_format, OutputFormat::PiStreamJson);
1657 }
1658
1659 #[test]
1660 fn test_for_interactive_prompt_pi() {
1661 let backend = CliBackend::for_interactive_prompt("pi").unwrap();
1662 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1663
1664 assert_eq!(cmd, "pi");
1665 assert_eq!(args, vec!["--no-session", "test prompt"]);
1666 assert!(stdin.is_none());
1667 assert_eq!(backend.output_format, OutputFormat::Text);
1668 }
1669
1670 #[test]
1671 fn test_from_config_pi() {
1672 let config = CliConfig {
1673 backend: "pi".to_string(),
1674 command: None,
1675 prompt_mode: "arg".to_string(),
1676 args: vec![
1677 "--provider".to_string(),
1678 "zai".to_string(),
1679 "--model".to_string(),
1680 "glm-5".to_string(),
1681 ],
1682 ..Default::default()
1683 };
1684 let backend = CliBackend::from_config(&config).unwrap();
1685 let (_cmd, args, _stdin, _temp) = backend.build_command("test prompt", false);
1686
1687 assert_eq!(backend.command, "pi");
1688 assert_eq!(backend.output_format, OutputFormat::PiStreamJson);
1689 assert!(args.contains(&"--provider".to_string()));
1690 assert!(args.contains(&"zai".to_string()));
1691 assert!(args.contains(&"--model".to_string()));
1692 assert!(args.contains(&"glm-5".to_string()));
1693 }
1694
1695 #[test]
1696 fn test_from_hat_backend_named_with_args_pi() {
1697 let hat_backend = HatBackend::NamedWithArgs {
1698 backend_type: "pi".to_string(),
1699 args: vec![
1700 "--provider".to_string(),
1701 "anthropic".to_string(),
1702 "--model".to_string(),
1703 "claude-sonnet-4".to_string(),
1704 ],
1705 };
1706 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1707 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1708
1709 assert_eq!(cmd, "pi");
1710 assert!(args.contains(&"-p".to_string()));
1712 assert!(args.contains(&"--mode".to_string()));
1713 assert!(args.contains(&"json".to_string()));
1714 assert!(args.contains(&"--no-session".to_string()));
1715 assert!(args.contains(&"--provider".to_string()));
1716 assert!(args.contains(&"anthropic".to_string()));
1717 assert!(args.contains(&"--model".to_string()));
1718 assert!(args.contains(&"claude-sonnet-4".to_string()));
1719 assert!(args.contains(&"test prompt".to_string()));
1720 }
1721
1722 #[test]
1723 fn test_pi_large_prompt_uses_temp_file() {
1724 let backend = CliBackend::pi();
1725 let large_prompt = "x".repeat(7001);
1726 let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
1727
1728 assert_eq!(cmd, "pi");
1729 assert!(temp.is_some());
1730 assert!(args.iter().any(|a| a.contains("Please read and execute")));
1731 }
1732
1733 #[test]
1734 fn test_pi_interactive_mode_unchanged() {
1735 let backend = CliBackend::pi();
1737 let (_, args_auto, _, _) = backend.build_command("test prompt", false);
1738 let (_, args_interactive, _, _) = backend.build_command("test prompt", true);
1739
1740 assert_eq!(args_auto, args_interactive);
1741 }
1742
1743 #[test]
1744 fn test_custom_args_can_be_appended() {
1745 let mut backend = CliBackend::opencode();
1748
1749 let custom_args = vec!["--model=gpt-4".to_string(), "--temperature=0.7".to_string()];
1751 backend.args.extend(custom_args.clone());
1752
1753 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1755
1756 assert_eq!(cmd, "opencode");
1757 assert!(args.contains(&"run".to_string())); assert!(args.contains(&"--model=gpt-4".to_string())); assert!(args.contains(&"--temperature=0.7".to_string())); assert!(args.contains(&"test prompt".to_string())); let run_idx = args.iter().position(|a| a == "run").unwrap();
1765 let model_idx = args.iter().position(|a| a == "--model=gpt-4").unwrap();
1766 assert!(
1767 run_idx < model_idx,
1768 "Original args should come before custom args"
1769 );
1770 }
1771
1772 #[test]
1777 fn test_claude_interactive_teams_backend() {
1778 let backend = CliBackend::claude_interactive_teams();
1779 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1780
1781 assert_eq!(cmd, "claude");
1782 assert_eq!(
1783 args,
1784 vec![
1785 "--dangerously-skip-permissions",
1786 "--setting-sources",
1787 "project,local",
1788 "--disallowedTools=TodoWrite",
1789 "test prompt"
1790 ]
1791 );
1792 assert!(stdin.is_none());
1793 assert_eq!(backend.output_format, OutputFormat::Text);
1794 assert_eq!(backend.prompt_flag, None);
1795 assert_eq!(
1796 backend.env_vars,
1797 vec![(
1798 "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS".to_string(),
1799 "1".to_string()
1800 )]
1801 );
1802 }
1803
1804 #[test]
1805 fn test_env_vars_default_empty() {
1806 assert!(CliBackend::claude().env_vars.is_empty());
1808 assert!(CliBackend::claude_interactive().env_vars.is_empty());
1809 assert!(CliBackend::kiro().env_vars.is_empty());
1810 assert!(CliBackend::gemini().env_vars.is_empty());
1811 assert!(CliBackend::codex().env_vars.is_empty());
1812 assert!(CliBackend::amp().env_vars.is_empty());
1813 assert!(CliBackend::copilot().env_vars.is_empty());
1814 assert!(CliBackend::opencode().env_vars.is_empty());
1815 assert!(CliBackend::pi().env_vars.is_empty());
1816 assert!(CliBackend::roo().env_vars.is_empty());
1817 }
1818
1819 #[test]
1820 fn test_all_claude_constructors_isolate_user_settings() {
1821 let claude = CliBackend::claude();
1822 let claude_interactive = CliBackend::claude_interactive();
1823 let claude_interactive_teams = CliBackend::claude_interactive_teams();
1824 let interactive_prompt = CliBackend::for_interactive_prompt("claude").unwrap();
1825
1826 for backend in [
1827 &claude,
1828 &claude_interactive,
1829 &claude_interactive_teams,
1830 &interactive_prompt,
1831 ] {
1832 let mut setting_sources = backend
1833 .args
1834 .windows(2)
1835 .filter(|window| window[0] == "--setting-sources")
1836 .map(|window| window[1].as_str());
1837
1838 assert_eq!(setting_sources.next(), Some("project,local"));
1839 assert_eq!(setting_sources.next(), None);
1840 }
1841 }
1842
1843 #[test]
1848 fn test_roo_backend() {
1849 let backend = CliBackend::roo();
1850 let (cmd, args, stdin, temp) = backend.build_command("test prompt", false);
1851
1852 assert_eq!(cmd, "roo");
1853 assert!(
1855 temp.is_some(),
1856 "roo should always use temp file for prompts"
1857 );
1858 assert!(
1859 args.contains(&"--print".to_string()),
1860 "roo headless should have --print"
1861 );
1862 assert!(
1863 args.contains(&"--ephemeral".to_string()),
1864 "roo headless should have --ephemeral"
1865 );
1866 assert!(
1867 args.contains(&"--prompt-file".to_string()),
1868 "roo should use --prompt-file"
1869 );
1870 assert!(stdin.is_none());
1871 assert_eq!(backend.output_format, OutputFormat::Text);
1872 }
1873
1874 #[test]
1875 fn test_roo_interactive() {
1876 let backend = CliBackend::roo_interactive();
1877 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1878
1879 assert_eq!(cmd, "roo");
1880 assert_eq!(args, vec!["test prompt"]);
1882 assert!(stdin.is_none());
1883 assert_eq!(backend.output_format, OutputFormat::Text);
1884 assert_eq!(backend.prompt_flag, None);
1885 }
1886
1887 #[test]
1888 fn test_from_name_roo() {
1889 let backend = CliBackend::from_name("roo").unwrap();
1890 assert_eq!(backend.command, "roo");
1891 assert_eq!(backend.prompt_flag, None);
1892 assert_eq!(backend.output_format, OutputFormat::Text);
1893 }
1894
1895 #[test]
1896 fn test_from_config_roo() {
1897 let config = CliConfig {
1898 backend: "roo".to_string(),
1899 command: None,
1900 prompt_mode: "arg".to_string(),
1901 ..Default::default()
1902 };
1903 let backend = CliBackend::from_config(&config).unwrap();
1904
1905 assert_eq!(backend.command, "roo");
1906 assert_eq!(backend.output_format, OutputFormat::Text);
1907 assert!(backend.args.contains(&"--print".to_string()));
1908 assert!(backend.args.contains(&"--ephemeral".to_string()));
1909 }
1910
1911 #[test]
1912 fn test_from_config_roo_with_args() {
1913 let config = CliConfig {
1914 backend: "roo".to_string(),
1915 command: None,
1916 prompt_mode: "arg".to_string(),
1917 args: vec![
1918 "--provider".to_string(),
1919 "bedrock".to_string(),
1920 "--model".to_string(),
1921 "anthropic.claude-sonnet-4-6".to_string(),
1922 ],
1923 ..Default::default()
1924 };
1925 let backend = CliBackend::from_config(&config).unwrap();
1926 let (_cmd, args, _stdin, _temp) = backend.build_command("test prompt", false);
1927
1928 assert_eq!(backend.command, "roo");
1929 assert!(args.contains(&"--print".to_string()));
1931 assert!(args.contains(&"--ephemeral".to_string()));
1932 assert!(args.contains(&"--provider".to_string()));
1933 assert!(args.contains(&"bedrock".to_string()));
1934 assert!(args.contains(&"--model".to_string()));
1935 assert!(args.contains(&"anthropic.claude-sonnet-4-6".to_string()));
1936 assert!(args.contains(&"--prompt-file".to_string()));
1937 }
1938
1939 #[test]
1940 fn test_for_interactive_prompt_roo() {
1941 let backend = CliBackend::for_interactive_prompt("roo").unwrap();
1942 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1943
1944 assert_eq!(cmd, "roo");
1945 assert_eq!(args, vec!["test prompt"]);
1947 assert!(stdin.is_none());
1948 assert_eq!(backend.output_format, OutputFormat::Text);
1949 }
1950
1951 #[test]
1952 fn test_roo_interactive_mode_removes_print() {
1953 let backend = CliBackend::roo();
1954 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1955
1956 assert_eq!(cmd, "roo");
1957 assert!(
1959 !args.contains(&"--print".to_string()),
1960 "interactive mode should remove --print"
1961 );
1962 assert!(
1963 !args.contains(&"--ephemeral".to_string()),
1964 "interactive mode should remove --ephemeral"
1965 );
1966 assert!(stdin.is_none());
1967 }
1968
1969 #[test]
1970 fn test_roo_uses_prompt_file() {
1971 let backend = CliBackend::roo();
1972 let (_, args_small, _, temp_small) = backend.build_command("small prompt", false);
1974 assert!(
1975 temp_small.is_some(),
1976 "even small prompts should use temp file"
1977 );
1978 assert!(
1979 args_small.contains(&"--prompt-file".to_string()),
1980 "should use --prompt-file"
1981 );
1982
1983 let large_prompt = "x".repeat(10000);
1985 let (_, args_large, _, temp_large) = backend.build_command(&large_prompt, false);
1986 assert!(temp_large.is_some(), "large prompts should use temp file");
1987 assert!(
1988 args_large.contains(&"--prompt-file".to_string()),
1989 "should use --prompt-file for large prompts"
1990 );
1991 }
1992
1993 #[test]
1994 fn test_roo_prompt_file_content() {
1995 use std::io::{Read, Seek};
1996 let backend = CliBackend::roo();
1997 let prompt = "This is a test prompt for roo";
1998 let (_, _, _, temp) = backend.build_command(prompt, false);
1999
2000 let mut temp_file = temp.expect("should have temp file");
2001 let mut content = String::new();
2002 temp_file
2003 .as_file_mut()
2004 .seek(std::io::SeekFrom::Start(0))
2005 .unwrap();
2006 temp_file
2007 .as_file_mut()
2008 .read_to_string(&mut content)
2009 .unwrap();
2010 assert_eq!(content, prompt);
2011 }
2012}