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_pty(
669 &self,
670 prompt: &str,
671 ) -> (String, Vec<String>, Option<String>, Option<NamedTempFile>) {
672 if self.prompt_mode == PromptMode::Stdin {
673 let mut pty_backend = self.clone();
675 pty_backend.prompt_mode = PromptMode::Arg;
676 if pty_backend.prompt_flag.is_none() {
678 pty_backend.prompt_flag = Some("-p".to_string());
679 }
680 pty_backend.build_command(prompt, false)
681 } else {
682 self.build_command(prompt, false)
683 }
684 }
685
686 pub fn build_command(
692 &self,
693 prompt: &str,
694 interactive: bool,
695 ) -> (String, Vec<String>, Option<String>, Option<NamedTempFile>) {
696 let mut args = self.args.clone();
697
698 if interactive {
700 args = self.filter_args_for_interactive(args);
701 }
702
703 let (stdin_input, temp_file) = match self.prompt_mode {
705 PromptMode::Arg => {
706 if self.command == "roo" && args.contains(&"--print".to_string()) {
709 Self::build_roo_prompt_file(&mut args, prompt)
710 } else {
711 let (prompt_text, temp_file) = if prompt.len() > 7000 {
713 match NamedTempFile::new() {
714 Ok(mut file) => {
715 if let Err(e) = file.write_all(prompt.as_bytes()) {
716 tracing::warn!("Failed to write prompt to temp file: {}", e);
717 (prompt.to_string(), None)
718 } else {
719 let path = file.path().display().to_string();
720 (
721 format!("Please read and execute the task in {}", path),
722 Some(file),
723 )
724 }
725 }
726 Err(e) => {
727 tracing::warn!("Failed to create temp file: {}", e);
728 (prompt.to_string(), None)
729 }
730 }
731 } else {
732 (prompt.to_string(), None)
733 };
734
735 if let Some(ref flag) = self.prompt_flag {
736 args.push(flag.clone());
737 }
738 args.push(prompt_text);
739 (None, temp_file)
740 }
741 }
742 PromptMode::Stdin => (Some(prompt.to_string()), None),
743 };
744
745 tracing::debug!(
747 command = %self.command,
748 args_count = args.len(),
749 prompt_len = prompt.len(),
750 interactive = interactive,
751 uses_stdin = stdin_input.is_some(),
752 uses_temp_file = temp_file.is_some(),
753 "Built CLI command"
754 );
755 tracing::trace!(prompt = %prompt, "Full prompt content");
757
758 (self.command.clone(), args, stdin_input, temp_file)
759 }
760
761 fn filter_args_for_interactive(&self, args: Vec<String>) -> Vec<String> {
763 match self.command.as_str() {
764 "kiro-cli" => args
765 .into_iter()
766 .filter(|a| a != "--no-interactive")
767 .collect(),
768 "codex" => args.into_iter().filter(|a| a != "--full-auto").collect(),
769 "amp" => args
770 .into_iter()
771 .filter(|a| a != "--dangerously-allow-all")
772 .collect(),
773 "copilot" => args
774 .into_iter()
775 .filter(|a| a != "--allow-all-tools")
776 .collect(),
777 "claude" => args.into_iter().filter(|a| a != "--print").collect(),
778 "roo" => args
779 .into_iter()
780 .filter(|a| a != "--print" && a != "--ephemeral")
781 .collect(),
782 _ => args, }
784 }
785
786 fn reconcile_codex_args(args: &mut Vec<String>) {
787 let had_dangerous_bypass = args
788 .iter()
789 .any(|arg| arg == "--dangerously-bypass-approvals-and-sandbox");
790 if had_dangerous_bypass {
791 args.retain(|arg| arg != "--dangerously-bypass-approvals-and-sandbox");
792 if !args.iter().any(|arg| arg == "--yolo") {
793 if let Some(pos) = args.iter().position(|arg| arg == "exec") {
794 args.insert(pos + 1, "--yolo".to_string());
795 } else {
796 args.push("--yolo".to_string());
797 }
798 }
799 }
800
801 if args.iter().any(|arg| arg == "--yolo") {
802 args.retain(|arg| arg != "--full-auto");
803 let mut seen_yolo = false;
805 args.retain(|arg| {
806 if arg == "--yolo" {
807 if seen_yolo {
808 return false;
809 }
810 seen_yolo = true;
811 }
812 true
813 });
814 if !seen_yolo {
815 if let Some(pos) = args.iter().position(|arg| arg == "exec") {
816 args.insert(pos + 1, "--yolo".to_string());
817 } else {
818 args.push("--yolo".to_string());
819 }
820 }
821 }
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828
829 #[test]
830 fn test_claude_backend() {
831 let backend = CliBackend::claude();
832 let (cmd, args, stdin, temp) = backend.build_command("test prompt", false);
833
834 assert_eq!(cmd, "claude");
835 assert_eq!(
836 args,
837 vec![
838 "--dangerously-skip-permissions",
839 "--verbose",
840 "--output-format",
841 "stream-json",
842 "--print",
843 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
844 ]
845 );
846 assert_eq!(stdin, Some("test prompt".to_string()));
847 assert!(temp.is_none());
848 assert_eq!(backend.output_format, OutputFormat::StreamJson);
849 }
850
851 #[test]
852 fn test_claude_interactive_backend() {
853 let backend = CliBackend::claude_interactive();
854 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
855
856 assert_eq!(cmd, "claude");
857 assert_eq!(
861 args,
862 vec![
863 "--dangerously-skip-permissions",
864 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
865 "test prompt"
866 ]
867 );
868 assert!(stdin.is_none()); assert_eq!(backend.output_format, OutputFormat::Text);
870 assert_eq!(backend.prompt_flag, None);
871 }
872
873 #[test]
874 fn test_claude_large_prompt_uses_stdin_not_temp_file() {
875 let backend = CliBackend::claude();
876 let large_prompt = "x".repeat(7001);
877 let (cmd, args, stdin, temp) = backend.build_command(&large_prompt, false);
878
879 assert_eq!(cmd, "claude");
880 assert!(args.contains(&"--print".to_string()));
881 assert_eq!(stdin, Some(large_prompt));
882 assert!(temp.is_none());
883 }
884
885 #[test]
888 fn test_claude_build_command_pty_uses_arg_mode() {
889 let backend = CliBackend::claude();
890 let large_prompt = "x".repeat(7001);
891 let (cmd, args, stdin, temp) = backend.build_command_pty(&large_prompt);
892
893 assert_eq!(cmd, "claude");
894 assert!(args.contains(&"--print".to_string()));
896 assert!(stdin.is_none(), "PTY mode should not use stdin");
898 assert!(
900 temp.is_some(),
901 "Large prompt in PTY mode should use temp file"
902 );
903 assert!(args.iter().any(|a| a.contains("Please read and execute")));
904 }
905
906 #[test]
907 fn test_claude_build_command_pty_small_prompt_uses_arg_directly() {
908 let backend = CliBackend::claude();
909 let (cmd, args, stdin, temp) = backend.build_command_pty("small prompt");
910
911 assert_eq!(cmd, "claude");
912 assert!(args.contains(&"--print".to_string()));
913 assert!(stdin.is_none());
914 assert!(temp.is_none());
915 assert!(args.contains(&"-p".to_string()));
917 assert!(args.contains(&"small prompt".to_string()));
918 }
919
920 #[test]
921 fn test_non_claude_large_prompt() {
922 let backend = CliBackend::kiro();
923 let large_prompt = "x".repeat(7001);
924 let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
925
926 assert_eq!(cmd, "kiro-cli");
927 assert!(temp.is_some());
928 assert!(args.iter().any(|a| a.contains("Please read and execute")));
929 }
930
931 #[test]
932 fn test_kiro_backend() {
933 let backend = CliBackend::kiro();
934 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
935
936 assert_eq!(cmd, "kiro-cli");
937 assert_eq!(
938 args,
939 vec![
940 "chat",
941 "--no-interactive",
942 "--trust-all-tools",
943 "test prompt"
944 ]
945 );
946 assert!(stdin.is_none());
947 }
948
949 #[test]
950 fn test_gemini_backend() {
951 let backend = CliBackend::gemini();
952 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
953
954 assert_eq!(cmd, "gemini");
955 assert_eq!(args, vec!["--yolo", "-p", "test prompt"]);
956 assert!(stdin.is_none());
957 }
958
959 #[test]
960 fn test_codex_backend() {
961 let backend = CliBackend::codex();
962 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
963
964 assert_eq!(cmd, "codex");
965 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
966 assert!(stdin.is_none());
967 }
968
969 #[test]
970 fn test_codex_large_prompt_uses_temp_file() {
971 let backend = CliBackend::codex();
972 let large_prompt = "x".repeat(7001);
973 let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
974
975 assert_eq!(cmd, "codex");
976 assert!(temp.is_some());
977 assert!(args.iter().any(|a| a.contains("Please read and execute")));
978 }
979
980 #[test]
981 fn test_amp_backend() {
982 let backend = CliBackend::amp();
983 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
984
985 assert_eq!(cmd, "amp");
986 assert_eq!(args, vec!["--dangerously-allow-all", "-x", "test prompt"]);
987 assert!(stdin.is_none());
988 }
989
990 #[test]
991 fn test_copilot_backend() {
992 let backend = CliBackend::copilot();
993 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
994
995 assert_eq!(cmd, "copilot");
996 assert_eq!(
997 args,
998 vec![
999 "--allow-all-tools",
1000 "--output-format",
1001 "json",
1002 "-p",
1003 "test prompt"
1004 ]
1005 );
1006 assert!(stdin.is_none());
1007 assert_eq!(backend.output_format, OutputFormat::CopilotStreamJson);
1008 }
1009
1010 #[test]
1011 fn test_copilot_tui_backend() {
1012 let backend = CliBackend::copilot_tui();
1013 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1014
1015 assert_eq!(cmd, "copilot");
1016 assert_eq!(args, vec!["test prompt"]);
1018 assert!(stdin.is_none());
1019 assert_eq!(backend.output_format, OutputFormat::Text);
1020 assert_eq!(backend.prompt_flag, None);
1021 }
1022
1023 #[test]
1024 fn test_from_config() {
1025 let config = CliConfig {
1026 backend: "claude".to_string(),
1027 command: None,
1028 prompt_mode: "arg".to_string(),
1029 ..Default::default()
1030 };
1031 let backend = CliBackend::from_config(&config).unwrap();
1032
1033 assert_eq!(backend.command, "claude");
1034 assert_eq!(backend.prompt_mode, PromptMode::Stdin);
1035 assert_eq!(backend.prompt_flag, None);
1036 assert!(backend.args.contains(&"--print".to_string()));
1037 }
1038
1039 #[test]
1040 fn test_from_config_command_override() {
1041 let config = CliConfig {
1042 backend: "claude".to_string(),
1043 command: Some("my-custom-claude".to_string()),
1044 prompt_mode: "arg".to_string(),
1045 ..Default::default()
1046 };
1047 let backend = CliBackend::from_config(&config).unwrap();
1048
1049 assert_eq!(backend.command, "my-custom-claude");
1050 assert_eq!(backend.prompt_flag, None);
1051 assert_eq!(backend.prompt_mode, PromptMode::Stdin);
1052 assert!(backend.args.contains(&"--print".to_string()));
1053 assert_eq!(backend.output_format, OutputFormat::StreamJson);
1054 }
1055
1056 #[test]
1057 fn test_kiro_interactive_mode_omits_no_interactive_flag() {
1058 let backend = CliBackend::kiro();
1059 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1060
1061 assert_eq!(cmd, "kiro-cli");
1062 assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
1063 assert!(stdin.is_none());
1064 assert!(!args.contains(&"--no-interactive".to_string()));
1065 }
1066
1067 #[test]
1068 fn test_codex_interactive_mode_omits_full_auto() {
1069 let backend = CliBackend::codex();
1070 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1071
1072 assert_eq!(cmd, "codex");
1073 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
1074 assert!(stdin.is_none());
1075 assert!(!args.contains(&"--full-auto".to_string()));
1076 }
1077
1078 #[test]
1079 fn test_amp_interactive_mode_no_flags() {
1080 let backend = CliBackend::amp();
1081 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1082
1083 assert_eq!(cmd, "amp");
1084 assert_eq!(args, vec!["-x", "test prompt"]);
1085 assert!(stdin.is_none());
1086 assert!(!args.contains(&"--dangerously-allow-all".to_string()));
1087 }
1088
1089 #[test]
1090 fn test_copilot_interactive_mode_omits_allow_all_tools() {
1091 let backend = CliBackend::copilot();
1092 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1093
1094 assert_eq!(cmd, "copilot");
1095 assert_eq!(args, vec!["--output-format", "json", "-p", "test prompt"]);
1096 assert!(stdin.is_none());
1097 assert!(!args.contains(&"--allow-all-tools".to_string()));
1098 }
1099
1100 #[test]
1101 fn test_claude_interactive_mode_omits_print() {
1102 let backend = CliBackend::claude();
1103 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
1104 let (_, args_interactive, stdin_interactive, _) =
1105 backend.build_command("test prompt", true);
1106
1107 assert_eq!(cmd, "claude");
1108 assert!(args_auto.contains(&"--print".to_string()));
1109 assert!(!args_interactive.contains(&"--print".to_string()));
1110 assert_eq!(
1111 args_interactive,
1112 vec![
1113 "--dangerously-skip-permissions",
1114 "--verbose",
1115 "--output-format",
1116 "stream-json",
1117 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
1118 ]
1119 );
1120 assert_eq!(stdin_auto, Some("test prompt".to_string()));
1121 assert_eq!(stdin_interactive, Some("test prompt".to_string()));
1122 }
1123
1124 #[test]
1125 fn test_gemini_interactive_mode_unchanged() {
1126 let backend = CliBackend::gemini();
1127 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
1128 let (_, args_interactive, stdin_interactive, _) =
1129 backend.build_command("test prompt", true);
1130
1131 assert_eq!(cmd, "gemini");
1132 assert_eq!(args_auto, args_interactive);
1133 assert_eq!(args_auto, vec!["--yolo", "-p", "test prompt"]);
1134 assert_eq!(stdin_auto, stdin_interactive);
1135 assert!(stdin_auto.is_none());
1136 }
1137
1138 #[test]
1139 fn test_custom_backend_with_prompt_flag_short() {
1140 let config = CliConfig {
1141 backend: "custom".to_string(),
1142 command: Some("my-agent".to_string()),
1143 prompt_mode: "arg".to_string(),
1144 prompt_flag: Some("-p".to_string()),
1145 ..Default::default()
1146 };
1147 let backend = CliBackend::from_config(&config).unwrap();
1148 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1149
1150 assert_eq!(cmd, "my-agent");
1151 assert_eq!(args, vec!["-p", "test prompt"]);
1152 assert!(stdin.is_none());
1153 }
1154
1155 #[test]
1156 fn test_custom_backend_with_prompt_flag_long() {
1157 let config = CliConfig {
1158 backend: "custom".to_string(),
1159 command: Some("my-agent".to_string()),
1160 prompt_mode: "arg".to_string(),
1161 prompt_flag: Some("--prompt".to_string()),
1162 ..Default::default()
1163 };
1164 let backend = CliBackend::from_config(&config).unwrap();
1165 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1166
1167 assert_eq!(cmd, "my-agent");
1168 assert_eq!(args, vec!["--prompt", "test prompt"]);
1169 assert!(stdin.is_none());
1170 }
1171
1172 #[test]
1173 fn test_custom_backend_without_prompt_flag_positional() {
1174 let config = CliConfig {
1175 backend: "custom".to_string(),
1176 command: Some("my-agent".to_string()),
1177 prompt_mode: "arg".to_string(),
1178 prompt_flag: None,
1179 ..Default::default()
1180 };
1181 let backend = CliBackend::from_config(&config).unwrap();
1182 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1183
1184 assert_eq!(cmd, "my-agent");
1185 assert_eq!(args, vec!["test prompt"]);
1186 assert!(stdin.is_none());
1187 }
1188
1189 #[test]
1190 fn test_custom_backend_without_command_returns_error() {
1191 let config = CliConfig {
1192 backend: "custom".to_string(),
1193 command: None,
1194 prompt_mode: "arg".to_string(),
1195 ..Default::default()
1196 };
1197 let result = CliBackend::from_config(&config);
1198
1199 assert!(result.is_err());
1200 let err = result.unwrap_err();
1201 assert_eq!(
1202 err.to_string(),
1203 "custom backend requires a command to be specified"
1204 );
1205 }
1206
1207 #[test]
1208 fn test_kiro_with_agent() {
1209 let backend = CliBackend::kiro_with_agent("my-agent".to_string(), &[]);
1210 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1211
1212 assert_eq!(cmd, "kiro-cli");
1213 assert_eq!(
1214 args,
1215 vec![
1216 "chat",
1217 "--no-interactive",
1218 "--trust-all-tools",
1219 "--agent",
1220 "my-agent",
1221 "test prompt"
1222 ]
1223 );
1224 assert!(stdin.is_none());
1225 }
1226
1227 #[test]
1228 fn test_kiro_with_agent_extra_args() {
1229 let extra_args = vec!["--verbose".to_string(), "--debug".to_string()];
1230 let backend = CliBackend::kiro_with_agent("my-agent".to_string(), &extra_args);
1231 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1232
1233 assert_eq!(cmd, "kiro-cli");
1234 assert_eq!(
1235 args,
1236 vec![
1237 "chat",
1238 "--no-interactive",
1239 "--trust-all-tools",
1240 "--agent",
1241 "my-agent",
1242 "--verbose",
1243 "--debug",
1244 "test prompt"
1245 ]
1246 );
1247 assert!(stdin.is_none());
1248 }
1249
1250 #[test]
1251 fn test_from_name_claude() {
1252 let backend = CliBackend::from_name("claude").unwrap();
1253 assert_eq!(backend.command, "claude");
1254 assert_eq!(backend.prompt_mode, PromptMode::Stdin);
1255 assert_eq!(backend.prompt_flag, None);
1256 assert!(backend.args.contains(&"--print".to_string()));
1257 }
1258
1259 #[test]
1260 fn test_from_name_kiro() {
1261 let backend = CliBackend::from_name("kiro").unwrap();
1262 assert_eq!(backend.command, "kiro-cli");
1263 }
1264
1265 #[test]
1266 fn test_from_name_gemini() {
1267 let backend = CliBackend::from_name("gemini").unwrap();
1268 assert_eq!(backend.command, "gemini");
1269 }
1270
1271 #[test]
1272 fn test_from_name_codex() {
1273 let backend = CliBackend::from_name("codex").unwrap();
1274 assert_eq!(backend.command, "codex");
1275 }
1276
1277 #[test]
1278 fn test_from_name_amp() {
1279 let backend = CliBackend::from_name("amp").unwrap();
1280 assert_eq!(backend.command, "amp");
1281 }
1282
1283 #[test]
1284 fn test_from_name_copilot() {
1285 let backend = CliBackend::from_name("copilot").unwrap();
1286 assert_eq!(backend.command, "copilot");
1287 assert_eq!(backend.prompt_flag, Some("-p".to_string()));
1288 }
1289
1290 #[test]
1291 fn test_from_name_invalid() {
1292 let result = CliBackend::from_name("invalid");
1293 assert!(result.is_err());
1294 }
1295
1296 #[test]
1297 fn test_from_hat_backend_named() {
1298 let hat_backend = HatBackend::Named("claude".to_string());
1299 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1300 assert_eq!(backend.command, "claude");
1301 }
1302
1303 #[test]
1304 fn test_from_hat_backend_kiro_agent() {
1305 let hat_backend = HatBackend::KiroAgent {
1306 backend_type: "kiro".to_string(),
1307 agent: "my-agent".to_string(),
1308 args: vec![],
1309 };
1310 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1311 let (cmd, args, _, _) = backend.build_command("test", false);
1312 assert_eq!(cmd, "kiro-cli");
1313 assert!(args.contains(&"--agent".to_string()));
1314 assert!(args.contains(&"my-agent".to_string()));
1315 }
1316
1317 #[test]
1318 fn test_from_hat_backend_kiro_acp_agent_uses_acp_executor() {
1319 let hat_backend = HatBackend::KiroAgent {
1320 backend_type: "kiro-acp".to_string(),
1321 agent: "my-agent".to_string(),
1322 args: vec![],
1323 };
1324 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1325 assert_eq!(backend.command, "kiro-cli");
1326 assert_eq!(backend.output_format, OutputFormat::Acp);
1327 assert!(backend.args.contains(&"acp".to_string()));
1328 assert!(backend.args.contains(&"--agent".to_string()));
1329 assert!(backend.args.contains(&"my-agent".to_string()));
1330 }
1331
1332 #[test]
1333 fn test_from_hat_backend_kiro_agent_with_args() {
1334 let hat_backend = HatBackend::KiroAgent {
1335 backend_type: "kiro".to_string(),
1336 agent: "my-agent".to_string(),
1337 args: vec!["--verbose".to_string()],
1338 };
1339 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1340 let (cmd, args, _, _) = backend.build_command("test", false);
1341 assert_eq!(cmd, "kiro-cli");
1342 assert!(args.contains(&"--agent".to_string()));
1343 assert!(args.contains(&"my-agent".to_string()));
1344 assert!(args.contains(&"--verbose".to_string()));
1345 }
1346
1347 #[test]
1348 fn test_from_hat_backend_named_with_args() {
1349 let hat_backend = HatBackend::NamedWithArgs {
1350 backend_type: "claude".to_string(),
1351 args: vec!["--model".to_string(), "claude-sonnet-4".to_string()],
1352 };
1353 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1354 assert_eq!(backend.command, "claude");
1355 assert!(backend.args.contains(&"--model".to_string()));
1356 assert!(backend.args.contains(&"claude-sonnet-4".to_string()));
1357 }
1358
1359 #[test]
1360 fn test_codex_named_with_args_dangerous_bypass_normalizes_to_yolo() {
1361 let hat_backend = HatBackend::NamedWithArgs {
1362 backend_type: "codex".to_string(),
1363 args: vec!["--dangerously-bypass-approvals-and-sandbox".to_string()],
1364 };
1365 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1366 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1367
1368 assert_eq!(cmd, "codex");
1369 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
1370 }
1371
1372 #[test]
1373 fn test_codex_named_with_args_yolo_removes_full_auto() {
1374 let hat_backend = HatBackend::NamedWithArgs {
1375 backend_type: "codex".to_string(),
1376 args: vec!["--yolo".to_string()],
1377 };
1378 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1379 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1380
1381 assert_eq!(cmd, "codex");
1382 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
1383 }
1384
1385 #[test]
1386 fn test_from_hat_backend_custom() {
1387 let hat_backend = HatBackend::Custom {
1388 command: "my-cli".to_string(),
1389 args: vec!["--flag".to_string()],
1390 };
1391 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1392 assert_eq!(backend.command, "my-cli");
1393 assert_eq!(backend.args, vec!["--flag"]);
1394 }
1395
1396 #[test]
1401 fn test_for_interactive_prompt_claude() {
1402 let backend = CliBackend::for_interactive_prompt("claude").unwrap();
1403 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1404
1405 assert_eq!(cmd, "claude");
1406 assert_eq!(
1408 args,
1409 vec![
1410 "--dangerously-skip-permissions",
1411 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
1412 "test prompt"
1413 ]
1414 );
1415 assert!(stdin.is_none());
1416 assert_eq!(backend.prompt_flag, None);
1417 }
1418
1419 #[test]
1420 fn test_for_interactive_prompt_kiro() {
1421 let backend = CliBackend::for_interactive_prompt("kiro").unwrap();
1422 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1423
1424 assert_eq!(cmd, "kiro-cli");
1425 assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
1427 assert!(!args.contains(&"--no-interactive".to_string()));
1428 assert!(stdin.is_none());
1429 }
1430
1431 #[test]
1432 fn test_for_interactive_prompt_gemini() {
1433 let backend = CliBackend::for_interactive_prompt("gemini").unwrap();
1434 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1435
1436 assert_eq!(cmd, "gemini");
1437 assert_eq!(args, vec!["--yolo", "-i", "test prompt"]);
1439 assert_eq!(backend.prompt_flag, Some("-i".to_string()));
1440 assert!(stdin.is_none());
1441 }
1442
1443 #[test]
1444 fn test_for_interactive_prompt_codex() {
1445 let backend = CliBackend::for_interactive_prompt("codex").unwrap();
1446 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1447
1448 assert_eq!(cmd, "codex");
1449 assert_eq!(args, vec!["test prompt"]);
1451 assert!(!args.contains(&"exec".to_string()));
1452 assert!(!args.contains(&"--full-auto".to_string()));
1453 assert!(stdin.is_none());
1454 }
1455
1456 #[test]
1457 fn test_for_interactive_prompt_amp() {
1458 let backend = CliBackend::for_interactive_prompt("amp").unwrap();
1459 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1460
1461 assert_eq!(cmd, "amp");
1462 assert_eq!(args, vec!["-x", "test prompt"]);
1464 assert!(!args.contains(&"--dangerously-allow-all".to_string()));
1465 assert!(stdin.is_none());
1466 }
1467
1468 #[test]
1469 fn test_for_interactive_prompt_copilot() {
1470 let backend = CliBackend::for_interactive_prompt("copilot").unwrap();
1471 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1472
1473 assert_eq!(cmd, "copilot");
1474 assert_eq!(args, vec!["-p", "test prompt"]);
1476 assert!(!args.contains(&"--allow-all-tools".to_string()));
1477 assert!(stdin.is_none());
1478 }
1479
1480 #[test]
1481 fn test_for_interactive_prompt_invalid() {
1482 let result = CliBackend::for_interactive_prompt("invalid_backend");
1483 assert!(result.is_err());
1484 }
1485
1486 #[test]
1491 fn test_opencode_backend() {
1492 let backend = CliBackend::opencode();
1493 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1494
1495 assert_eq!(cmd, "opencode");
1496 assert_eq!(args, vec!["run", "test prompt"]);
1498 assert!(stdin.is_none());
1499 assert_eq!(backend.output_format, OutputFormat::Text);
1500 assert_eq!(backend.prompt_flag, None);
1501 }
1502
1503 #[test]
1504 fn test_opencode_tui_backend() {
1505 let backend = CliBackend::opencode_tui();
1506 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1507
1508 assert_eq!(cmd, "opencode");
1509 assert_eq!(args, vec!["run", "test prompt"]);
1511 assert!(stdin.is_none());
1512 assert_eq!(backend.output_format, OutputFormat::Text);
1513 assert_eq!(backend.prompt_flag, None);
1514 }
1515
1516 #[test]
1517 fn test_opencode_interactive_mode_unchanged() {
1518 let backend = CliBackend::opencode();
1520 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
1521 let (_, args_interactive, stdin_interactive, _) =
1522 backend.build_command("test prompt", true);
1523
1524 assert_eq!(cmd, "opencode");
1525 assert_eq!(args_auto, args_interactive);
1527 assert_eq!(args_auto, vec!["run", "test prompt"]);
1528 assert!(stdin_auto.is_none());
1529 assert!(stdin_interactive.is_none());
1530 }
1531
1532 #[test]
1533 fn test_from_name_opencode() {
1534 let backend = CliBackend::from_name("opencode").unwrap();
1535 assert_eq!(backend.command, "opencode");
1536 assert_eq!(backend.prompt_flag, None); }
1538
1539 #[test]
1540 fn test_for_interactive_prompt_opencode() {
1541 let backend = CliBackend::for_interactive_prompt("opencode").unwrap();
1542 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1543
1544 assert_eq!(cmd, "opencode");
1545 assert_eq!(args, vec!["--prompt", "test prompt"]);
1547 assert!(stdin.is_none());
1548 assert_eq!(backend.prompt_flag, Some("--prompt".to_string()));
1549 }
1550
1551 #[test]
1552 fn test_opencode_interactive_launches_tui_not_headless() {
1553 let backend = CliBackend::opencode_interactive();
1563 let (cmd, args, _, _) = backend.build_command("test prompt", true);
1564
1565 assert_eq!(cmd, "opencode");
1566 assert!(
1569 !args.contains(&"run".to_string()),
1570 "opencode_interactive() should not use 'run' subcommand. \
1571 'opencode run' is headless mode, but interactive mode needs TUI. \
1572 Expected: opencode --prompt \"test prompt\", got: opencode {}",
1573 args.join(" ")
1574 );
1575 assert!(
1577 args.contains(&"--prompt".to_string()),
1578 "opencode_interactive() should use --prompt flag for TUI mode. \
1579 Expected args to contain '--prompt', got: {:?}",
1580 args
1581 );
1582 }
1583
1584 #[test]
1589 fn test_pi_backend() {
1590 let backend = CliBackend::pi();
1591 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1592
1593 assert_eq!(cmd, "pi");
1594 assert_eq!(
1595 args,
1596 vec!["-p", "--mode", "json", "--no-session", "test prompt"]
1597 );
1598 assert!(stdin.is_none());
1599 assert_eq!(backend.output_format, OutputFormat::PiStreamJson);
1600 assert_eq!(backend.prompt_flag, None); }
1602
1603 #[test]
1604 fn test_pi_interactive_backend() {
1605 let backend = CliBackend::pi_interactive();
1606 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1607
1608 assert_eq!(cmd, "pi");
1609 assert_eq!(args, vec!["--no-session", "test prompt"]);
1611 assert!(stdin.is_none());
1612 assert_eq!(backend.output_format, OutputFormat::Text);
1613 assert_eq!(backend.prompt_flag, None);
1614 }
1615
1616 #[test]
1617 fn test_from_name_pi() {
1618 let backend = CliBackend::from_name("pi").unwrap();
1619 assert_eq!(backend.command, "pi");
1620 assert_eq!(backend.prompt_flag, None);
1621 assert_eq!(backend.output_format, OutputFormat::PiStreamJson);
1622 }
1623
1624 #[test]
1625 fn test_for_interactive_prompt_pi() {
1626 let backend = CliBackend::for_interactive_prompt("pi").unwrap();
1627 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1628
1629 assert_eq!(cmd, "pi");
1630 assert_eq!(args, vec!["--no-session", "test prompt"]);
1631 assert!(stdin.is_none());
1632 assert_eq!(backend.output_format, OutputFormat::Text);
1633 }
1634
1635 #[test]
1636 fn test_from_config_pi() {
1637 let config = CliConfig {
1638 backend: "pi".to_string(),
1639 command: None,
1640 prompt_mode: "arg".to_string(),
1641 args: vec![
1642 "--provider".to_string(),
1643 "zai".to_string(),
1644 "--model".to_string(),
1645 "glm-5".to_string(),
1646 ],
1647 ..Default::default()
1648 };
1649 let backend = CliBackend::from_config(&config).unwrap();
1650 let (_cmd, args, _stdin, _temp) = backend.build_command("test prompt", false);
1651
1652 assert_eq!(backend.command, "pi");
1653 assert_eq!(backend.output_format, OutputFormat::PiStreamJson);
1654 assert!(args.contains(&"--provider".to_string()));
1655 assert!(args.contains(&"zai".to_string()));
1656 assert!(args.contains(&"--model".to_string()));
1657 assert!(args.contains(&"glm-5".to_string()));
1658 }
1659
1660 #[test]
1661 fn test_from_hat_backend_named_with_args_pi() {
1662 let hat_backend = HatBackend::NamedWithArgs {
1663 backend_type: "pi".to_string(),
1664 args: vec![
1665 "--provider".to_string(),
1666 "anthropic".to_string(),
1667 "--model".to_string(),
1668 "claude-sonnet-4".to_string(),
1669 ],
1670 };
1671 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1672 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1673
1674 assert_eq!(cmd, "pi");
1675 assert!(args.contains(&"-p".to_string()));
1677 assert!(args.contains(&"--mode".to_string()));
1678 assert!(args.contains(&"json".to_string()));
1679 assert!(args.contains(&"--no-session".to_string()));
1680 assert!(args.contains(&"--provider".to_string()));
1681 assert!(args.contains(&"anthropic".to_string()));
1682 assert!(args.contains(&"--model".to_string()));
1683 assert!(args.contains(&"claude-sonnet-4".to_string()));
1684 assert!(args.contains(&"test prompt".to_string()));
1685 }
1686
1687 #[test]
1688 fn test_pi_large_prompt_uses_temp_file() {
1689 let backend = CliBackend::pi();
1690 let large_prompt = "x".repeat(7001);
1691 let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
1692
1693 assert_eq!(cmd, "pi");
1694 assert!(temp.is_some());
1695 assert!(args.iter().any(|a| a.contains("Please read and execute")));
1696 }
1697
1698 #[test]
1699 fn test_pi_interactive_mode_unchanged() {
1700 let backend = CliBackend::pi();
1702 let (_, args_auto, _, _) = backend.build_command("test prompt", false);
1703 let (_, args_interactive, _, _) = backend.build_command("test prompt", true);
1704
1705 assert_eq!(args_auto, args_interactive);
1706 }
1707
1708 #[test]
1709 fn test_custom_args_can_be_appended() {
1710 let mut backend = CliBackend::opencode();
1713
1714 let custom_args = vec!["--model=gpt-4".to_string(), "--temperature=0.7".to_string()];
1716 backend.args.extend(custom_args.clone());
1717
1718 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1720
1721 assert_eq!(cmd, "opencode");
1722 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();
1730 let model_idx = args.iter().position(|a| a == "--model=gpt-4").unwrap();
1731 assert!(
1732 run_idx < model_idx,
1733 "Original args should come before custom args"
1734 );
1735 }
1736
1737 #[test]
1742 fn test_claude_interactive_teams_backend() {
1743 let backend = CliBackend::claude_interactive_teams();
1744 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1745
1746 assert_eq!(cmd, "claude");
1747 assert_eq!(
1748 args,
1749 vec![
1750 "--dangerously-skip-permissions",
1751 "--disallowedTools=TodoWrite",
1752 "test prompt"
1753 ]
1754 );
1755 assert!(stdin.is_none());
1756 assert_eq!(backend.output_format, OutputFormat::Text);
1757 assert_eq!(backend.prompt_flag, None);
1758 assert_eq!(
1759 backend.env_vars,
1760 vec![(
1761 "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS".to_string(),
1762 "1".to_string()
1763 )]
1764 );
1765 }
1766
1767 #[test]
1768 fn test_env_vars_default_empty() {
1769 assert!(CliBackend::claude().env_vars.is_empty());
1771 assert!(CliBackend::claude_interactive().env_vars.is_empty());
1772 assert!(CliBackend::kiro().env_vars.is_empty());
1773 assert!(CliBackend::gemini().env_vars.is_empty());
1774 assert!(CliBackend::codex().env_vars.is_empty());
1775 assert!(CliBackend::amp().env_vars.is_empty());
1776 assert!(CliBackend::copilot().env_vars.is_empty());
1777 assert!(CliBackend::opencode().env_vars.is_empty());
1778 assert!(CliBackend::pi().env_vars.is_empty());
1779 assert!(CliBackend::roo().env_vars.is_empty());
1780 }
1781
1782 #[test]
1787 fn test_roo_backend() {
1788 let backend = CliBackend::roo();
1789 let (cmd, args, stdin, temp) = backend.build_command("test prompt", false);
1790
1791 assert_eq!(cmd, "roo");
1792 assert!(
1794 temp.is_some(),
1795 "roo should always use temp file for prompts"
1796 );
1797 assert!(
1798 args.contains(&"--print".to_string()),
1799 "roo headless should have --print"
1800 );
1801 assert!(
1802 args.contains(&"--ephemeral".to_string()),
1803 "roo headless should have --ephemeral"
1804 );
1805 assert!(
1806 args.contains(&"--prompt-file".to_string()),
1807 "roo should use --prompt-file"
1808 );
1809 assert!(stdin.is_none());
1810 assert_eq!(backend.output_format, OutputFormat::Text);
1811 }
1812
1813 #[test]
1814 fn test_roo_interactive() {
1815 let backend = CliBackend::roo_interactive();
1816 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1817
1818 assert_eq!(cmd, "roo");
1819 assert_eq!(args, vec!["test prompt"]);
1821 assert!(stdin.is_none());
1822 assert_eq!(backend.output_format, OutputFormat::Text);
1823 assert_eq!(backend.prompt_flag, None);
1824 }
1825
1826 #[test]
1827 fn test_from_name_roo() {
1828 let backend = CliBackend::from_name("roo").unwrap();
1829 assert_eq!(backend.command, "roo");
1830 assert_eq!(backend.prompt_flag, None);
1831 assert_eq!(backend.output_format, OutputFormat::Text);
1832 }
1833
1834 #[test]
1835 fn test_from_config_roo() {
1836 let config = CliConfig {
1837 backend: "roo".to_string(),
1838 command: None,
1839 prompt_mode: "arg".to_string(),
1840 ..Default::default()
1841 };
1842 let backend = CliBackend::from_config(&config).unwrap();
1843
1844 assert_eq!(backend.command, "roo");
1845 assert_eq!(backend.output_format, OutputFormat::Text);
1846 assert!(backend.args.contains(&"--print".to_string()));
1847 assert!(backend.args.contains(&"--ephemeral".to_string()));
1848 }
1849
1850 #[test]
1851 fn test_from_config_roo_with_args() {
1852 let config = CliConfig {
1853 backend: "roo".to_string(),
1854 command: None,
1855 prompt_mode: "arg".to_string(),
1856 args: vec![
1857 "--provider".to_string(),
1858 "bedrock".to_string(),
1859 "--model".to_string(),
1860 "anthropic.claude-sonnet-4-6".to_string(),
1861 ],
1862 ..Default::default()
1863 };
1864 let backend = CliBackend::from_config(&config).unwrap();
1865 let (_cmd, args, _stdin, _temp) = backend.build_command("test prompt", false);
1866
1867 assert_eq!(backend.command, "roo");
1868 assert!(args.contains(&"--print".to_string()));
1870 assert!(args.contains(&"--ephemeral".to_string()));
1871 assert!(args.contains(&"--provider".to_string()));
1872 assert!(args.contains(&"bedrock".to_string()));
1873 assert!(args.contains(&"--model".to_string()));
1874 assert!(args.contains(&"anthropic.claude-sonnet-4-6".to_string()));
1875 assert!(args.contains(&"--prompt-file".to_string()));
1876 }
1877
1878 #[test]
1879 fn test_for_interactive_prompt_roo() {
1880 let backend = CliBackend::for_interactive_prompt("roo").unwrap();
1881 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1882
1883 assert_eq!(cmd, "roo");
1884 assert_eq!(args, vec!["test prompt"]);
1886 assert!(stdin.is_none());
1887 assert_eq!(backend.output_format, OutputFormat::Text);
1888 }
1889
1890 #[test]
1891 fn test_roo_interactive_mode_removes_print() {
1892 let backend = CliBackend::roo();
1893 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
1894
1895 assert_eq!(cmd, "roo");
1896 assert!(
1898 !args.contains(&"--print".to_string()),
1899 "interactive mode should remove --print"
1900 );
1901 assert!(
1902 !args.contains(&"--ephemeral".to_string()),
1903 "interactive mode should remove --ephemeral"
1904 );
1905 assert!(stdin.is_none());
1906 }
1907
1908 #[test]
1909 fn test_roo_uses_prompt_file() {
1910 let backend = CliBackend::roo();
1911 let (_, args_small, _, temp_small) = backend.build_command("small prompt", false);
1913 assert!(
1914 temp_small.is_some(),
1915 "even small prompts should use temp file"
1916 );
1917 assert!(
1918 args_small.contains(&"--prompt-file".to_string()),
1919 "should use --prompt-file"
1920 );
1921
1922 let large_prompt = "x".repeat(10000);
1924 let (_, args_large, _, temp_large) = backend.build_command(&large_prompt, false);
1925 assert!(temp_large.is_some(), "large prompts should use temp file");
1926 assert!(
1927 args_large.contains(&"--prompt-file".to_string()),
1928 "should use --prompt-file for large prompts"
1929 );
1930 }
1931
1932 #[test]
1933 fn test_roo_prompt_file_content() {
1934 use std::io::{Read, Seek};
1935 let backend = CliBackend::roo();
1936 let prompt = "This is a test prompt for roo";
1937 let (_, _, _, temp) = backend.build_command(prompt, false);
1938
1939 let mut temp_file = temp.expect("should have temp file");
1940 let mut content = String::new();
1941 temp_file
1942 .as_file_mut()
1943 .seek(std::io::SeekFrom::Start(0))
1944 .unwrap();
1945 temp_file
1946 .as_file_mut()
1947 .read_to_string(&mut content)
1948 .unwrap();
1949 assert_eq!(content, prompt);
1950 }
1951}