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 "--print".to_string(),
119 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet".to_string(),
120 ],
121 prompt_mode: PromptMode::Stdin,
122 prompt_flag: None,
123 output_format: OutputFormat::StreamJson,
124 env_vars: vec![],
125 }
126 }
127
128 pub fn claude_interactive() -> Self {
138 Self {
139 command: "claude".to_string(),
140 args: vec![
141 "--dangerously-skip-permissions".to_string(),
142 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet".to_string(),
143 ],
144 prompt_mode: PromptMode::Arg,
145 prompt_flag: None,
146 output_format: OutputFormat::Text,
147 env_vars: vec![],
148 }
149 }
150
151 pub fn kiro() -> Self {
155 Self {
156 command: "kiro-cli".to_string(),
157 args: vec![
158 "chat".to_string(),
159 "--no-interactive".to_string(),
160 "--trust-all-tools".to_string(),
161 ],
162 prompt_mode: PromptMode::Arg,
163 prompt_flag: None,
164 output_format: OutputFormat::Text,
165 env_vars: vec![],
166 }
167 }
168
169 pub fn kiro_with_agent(agent: String, extra_args: &[String]) -> Self {
173 let mut backend = Self {
174 command: "kiro-cli".to_string(),
175 args: vec![
176 "chat".to_string(),
177 "--no-interactive".to_string(),
178 "--trust-all-tools".to_string(),
179 "--agent".to_string(),
180 agent,
181 ],
182 prompt_mode: PromptMode::Arg,
183 prompt_flag: None,
184 output_format: OutputFormat::Text,
185 env_vars: vec![],
186 };
187 backend.args.extend(extra_args.iter().cloned());
188 backend
189 }
190
191 pub fn kiro_acp() -> Self {
196 Self::kiro_acp_with_options(None, None)
197 }
198
199 pub fn kiro_acp_with_options(agent: Option<&str>, model: Option<&str>) -> Self {
201 let mut args = vec!["acp".to_string()];
202 if let Some(name) = agent {
203 args.push("--agent".to_string());
204 args.push(name.to_string());
205 }
206 if let Some(m) = model {
207 args.push("--model".to_string());
208 args.push(m.to_string());
209 }
210 Self {
211 command: "kiro-cli".to_string(),
212 args,
213 prompt_mode: PromptMode::Stdin,
214 prompt_flag: None,
215 output_format: OutputFormat::Acp,
216 env_vars: vec![],
217 }
218 }
219
220 pub fn from_name_with_args(
225 name: &str,
226 extra_args: &[String],
227 ) -> Result<Self, CustomBackendError> {
228 let mut backend = Self::from_name(name)?;
229 backend.args.extend(extra_args.iter().cloned());
230 if backend.command == "codex" {
231 Self::reconcile_codex_args(&mut backend.args);
232 }
233 Ok(backend)
234 }
235
236 pub fn from_name(name: &str) -> Result<Self, CustomBackendError> {
241 match name {
242 "claude" => Ok(Self::claude()),
243 "kiro" => Ok(Self::kiro()),
244 "kiro-acp" => Ok(Self::kiro_acp()),
245 "gemini" => Ok(Self::gemini()),
246 "codex" => Ok(Self::codex()),
247 "amp" => Ok(Self::amp()),
248 "copilot" => Ok(Self::copilot()),
249 "opencode" => Ok(Self::opencode()),
250 "pi" => Ok(Self::pi()),
251 "roo" => Ok(Self::roo()),
252 _ => Err(CustomBackendError),
253 }
254 }
255
256 pub fn from_hat_backend(hat_backend: &HatBackend) -> Result<Self, CustomBackendError> {
261 match hat_backend {
262 HatBackend::Named(name) => Self::from_name(name),
263 HatBackend::NamedWithArgs { backend_type, args } => {
264 Self::from_name_with_args(backend_type, args)
265 }
266 HatBackend::KiroAgent {
267 backend_type,
268 agent,
269 args,
270 } => {
271 if backend_type == "kiro-acp" {
272 Ok(Self::kiro_acp_with_options(Some(agent), None))
273 } else {
274 Ok(Self::kiro_with_agent(agent.clone(), args))
275 }
276 }
277 HatBackend::Custom { command, args } => Ok(Self {
278 command: command.clone(),
279 args: args.clone(),
280 prompt_mode: PromptMode::Arg,
281 prompt_flag: None,
282 output_format: OutputFormat::Text,
283 env_vars: vec![],
284 }),
285 }
286 }
287
288 pub fn gemini() -> Self {
290 Self {
291 command: "gemini".to_string(),
292 args: vec!["--yolo".to_string()],
293 prompt_mode: PromptMode::Arg,
294 prompt_flag: Some("-p".to_string()),
295 output_format: OutputFormat::Text,
296 env_vars: vec![],
297 }
298 }
299
300 pub fn codex() -> Self {
302 Self {
303 command: "codex".to_string(),
304 args: vec!["exec".to_string(), "--yolo".to_string()],
305 prompt_mode: PromptMode::Arg,
306 prompt_flag: None, output_format: OutputFormat::Text,
308 env_vars: vec![],
309 }
310 }
311
312 pub fn amp() -> Self {
314 Self {
315 command: "amp".to_string(),
316 args: vec!["--dangerously-allow-all".to_string()],
317 prompt_mode: PromptMode::Arg,
318 prompt_flag: Some("-x".to_string()),
319 output_format: OutputFormat::Text,
320 env_vars: vec![],
321 }
322 }
323
324 pub fn copilot() -> Self {
329 Self {
330 command: "copilot".to_string(),
331 args: vec![
332 "--allow-all-tools".to_string(),
333 "--output-format".to_string(),
334 "json".to_string(),
335 ],
336 prompt_mode: PromptMode::Arg,
337 prompt_flag: Some("-p".to_string()),
338 output_format: OutputFormat::CopilotStreamJson,
339 env_vars: vec![],
340 }
341 }
342
343 pub fn copilot_tui() -> Self {
349 Self {
350 command: "copilot".to_string(),
351 args: vec![], prompt_mode: PromptMode::Arg,
353 prompt_flag: None, output_format: OutputFormat::Text,
355 env_vars: vec![],
356 }
357 }
358
359 pub fn claude_interactive_teams() -> Self {
364 Self {
365 command: "claude".to_string(),
366 args: vec![
367 "--dangerously-skip-permissions".to_string(),
368 "--disallowedTools=TodoWrite".to_string(),
369 ],
370 prompt_mode: PromptMode::Arg,
371 prompt_flag: None,
372 output_format: OutputFormat::Text,
373 env_vars: vec![(
374 "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS".to_string(),
375 "1".to_string(),
376 )],
377 }
378 }
379
380 pub fn for_interactive_prompt(backend_name: &str) -> Result<Self, CustomBackendError> {
399 match backend_name {
400 "claude" => Ok(Self::claude_interactive()),
401 "kiro" => Ok(Self::kiro_interactive()),
402 "gemini" => Ok(Self::gemini_interactive()),
403 "codex" => Ok(Self::codex_interactive()),
404 "amp" => Ok(Self::amp_interactive()),
405 "copilot" => Ok(Self::copilot_interactive()),
406 "opencode" => Ok(Self::opencode_interactive()),
407 "pi" => Ok(Self::pi_interactive()),
408 "roo" => Ok(Self::roo_interactive()),
409 _ => Err(CustomBackendError),
410 }
411 }
412
413 pub fn kiro_interactive() -> Self {
418 Self {
419 command: "kiro-cli".to_string(),
420 args: vec!["chat".to_string(), "--trust-all-tools".to_string()],
421 prompt_mode: PromptMode::Arg,
422 prompt_flag: None,
423 output_format: OutputFormat::Text,
424 env_vars: vec![],
425 }
426 }
427
428 pub fn gemini_interactive() -> Self {
433 Self {
434 command: "gemini".to_string(),
435 args: vec!["--yolo".to_string()],
436 prompt_mode: PromptMode::Arg,
437 prompt_flag: Some("-i".to_string()), output_format: OutputFormat::Text,
439 env_vars: vec![],
440 }
441 }
442
443 pub fn codex_interactive() -> Self {
448 Self {
449 command: "codex".to_string(),
450 args: vec![], prompt_mode: PromptMode::Arg,
452 prompt_flag: None, output_format: OutputFormat::Text,
454 env_vars: vec![],
455 }
456 }
457
458 pub fn amp_interactive() -> Self {
463 Self {
464 command: "amp".to_string(),
465 args: vec![],
466 prompt_mode: PromptMode::Arg,
467 prompt_flag: Some("-x".to_string()),
468 output_format: OutputFormat::Text,
469 env_vars: vec![],
470 }
471 }
472
473 pub fn copilot_interactive() -> Self {
478 Self {
479 command: "copilot".to_string(),
480 args: vec![],
481 prompt_mode: PromptMode::Arg,
482 prompt_flag: Some("-p".to_string()),
483 output_format: OutputFormat::Text,
484 env_vars: vec![],
485 }
486 }
487
488 pub fn opencode() -> Self {
498 Self {
499 command: "opencode".to_string(),
500 args: vec!["run".to_string()],
501 prompt_mode: PromptMode::Arg,
502 prompt_flag: None, output_format: OutputFormat::Text,
504 env_vars: vec![],
505 }
506 }
507
508 pub fn opencode_tui() -> Self {
516 Self {
517 command: "opencode".to_string(),
518 args: vec!["run".to_string()],
519 prompt_mode: PromptMode::Arg,
520 prompt_flag: None, output_format: OutputFormat::Text,
522 env_vars: vec![],
523 }
524 }
525
526 pub fn opencode_interactive() -> Self {
536 Self {
537 command: "opencode".to_string(),
538 args: vec![],
539 prompt_mode: PromptMode::Arg,
540 prompt_flag: Some("--prompt".to_string()),
541 output_format: OutputFormat::Text,
542 env_vars: vec![],
543 }
544 }
545
546 pub fn pi() -> Self {
551 Self {
552 command: "pi".to_string(),
553 args: vec![
554 "-p".to_string(),
555 "--mode".to_string(),
556 "json".to_string(),
557 "--no-session".to_string(),
558 ],
559 prompt_mode: PromptMode::Arg,
560 prompt_flag: None, output_format: OutputFormat::PiStreamJson,
562 env_vars: vec![],
563 }
564 }
565
566 pub fn pi_interactive() -> Self {
571 Self {
572 command: "pi".to_string(),
573 args: vec!["--no-session".to_string()],
574 prompt_mode: PromptMode::Arg,
575 prompt_flag: None, output_format: OutputFormat::Text,
577 env_vars: vec![],
578 }
579 }
580
581 pub fn roo() -> Self {
588 Self {
589 command: "roo".to_string(),
590 args: vec!["--print".to_string(), "--ephemeral".to_string()],
591 prompt_mode: PromptMode::Arg,
592 prompt_flag: None,
593 output_format: OutputFormat::Text,
594 env_vars: vec![],
595 }
596 }
597
598 pub fn roo_interactive() -> Self {
603 Self {
604 command: "roo".to_string(),
605 args: vec![],
606 prompt_mode: PromptMode::Arg,
607 prompt_flag: None,
608 output_format: OutputFormat::Text,
609 env_vars: vec![],
610 }
611 }
612
613 pub fn custom(config: &CliConfig) -> Result<Self, CustomBackendError> {
618 let command = config.command.clone().ok_or(CustomBackendError)?;
619 let prompt_mode = if config.prompt_mode == "stdin" {
620 PromptMode::Stdin
621 } else {
622 PromptMode::Arg
623 };
624
625 Ok(Self {
626 command,
627 args: config.args.clone(),
628 prompt_mode,
629 prompt_flag: config.prompt_flag.clone(),
630 output_format: OutputFormat::Text,
631 env_vars: vec![],
632 })
633 }
634
635 fn build_roo_prompt_file(
639 args: &mut Vec<String>,
640 prompt: &str,
641 ) -> (Option<String>, Option<NamedTempFile>) {
642 match NamedTempFile::new() {
643 Ok(mut file) => {
644 if let Err(e) = file.write_all(prompt.as_bytes()) {
645 tracing::warn!("Failed to write roo prompt to temp file: {}", e);
646 args.push(prompt.to_string());
647 (None, None)
648 } else {
649 args.push("--prompt-file".to_string());
650 args.push(file.path().display().to_string());
651 (None, Some(file))
652 }
653 }
654 Err(e) => {
655 tracing::warn!("Failed to create temp file for roo: {}", e);
656 args.push(prompt.to_string());
657 (None, None)
658 }
659 }
660 }
661
662 pub fn build_command(
668 &self,
669 prompt: &str,
670 interactive: bool,
671 ) -> (String, Vec<String>, Option<String>, Option<NamedTempFile>) {
672 let mut args = self.args.clone();
673
674 if interactive {
676 args = self.filter_args_for_interactive(args);
677 }
678
679 let (stdin_input, temp_file) = match self.prompt_mode {
681 PromptMode::Arg => {
682 if self.command == "roo" && args.contains(&"--print".to_string()) {
685 Self::build_roo_prompt_file(&mut args, prompt)
686 } else {
687 let (prompt_text, temp_file) = if prompt.len() > 7000 {
689 match NamedTempFile::new() {
690 Ok(mut file) => {
691 if let Err(e) = file.write_all(prompt.as_bytes()) {
692 tracing::warn!("Failed to write prompt to temp file: {}", e);
693 (prompt.to_string(), None)
694 } else {
695 let path = file.path().display().to_string();
696 (
697 format!("Please read and execute the task in {}", path),
698 Some(file),
699 )
700 }
701 }
702 Err(e) => {
703 tracing::warn!("Failed to create temp file: {}", e);
704 (prompt.to_string(), None)
705 }
706 }
707 } else {
708 (prompt.to_string(), None)
709 };
710
711 if let Some(ref flag) = self.prompt_flag {
712 args.push(flag.clone());
713 }
714 args.push(prompt_text);
715 (None, temp_file)
716 }
717 }
718 PromptMode::Stdin => (Some(prompt.to_string()), None),
719 };
720
721 tracing::debug!(
723 command = %self.command,
724 args_count = args.len(),
725 prompt_len = prompt.len(),
726 interactive = interactive,
727 uses_stdin = stdin_input.is_some(),
728 uses_temp_file = temp_file.is_some(),
729 "Built CLI command"
730 );
731 tracing::trace!(prompt = %prompt, "Full prompt content");
733
734 (self.command.clone(), args, stdin_input, temp_file)
735 }
736
737 fn filter_args_for_interactive(&self, args: Vec<String>) -> Vec<String> {
739 match self.command.as_str() {
740 "kiro-cli" => args
741 .into_iter()
742 .filter(|a| a != "--no-interactive")
743 .collect(),
744 "codex" => args.into_iter().filter(|a| a != "--full-auto").collect(),
745 "amp" => args
746 .into_iter()
747 .filter(|a| a != "--dangerously-allow-all")
748 .collect(),
749 "copilot" => args
750 .into_iter()
751 .filter(|a| a != "--allow-all-tools")
752 .collect(),
753 "claude" => args.into_iter().filter(|a| a != "--print").collect(),
754 "roo" => args
755 .into_iter()
756 .filter(|a| a != "--print" && a != "--ephemeral")
757 .collect(),
758 _ => args, }
760 }
761
762 fn reconcile_codex_args(args: &mut Vec<String>) {
763 let had_dangerous_bypass = args
764 .iter()
765 .any(|arg| arg == "--dangerously-bypass-approvals-and-sandbox");
766 if had_dangerous_bypass {
767 args.retain(|arg| arg != "--dangerously-bypass-approvals-and-sandbox");
768 if !args.iter().any(|arg| arg == "--yolo") {
769 if let Some(pos) = args.iter().position(|arg| arg == "exec") {
770 args.insert(pos + 1, "--yolo".to_string());
771 } else {
772 args.push("--yolo".to_string());
773 }
774 }
775 }
776
777 if args.iter().any(|arg| arg == "--yolo") {
778 args.retain(|arg| arg != "--full-auto");
779 let mut seen_yolo = false;
781 args.retain(|arg| {
782 if arg == "--yolo" {
783 if seen_yolo {
784 return false;
785 }
786 seen_yolo = true;
787 }
788 true
789 });
790 if !seen_yolo {
791 if let Some(pos) = args.iter().position(|arg| arg == "exec") {
792 args.insert(pos + 1, "--yolo".to_string());
793 } else {
794 args.push("--yolo".to_string());
795 }
796 }
797 }
798 }
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804
805 #[test]
806 fn test_claude_backend() {
807 let backend = CliBackend::claude();
808 let (cmd, args, stdin, temp) = backend.build_command("test prompt", false);
809
810 assert_eq!(cmd, "claude");
811 assert_eq!(
812 args,
813 vec![
814 "--dangerously-skip-permissions",
815 "--verbose",
816 "--output-format",
817 "stream-json",
818 "--print",
819 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
820 ]
821 );
822 assert_eq!(stdin, Some("test prompt".to_string()));
823 assert!(temp.is_none());
824 assert_eq!(backend.output_format, OutputFormat::StreamJson);
825 }
826
827 #[test]
828 fn test_claude_interactive_backend() {
829 let backend = CliBackend::claude_interactive();
830 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
831
832 assert_eq!(cmd, "claude");
833 assert_eq!(
837 args,
838 vec![
839 "--dangerously-skip-permissions",
840 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
841 "test prompt"
842 ]
843 );
844 assert!(stdin.is_none()); assert_eq!(backend.output_format, OutputFormat::Text);
846 assert_eq!(backend.prompt_flag, None);
847 }
848
849 #[test]
850 fn test_claude_large_prompt_uses_stdin_not_temp_file() {
851 let backend = CliBackend::claude();
852 let large_prompt = "x".repeat(7001);
853 let (cmd, args, stdin, temp) = backend.build_command(&large_prompt, false);
854
855 assert_eq!(cmd, "claude");
856 assert!(args.contains(&"--print".to_string()));
857 assert_eq!(stdin, Some(large_prompt));
858 assert!(temp.is_none());
859 }
860
861 #[test]
862 fn test_non_claude_large_prompt() {
863 let backend = CliBackend::kiro();
864 let large_prompt = "x".repeat(7001);
865 let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
866
867 assert_eq!(cmd, "kiro-cli");
868 assert!(temp.is_some());
869 assert!(args.iter().any(|a| a.contains("Please read and execute")));
870 }
871
872 #[test]
873 fn test_kiro_backend() {
874 let backend = CliBackend::kiro();
875 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
876
877 assert_eq!(cmd, "kiro-cli");
878 assert_eq!(
879 args,
880 vec![
881 "chat",
882 "--no-interactive",
883 "--trust-all-tools",
884 "test prompt"
885 ]
886 );
887 assert!(stdin.is_none());
888 }
889
890 #[test]
891 fn test_gemini_backend() {
892 let backend = CliBackend::gemini();
893 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
894
895 assert_eq!(cmd, "gemini");
896 assert_eq!(args, vec!["--yolo", "-p", "test prompt"]);
897 assert!(stdin.is_none());
898 }
899
900 #[test]
901 fn test_codex_backend() {
902 let backend = CliBackend::codex();
903 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
904
905 assert_eq!(cmd, "codex");
906 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
907 assert!(stdin.is_none());
908 }
909
910 #[test]
911 fn test_codex_large_prompt_uses_temp_file() {
912 let backend = CliBackend::codex();
913 let large_prompt = "x".repeat(7001);
914 let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
915
916 assert_eq!(cmd, "codex");
917 assert!(temp.is_some());
918 assert!(args.iter().any(|a| a.contains("Please read and execute")));
919 }
920
921 #[test]
922 fn test_amp_backend() {
923 let backend = CliBackend::amp();
924 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
925
926 assert_eq!(cmd, "amp");
927 assert_eq!(args, vec!["--dangerously-allow-all", "-x", "test prompt"]);
928 assert!(stdin.is_none());
929 }
930
931 #[test]
932 fn test_copilot_backend() {
933 let backend = CliBackend::copilot();
934 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
935
936 assert_eq!(cmd, "copilot");
937 assert_eq!(
938 args,
939 vec![
940 "--allow-all-tools",
941 "--output-format",
942 "json",
943 "-p",
944 "test prompt"
945 ]
946 );
947 assert!(stdin.is_none());
948 assert_eq!(backend.output_format, OutputFormat::CopilotStreamJson);
949 }
950
951 #[test]
952 fn test_copilot_tui_backend() {
953 let backend = CliBackend::copilot_tui();
954 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
955
956 assert_eq!(cmd, "copilot");
957 assert_eq!(args, vec!["test prompt"]);
959 assert!(stdin.is_none());
960 assert_eq!(backend.output_format, OutputFormat::Text);
961 assert_eq!(backend.prompt_flag, None);
962 }
963
964 #[test]
965 fn test_from_config() {
966 let config = CliConfig {
967 backend: "claude".to_string(),
968 command: None,
969 prompt_mode: "arg".to_string(),
970 ..Default::default()
971 };
972 let backend = CliBackend::from_config(&config).unwrap();
973
974 assert_eq!(backend.command, "claude");
975 assert_eq!(backend.prompt_mode, PromptMode::Stdin);
976 assert_eq!(backend.prompt_flag, None);
977 assert!(backend.args.contains(&"--print".to_string()));
978 }
979
980 #[test]
981 fn test_from_config_command_override() {
982 let config = CliConfig {
983 backend: "claude".to_string(),
984 command: Some("my-custom-claude".to_string()),
985 prompt_mode: "arg".to_string(),
986 ..Default::default()
987 };
988 let backend = CliBackend::from_config(&config).unwrap();
989
990 assert_eq!(backend.command, "my-custom-claude");
991 assert_eq!(backend.prompt_flag, None);
992 assert_eq!(backend.prompt_mode, PromptMode::Stdin);
993 assert!(backend.args.contains(&"--print".to_string()));
994 assert_eq!(backend.output_format, OutputFormat::StreamJson);
995 }
996
997 #[test]
998 fn test_kiro_interactive_mode_omits_no_interactive_flag() {
999 let backend = CliBackend::kiro();
1000 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1001
1002 assert_eq!(cmd, "kiro-cli");
1003 assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
1004 assert!(stdin.is_none());
1005 assert!(!args.contains(&"--no-interactive".to_string()));
1006 }
1007
1008 #[test]
1009 fn test_codex_interactive_mode_omits_full_auto() {
1010 let backend = CliBackend::codex();
1011 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1012
1013 assert_eq!(cmd, "codex");
1014 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
1015 assert!(stdin.is_none());
1016 assert!(!args.contains(&"--full-auto".to_string()));
1017 }
1018
1019 #[test]
1020 fn test_amp_interactive_mode_no_flags() {
1021 let backend = CliBackend::amp();
1022 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1023
1024 assert_eq!(cmd, "amp");
1025 assert_eq!(args, vec!["-x", "test prompt"]);
1026 assert!(stdin.is_none());
1027 assert!(!args.contains(&"--dangerously-allow-all".to_string()));
1028 }
1029
1030 #[test]
1031 fn test_copilot_interactive_mode_omits_allow_all_tools() {
1032 let backend = CliBackend::copilot();
1033 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1034
1035 assert_eq!(cmd, "copilot");
1036 assert_eq!(args, vec!["--output-format", "json", "-p", "test prompt"]);
1037 assert!(stdin.is_none());
1038 assert!(!args.contains(&"--allow-all-tools".to_string()));
1039 }
1040
1041 #[test]
1042 fn test_claude_interactive_mode_omits_print() {
1043 let backend = CliBackend::claude();
1044 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
1045 let (_, args_interactive, stdin_interactive, _) =
1046 backend.build_command("test prompt", true);
1047
1048 assert_eq!(cmd, "claude");
1049 assert!(args_auto.contains(&"--print".to_string()));
1050 assert!(!args_interactive.contains(&"--print".to_string()));
1051 assert_eq!(
1052 args_interactive,
1053 vec![
1054 "--dangerously-skip-permissions",
1055 "--verbose",
1056 "--output-format",
1057 "stream-json",
1058 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
1059 ]
1060 );
1061 assert_eq!(stdin_auto, Some("test prompt".to_string()));
1062 assert_eq!(stdin_interactive, Some("test prompt".to_string()));
1063 }
1064
1065 #[test]
1066 fn test_gemini_interactive_mode_unchanged() {
1067 let backend = CliBackend::gemini();
1068 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
1069 let (_, args_interactive, stdin_interactive, _) =
1070 backend.build_command("test prompt", true);
1071
1072 assert_eq!(cmd, "gemini");
1073 assert_eq!(args_auto, args_interactive);
1074 assert_eq!(args_auto, vec!["--yolo", "-p", "test prompt"]);
1075 assert_eq!(stdin_auto, stdin_interactive);
1076 assert!(stdin_auto.is_none());
1077 }
1078
1079 #[test]
1080 fn test_custom_backend_with_prompt_flag_short() {
1081 let config = CliConfig {
1082 backend: "custom".to_string(),
1083 command: Some("my-agent".to_string()),
1084 prompt_mode: "arg".to_string(),
1085 prompt_flag: Some("-p".to_string()),
1086 ..Default::default()
1087 };
1088 let backend = CliBackend::from_config(&config).unwrap();
1089 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1090
1091 assert_eq!(cmd, "my-agent");
1092 assert_eq!(args, vec!["-p", "test prompt"]);
1093 assert!(stdin.is_none());
1094 }
1095
1096 #[test]
1097 fn test_custom_backend_with_prompt_flag_long() {
1098 let config = CliConfig {
1099 backend: "custom".to_string(),
1100 command: Some("my-agent".to_string()),
1101 prompt_mode: "arg".to_string(),
1102 prompt_flag: Some("--prompt".to_string()),
1103 ..Default::default()
1104 };
1105 let backend = CliBackend::from_config(&config).unwrap();
1106 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1107
1108 assert_eq!(cmd, "my-agent");
1109 assert_eq!(args, vec!["--prompt", "test prompt"]);
1110 assert!(stdin.is_none());
1111 }
1112
1113 #[test]
1114 fn test_custom_backend_without_prompt_flag_positional() {
1115 let config = CliConfig {
1116 backend: "custom".to_string(),
1117 command: Some("my-agent".to_string()),
1118 prompt_mode: "arg".to_string(),
1119 prompt_flag: None,
1120 ..Default::default()
1121 };
1122 let backend = CliBackend::from_config(&config).unwrap();
1123 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1124
1125 assert_eq!(cmd, "my-agent");
1126 assert_eq!(args, vec!["test prompt"]);
1127 assert!(stdin.is_none());
1128 }
1129
1130 #[test]
1131 fn test_custom_backend_without_command_returns_error() {
1132 let config = CliConfig {
1133 backend: "custom".to_string(),
1134 command: None,
1135 prompt_mode: "arg".to_string(),
1136 ..Default::default()
1137 };
1138 let result = CliBackend::from_config(&config);
1139
1140 assert!(result.is_err());
1141 let err = result.unwrap_err();
1142 assert_eq!(
1143 err.to_string(),
1144 "custom backend requires a command to be specified"
1145 );
1146 }
1147
1148 #[test]
1149 fn test_kiro_with_agent() {
1150 let backend = CliBackend::kiro_with_agent("my-agent".to_string(), &[]);
1151 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1152
1153 assert_eq!(cmd, "kiro-cli");
1154 assert_eq!(
1155 args,
1156 vec![
1157 "chat",
1158 "--no-interactive",
1159 "--trust-all-tools",
1160 "--agent",
1161 "my-agent",
1162 "test prompt"
1163 ]
1164 );
1165 assert!(stdin.is_none());
1166 }
1167
1168 #[test]
1169 fn test_kiro_with_agent_extra_args() {
1170 let extra_args = vec!["--verbose".to_string(), "--debug".to_string()];
1171 let backend = CliBackend::kiro_with_agent("my-agent".to_string(), &extra_args);
1172 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1173
1174 assert_eq!(cmd, "kiro-cli");
1175 assert_eq!(
1176 args,
1177 vec![
1178 "chat",
1179 "--no-interactive",
1180 "--trust-all-tools",
1181 "--agent",
1182 "my-agent",
1183 "--verbose",
1184 "--debug",
1185 "test prompt"
1186 ]
1187 );
1188 assert!(stdin.is_none());
1189 }
1190
1191 #[test]
1192 fn test_from_name_claude() {
1193 let backend = CliBackend::from_name("claude").unwrap();
1194 assert_eq!(backend.command, "claude");
1195 assert_eq!(backend.prompt_mode, PromptMode::Stdin);
1196 assert_eq!(backend.prompt_flag, None);
1197 assert!(backend.args.contains(&"--print".to_string()));
1198 }
1199
1200 #[test]
1201 fn test_from_name_kiro() {
1202 let backend = CliBackend::from_name("kiro").unwrap();
1203 assert_eq!(backend.command, "kiro-cli");
1204 }
1205
1206 #[test]
1207 fn test_from_name_gemini() {
1208 let backend = CliBackend::from_name("gemini").unwrap();
1209 assert_eq!(backend.command, "gemini");
1210 }
1211
1212 #[test]
1213 fn test_from_name_codex() {
1214 let backend = CliBackend::from_name("codex").unwrap();
1215 assert_eq!(backend.command, "codex");
1216 }
1217
1218 #[test]
1219 fn test_from_name_amp() {
1220 let backend = CliBackend::from_name("amp").unwrap();
1221 assert_eq!(backend.command, "amp");
1222 }
1223
1224 #[test]
1225 fn test_from_name_copilot() {
1226 let backend = CliBackend::from_name("copilot").unwrap();
1227 assert_eq!(backend.command, "copilot");
1228 assert_eq!(backend.prompt_flag, Some("-p".to_string()));
1229 }
1230
1231 #[test]
1232 fn test_from_name_invalid() {
1233 let result = CliBackend::from_name("invalid");
1234 assert!(result.is_err());
1235 }
1236
1237 #[test]
1238 fn test_from_hat_backend_named() {
1239 let hat_backend = HatBackend::Named("claude".to_string());
1240 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1241 assert_eq!(backend.command, "claude");
1242 }
1243
1244 #[test]
1245 fn test_from_hat_backend_kiro_agent() {
1246 let hat_backend = HatBackend::KiroAgent {
1247 backend_type: "kiro".to_string(),
1248 agent: "my-agent".to_string(),
1249 args: vec![],
1250 };
1251 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1252 let (cmd, args, _, _) = backend.build_command("test", false);
1253 assert_eq!(cmd, "kiro-cli");
1254 assert!(args.contains(&"--agent".to_string()));
1255 assert!(args.contains(&"my-agent".to_string()));
1256 }
1257
1258 #[test]
1259 fn test_from_hat_backend_kiro_acp_agent_uses_acp_executor() {
1260 let hat_backend = HatBackend::KiroAgent {
1261 backend_type: "kiro-acp".to_string(),
1262 agent: "my-agent".to_string(),
1263 args: vec![],
1264 };
1265 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1266 assert_eq!(backend.command, "kiro-cli");
1267 assert_eq!(backend.output_format, OutputFormat::Acp);
1268 assert!(backend.args.contains(&"acp".to_string()));
1269 assert!(backend.args.contains(&"--agent".to_string()));
1270 assert!(backend.args.contains(&"my-agent".to_string()));
1271 }
1272
1273 #[test]
1274 fn test_from_hat_backend_kiro_agent_with_args() {
1275 let hat_backend = HatBackend::KiroAgent {
1276 backend_type: "kiro".to_string(),
1277 agent: "my-agent".to_string(),
1278 args: vec!["--verbose".to_string()],
1279 };
1280 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1281 let (cmd, args, _, _) = backend.build_command("test", false);
1282 assert_eq!(cmd, "kiro-cli");
1283 assert!(args.contains(&"--agent".to_string()));
1284 assert!(args.contains(&"my-agent".to_string()));
1285 assert!(args.contains(&"--verbose".to_string()));
1286 }
1287
1288 #[test]
1289 fn test_from_hat_backend_named_with_args() {
1290 let hat_backend = HatBackend::NamedWithArgs {
1291 backend_type: "claude".to_string(),
1292 args: vec!["--model".to_string(), "claude-sonnet-4".to_string()],
1293 };
1294 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1295 assert_eq!(backend.command, "claude");
1296 assert!(backend.args.contains(&"--model".to_string()));
1297 assert!(backend.args.contains(&"claude-sonnet-4".to_string()));
1298 }
1299
1300 #[test]
1301 fn test_codex_named_with_args_dangerous_bypass_normalizes_to_yolo() {
1302 let hat_backend = HatBackend::NamedWithArgs {
1303 backend_type: "codex".to_string(),
1304 args: vec!["--dangerously-bypass-approvals-and-sandbox".to_string()],
1305 };
1306 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1307 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1308
1309 assert_eq!(cmd, "codex");
1310 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
1311 }
1312
1313 #[test]
1314 fn test_codex_named_with_args_yolo_removes_full_auto() {
1315 let hat_backend = HatBackend::NamedWithArgs {
1316 backend_type: "codex".to_string(),
1317 args: vec!["--yolo".to_string()],
1318 };
1319 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1320 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1321
1322 assert_eq!(cmd, "codex");
1323 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
1324 }
1325
1326 #[test]
1327 fn test_from_hat_backend_custom() {
1328 let hat_backend = HatBackend::Custom {
1329 command: "my-cli".to_string(),
1330 args: vec!["--flag".to_string()],
1331 };
1332 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1333 assert_eq!(backend.command, "my-cli");
1334 assert_eq!(backend.args, vec!["--flag"]);
1335 }
1336
1337 #[test]
1342 fn test_for_interactive_prompt_claude() {
1343 let backend = CliBackend::for_interactive_prompt("claude").unwrap();
1344 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1345
1346 assert_eq!(cmd, "claude");
1347 assert_eq!(
1349 args,
1350 vec![
1351 "--dangerously-skip-permissions",
1352 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
1353 "test prompt"
1354 ]
1355 );
1356 assert!(stdin.is_none());
1357 assert_eq!(backend.prompt_flag, None);
1358 }
1359
1360 #[test]
1361 fn test_for_interactive_prompt_kiro() {
1362 let backend = CliBackend::for_interactive_prompt("kiro").unwrap();
1363 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1364
1365 assert_eq!(cmd, "kiro-cli");
1366 assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
1368 assert!(!args.contains(&"--no-interactive".to_string()));
1369 assert!(stdin.is_none());
1370 }
1371
1372 #[test]
1373 fn test_for_interactive_prompt_gemini() {
1374 let backend = CliBackend::for_interactive_prompt("gemini").unwrap();
1375 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1376
1377 assert_eq!(cmd, "gemini");
1378 assert_eq!(args, vec!["--yolo", "-i", "test prompt"]);
1380 assert_eq!(backend.prompt_flag, Some("-i".to_string()));
1381 assert!(stdin.is_none());
1382 }
1383
1384 #[test]
1385 fn test_for_interactive_prompt_codex() {
1386 let backend = CliBackend::for_interactive_prompt("codex").unwrap();
1387 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1388
1389 assert_eq!(cmd, "codex");
1390 assert_eq!(args, vec!["test prompt"]);
1392 assert!(!args.contains(&"exec".to_string()));
1393 assert!(!args.contains(&"--full-auto".to_string()));
1394 assert!(stdin.is_none());
1395 }
1396
1397 #[test]
1398 fn test_for_interactive_prompt_amp() {
1399 let backend = CliBackend::for_interactive_prompt("amp").unwrap();
1400 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1401
1402 assert_eq!(cmd, "amp");
1403 assert_eq!(args, vec!["-x", "test prompt"]);
1405 assert!(!args.contains(&"--dangerously-allow-all".to_string()));
1406 assert!(stdin.is_none());
1407 }
1408
1409 #[test]
1410 fn test_for_interactive_prompt_copilot() {
1411 let backend = CliBackend::for_interactive_prompt("copilot").unwrap();
1412 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1413
1414 assert_eq!(cmd, "copilot");
1415 assert_eq!(args, vec!["-p", "test prompt"]);
1417 assert!(!args.contains(&"--allow-all-tools".to_string()));
1418 assert!(stdin.is_none());
1419 }
1420
1421 #[test]
1422 fn test_for_interactive_prompt_invalid() {
1423 let result = CliBackend::for_interactive_prompt("invalid_backend");
1424 assert!(result.is_err());
1425 }
1426
1427 #[test]
1432 fn test_opencode_backend() {
1433 let backend = CliBackend::opencode();
1434 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1435
1436 assert_eq!(cmd, "opencode");
1437 assert_eq!(args, vec!["run", "test prompt"]);
1439 assert!(stdin.is_none());
1440 assert_eq!(backend.output_format, OutputFormat::Text);
1441 assert_eq!(backend.prompt_flag, None);
1442 }
1443
1444 #[test]
1445 fn test_opencode_tui_backend() {
1446 let backend = CliBackend::opencode_tui();
1447 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1448
1449 assert_eq!(cmd, "opencode");
1450 assert_eq!(args, vec!["run", "test prompt"]);
1452 assert!(stdin.is_none());
1453 assert_eq!(backend.output_format, OutputFormat::Text);
1454 assert_eq!(backend.prompt_flag, None);
1455 }
1456
1457 #[test]
1458 fn test_opencode_interactive_mode_unchanged() {
1459 let backend = CliBackend::opencode();
1461 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
1462 let (_, args_interactive, stdin_interactive, _) =
1463 backend.build_command("test prompt", true);
1464
1465 assert_eq!(cmd, "opencode");
1466 assert_eq!(args_auto, args_interactive);
1468 assert_eq!(args_auto, vec!["run", "test prompt"]);
1469 assert!(stdin_auto.is_none());
1470 assert!(stdin_interactive.is_none());
1471 }
1472
1473 #[test]
1474 fn test_from_name_opencode() {
1475 let backend = CliBackend::from_name("opencode").unwrap();
1476 assert_eq!(backend.command, "opencode");
1477 assert_eq!(backend.prompt_flag, None); }
1479
1480 #[test]
1481 fn test_for_interactive_prompt_opencode() {
1482 let backend = CliBackend::for_interactive_prompt("opencode").unwrap();
1483 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1484
1485 assert_eq!(cmd, "opencode");
1486 assert_eq!(args, vec!["--prompt", "test prompt"]);
1488 assert!(stdin.is_none());
1489 assert_eq!(backend.prompt_flag, Some("--prompt".to_string()));
1490 }
1491
1492 #[test]
1493 fn test_opencode_interactive_launches_tui_not_headless() {
1494 let backend = CliBackend::opencode_interactive();
1504 let (cmd, args, _, _) = backend.build_command("test prompt", true);
1505
1506 assert_eq!(cmd, "opencode");
1507 assert!(
1510 !args.contains(&"run".to_string()),
1511 "opencode_interactive() should not use 'run' subcommand. \
1512 'opencode run' is headless mode, but interactive mode needs TUI. \
1513 Expected: opencode --prompt \"test prompt\", got: opencode {}",
1514 args.join(" ")
1515 );
1516 assert!(
1518 args.contains(&"--prompt".to_string()),
1519 "opencode_interactive() should use --prompt flag for TUI mode. \
1520 Expected args to contain '--prompt', got: {:?}",
1521 args
1522 );
1523 }
1524
1525 #[test]
1530 fn test_pi_backend() {
1531 let backend = CliBackend::pi();
1532 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1533
1534 assert_eq!(cmd, "pi");
1535 assert_eq!(
1536 args,
1537 vec!["-p", "--mode", "json", "--no-session", "test prompt"]
1538 );
1539 assert!(stdin.is_none());
1540 assert_eq!(backend.output_format, OutputFormat::PiStreamJson);
1541 assert_eq!(backend.prompt_flag, None); }
1543
1544 #[test]
1545 fn test_pi_interactive_backend() {
1546 let backend = CliBackend::pi_interactive();
1547 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1548
1549 assert_eq!(cmd, "pi");
1550 assert_eq!(args, vec!["--no-session", "test prompt"]);
1552 assert!(stdin.is_none());
1553 assert_eq!(backend.output_format, OutputFormat::Text);
1554 assert_eq!(backend.prompt_flag, None);
1555 }
1556
1557 #[test]
1558 fn test_from_name_pi() {
1559 let backend = CliBackend::from_name("pi").unwrap();
1560 assert_eq!(backend.command, "pi");
1561 assert_eq!(backend.prompt_flag, None);
1562 assert_eq!(backend.output_format, OutputFormat::PiStreamJson);
1563 }
1564
1565 #[test]
1566 fn test_for_interactive_prompt_pi() {
1567 let backend = CliBackend::for_interactive_prompt("pi").unwrap();
1568 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1569
1570 assert_eq!(cmd, "pi");
1571 assert_eq!(args, vec!["--no-session", "test prompt"]);
1572 assert!(stdin.is_none());
1573 assert_eq!(backend.output_format, OutputFormat::Text);
1574 }
1575
1576 #[test]
1577 fn test_from_config_pi() {
1578 let config = CliConfig {
1579 backend: "pi".to_string(),
1580 command: None,
1581 prompt_mode: "arg".to_string(),
1582 args: vec![
1583 "--provider".to_string(),
1584 "zai".to_string(),
1585 "--model".to_string(),
1586 "glm-5".to_string(),
1587 ],
1588 ..Default::default()
1589 };
1590 let backend = CliBackend::from_config(&config).unwrap();
1591 let (_cmd, args, _stdin, _temp) = backend.build_command("test prompt", false);
1592
1593 assert_eq!(backend.command, "pi");
1594 assert_eq!(backend.output_format, OutputFormat::PiStreamJson);
1595 assert!(args.contains(&"--provider".to_string()));
1596 assert!(args.contains(&"zai".to_string()));
1597 assert!(args.contains(&"--model".to_string()));
1598 assert!(args.contains(&"glm-5".to_string()));
1599 }
1600
1601 #[test]
1602 fn test_from_hat_backend_named_with_args_pi() {
1603 let hat_backend = HatBackend::NamedWithArgs {
1604 backend_type: "pi".to_string(),
1605 args: vec![
1606 "--provider".to_string(),
1607 "anthropic".to_string(),
1608 "--model".to_string(),
1609 "claude-sonnet-4".to_string(),
1610 ],
1611 };
1612 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1613 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1614
1615 assert_eq!(cmd, "pi");
1616 assert!(args.contains(&"-p".to_string()));
1618 assert!(args.contains(&"--mode".to_string()));
1619 assert!(args.contains(&"json".to_string()));
1620 assert!(args.contains(&"--no-session".to_string()));
1621 assert!(args.contains(&"--provider".to_string()));
1622 assert!(args.contains(&"anthropic".to_string()));
1623 assert!(args.contains(&"--model".to_string()));
1624 assert!(args.contains(&"claude-sonnet-4".to_string()));
1625 assert!(args.contains(&"test prompt".to_string()));
1626 }
1627
1628 #[test]
1629 fn test_pi_large_prompt_uses_temp_file() {
1630 let backend = CliBackend::pi();
1631 let large_prompt = "x".repeat(7001);
1632 let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
1633
1634 assert_eq!(cmd, "pi");
1635 assert!(temp.is_some());
1636 assert!(args.iter().any(|a| a.contains("Please read and execute")));
1637 }
1638
1639 #[test]
1640 fn test_pi_interactive_mode_unchanged() {
1641 let backend = CliBackend::pi();
1643 let (_, args_auto, _, _) = backend.build_command("test prompt", false);
1644 let (_, args_interactive, _, _) = backend.build_command("test prompt", true);
1645
1646 assert_eq!(args_auto, args_interactive);
1647 }
1648
1649 #[test]
1650 fn test_custom_args_can_be_appended() {
1651 let mut backend = CliBackend::opencode();
1654
1655 let custom_args = vec!["--model=gpt-4".to_string(), "--temperature=0.7".to_string()];
1657 backend.args.extend(custom_args.clone());
1658
1659 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1661
1662 assert_eq!(cmd, "opencode");
1663 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();
1671 let model_idx = args.iter().position(|a| a == "--model=gpt-4").unwrap();
1672 assert!(
1673 run_idx < model_idx,
1674 "Original args should come before custom args"
1675 );
1676 }
1677
1678 #[test]
1683 fn test_claude_interactive_teams_backend() {
1684 let backend = CliBackend::claude_interactive_teams();
1685 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1686
1687 assert_eq!(cmd, "claude");
1688 assert_eq!(
1689 args,
1690 vec![
1691 "--dangerously-skip-permissions",
1692 "--disallowedTools=TodoWrite",
1693 "test prompt"
1694 ]
1695 );
1696 assert!(stdin.is_none());
1697 assert_eq!(backend.output_format, OutputFormat::Text);
1698 assert_eq!(backend.prompt_flag, None);
1699 assert_eq!(
1700 backend.env_vars,
1701 vec![(
1702 "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS".to_string(),
1703 "1".to_string()
1704 )]
1705 );
1706 }
1707
1708 #[test]
1709 fn test_env_vars_default_empty() {
1710 assert!(CliBackend::claude().env_vars.is_empty());
1712 assert!(CliBackend::claude_interactive().env_vars.is_empty());
1713 assert!(CliBackend::kiro().env_vars.is_empty());
1714 assert!(CliBackend::gemini().env_vars.is_empty());
1715 assert!(CliBackend::codex().env_vars.is_empty());
1716 assert!(CliBackend::amp().env_vars.is_empty());
1717 assert!(CliBackend::copilot().env_vars.is_empty());
1718 assert!(CliBackend::opencode().env_vars.is_empty());
1719 assert!(CliBackend::pi().env_vars.is_empty());
1720 assert!(CliBackend::roo().env_vars.is_empty());
1721 }
1722
1723 #[test]
1728 fn test_roo_backend() {
1729 let backend = CliBackend::roo();
1730 let (cmd, args, stdin, temp) = backend.build_command("test prompt", false);
1731
1732 assert_eq!(cmd, "roo");
1733 assert!(
1735 temp.is_some(),
1736 "roo should always use temp file for prompts"
1737 );
1738 assert!(
1739 args.contains(&"--print".to_string()),
1740 "roo headless should have --print"
1741 );
1742 assert!(
1743 args.contains(&"--ephemeral".to_string()),
1744 "roo headless should have --ephemeral"
1745 );
1746 assert!(
1747 args.contains(&"--prompt-file".to_string()),
1748 "roo should use --prompt-file"
1749 );
1750 assert!(stdin.is_none());
1751 assert_eq!(backend.output_format, OutputFormat::Text);
1752 }
1753
1754 #[test]
1755 fn test_roo_interactive() {
1756 let backend = CliBackend::roo_interactive();
1757 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1758
1759 assert_eq!(cmd, "roo");
1760 assert_eq!(args, vec!["test prompt"]);
1762 assert!(stdin.is_none());
1763 assert_eq!(backend.output_format, OutputFormat::Text);
1764 assert_eq!(backend.prompt_flag, None);
1765 }
1766
1767 #[test]
1768 fn test_from_name_roo() {
1769 let backend = CliBackend::from_name("roo").unwrap();
1770 assert_eq!(backend.command, "roo");
1771 assert_eq!(backend.prompt_flag, None);
1772 assert_eq!(backend.output_format, OutputFormat::Text);
1773 }
1774
1775 #[test]
1776 fn test_from_config_roo() {
1777 let config = CliConfig {
1778 backend: "roo".to_string(),
1779 command: None,
1780 prompt_mode: "arg".to_string(),
1781 ..Default::default()
1782 };
1783 let backend = CliBackend::from_config(&config).unwrap();
1784
1785 assert_eq!(backend.command, "roo");
1786 assert_eq!(backend.output_format, OutputFormat::Text);
1787 assert!(backend.args.contains(&"--print".to_string()));
1788 assert!(backend.args.contains(&"--ephemeral".to_string()));
1789 }
1790
1791 #[test]
1792 fn test_from_config_roo_with_args() {
1793 let config = CliConfig {
1794 backend: "roo".to_string(),
1795 command: None,
1796 prompt_mode: "arg".to_string(),
1797 args: vec![
1798 "--provider".to_string(),
1799 "bedrock".to_string(),
1800 "--model".to_string(),
1801 "anthropic.claude-sonnet-4-6".to_string(),
1802 ],
1803 ..Default::default()
1804 };
1805 let backend = CliBackend::from_config(&config).unwrap();
1806 let (_cmd, args, _stdin, _temp) = backend.build_command("test prompt", false);
1807
1808 assert_eq!(backend.command, "roo");
1809 assert!(args.contains(&"--print".to_string()));
1811 assert!(args.contains(&"--ephemeral".to_string()));
1812 assert!(args.contains(&"--provider".to_string()));
1813 assert!(args.contains(&"bedrock".to_string()));
1814 assert!(args.contains(&"--model".to_string()));
1815 assert!(args.contains(&"anthropic.claude-sonnet-4-6".to_string()));
1816 assert!(args.contains(&"--prompt-file".to_string()));
1817 }
1818
1819 #[test]
1820 fn test_for_interactive_prompt_roo() {
1821 let backend = CliBackend::for_interactive_prompt("roo").unwrap();
1822 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1823
1824 assert_eq!(cmd, "roo");
1825 assert_eq!(args, vec!["test prompt"]);
1827 assert!(stdin.is_none());
1828 assert_eq!(backend.output_format, OutputFormat::Text);
1829 }
1830
1831 #[test]
1832 fn test_roo_interactive_mode_removes_print() {
1833 let backend = CliBackend::roo();
1834 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1835
1836 assert_eq!(cmd, "roo");
1837 assert!(
1839 !args.contains(&"--print".to_string()),
1840 "interactive mode should remove --print"
1841 );
1842 assert!(
1843 !args.contains(&"--ephemeral".to_string()),
1844 "interactive mode should remove --ephemeral"
1845 );
1846 assert!(stdin.is_none());
1847 }
1848
1849 #[test]
1850 fn test_roo_uses_prompt_file() {
1851 let backend = CliBackend::roo();
1852 let (_, args_small, _, temp_small) = backend.build_command("small prompt", false);
1854 assert!(
1855 temp_small.is_some(),
1856 "even small prompts should use temp file"
1857 );
1858 assert!(
1859 args_small.contains(&"--prompt-file".to_string()),
1860 "should use --prompt-file"
1861 );
1862
1863 let large_prompt = "x".repeat(10000);
1865 let (_, args_large, _, temp_large) = backend.build_command(&large_prompt, false);
1866 assert!(temp_large.is_some(), "large prompts should use temp file");
1867 assert!(
1868 args_large.contains(&"--prompt-file".to_string()),
1869 "should use --prompt-file for large prompts"
1870 );
1871 }
1872
1873 #[test]
1874 fn test_roo_prompt_file_content() {
1875 use std::io::{Read, Seek};
1876 let backend = CliBackend::roo();
1877 let prompt = "This is a test prompt for roo";
1878 let (_, _, _, temp) = backend.build_command(prompt, false);
1879
1880 let mut temp_file = temp.expect("should have temp file");
1881 let mut content = String::new();
1882 temp_file
1883 .as_file_mut()
1884 .seek(std::io::SeekFrom::Start(0))
1885 .unwrap();
1886 temp_file
1887 .as_file_mut()
1888 .read_to_string(&mut content)
1889 .unwrap();
1890 assert_eq!(content, prompt);
1891 }
1892}