1use crate::Codex;
2use crate::command::CodexCommand;
3#[cfg(feature = "json")]
4use crate::error::Error;
5use crate::error::Result;
6use crate::exec::{self, CommandOutput};
7use crate::rollout_budget::RolloutBudgetConfig;
8use crate::types::{ApprovalPolicyConfig, Color, SandboxMode, WebSearchMode};
9#[cfg(feature = "json")]
10use crate::types::{JsonLineEvent, QueryResult};
11
12pub(crate) fn push_typed_config(
19 args: &mut Vec<String>,
20 approval_policy: Option<ApprovalPolicyConfig>,
21 web_search: Option<WebSearchMode>,
22) {
23 if let Some(policy) = approval_policy {
24 args.push("-c".into());
25 args.push(format!("approval_policy=\"{}\"", policy.as_config_value()));
26 }
27 if let Some(mode) = web_search {
28 args.push("-c".into());
29 args.push(format!("web_search=\"{}\"", mode.as_config_value()));
30 }
31}
32
33pub(crate) fn effective_sandbox(
39 sandbox: Option<SandboxMode>,
40 full_auto: bool,
41) -> Option<SandboxMode> {
42 sandbox.or(full_auto.then_some(SandboxMode::WorkspaceWrite))
43}
44
45#[derive(Debug, Clone)]
69pub struct ExecCommand {
70 approve_for_me: bool,
71 prompt: Option<String>,
72 prompt_via_stdin: bool,
73 approval_policy: Option<ApprovalPolicyConfig>,
74 web_search: Option<WebSearchMode>,
75 config_overrides: Vec<String>,
76 enabled_features: Vec<String>,
77 disabled_features: Vec<String>,
78 rollout_budget: Option<RolloutBudgetConfig>,
79 images: Vec<String>,
80 model: Option<String>,
81 oss: bool,
82 local_provider: Option<String>,
83 sandbox: Option<SandboxMode>,
84 strict_config: bool,
85 dangerously_bypass_hook_trust: bool,
86 ignore_user_config: bool,
87 ignore_rules: bool,
88 profile: Option<String>,
89 full_auto: bool,
90 dangerously_bypass_approvals_and_sandbox: bool,
91 cd: Option<String>,
92 skip_git_repo_check: bool,
93 add_dirs: Vec<String>,
94 ephemeral: bool,
95 output_schema: Option<String>,
96 color: Option<Color>,
97 json: bool,
98 output_last_message: Option<String>,
99 retry_policy: Option<crate::retry::RetryPolicy>,
100}
101
102impl ExecCommand {
103 #[must_use]
105 pub fn new(prompt: impl Into<String>) -> Self {
106 Self {
107 approve_for_me: false,
108 prompt: Some(prompt.into()),
109 prompt_via_stdin: false,
110 approval_policy: None,
111 web_search: None,
112 config_overrides: Vec::new(),
113 enabled_features: Vec::new(),
114 disabled_features: Vec::new(),
115 rollout_budget: None,
116 images: Vec::new(),
117 model: None,
118 oss: false,
119 local_provider: None,
120 sandbox: None,
121 strict_config: false,
122 dangerously_bypass_hook_trust: false,
123 ignore_user_config: false,
124 ignore_rules: false,
125 profile: None,
126 full_auto: false,
127 dangerously_bypass_approvals_and_sandbox: false,
128 cd: None,
129 skip_git_repo_check: false,
130 add_dirs: Vec::new(),
131 ephemeral: false,
132 output_schema: None,
133 color: None,
134 json: false,
135 output_last_message: None,
136 retry_policy: None,
137 }
138 }
139
140 #[must_use]
164 pub fn from_stdin(prompt: impl Into<String>) -> Self {
165 Self::new(prompt).prompt_via_stdin()
166 }
167
168 #[must_use]
179 pub fn prompt_via_stdin(mut self) -> Self {
180 self.prompt_via_stdin = true;
181 self
182 }
183
184 #[cfg(feature = "json")]
189 pub(crate) fn stdin_prompt(&self) -> Option<&str> {
190 self.prompt_via_stdin
191 .then(|| self.prompt.as_deref().unwrap_or_default())
192 }
193
194 #[must_use]
201 pub fn config(mut self, key_value: impl Into<String>) -> Self {
202 self.config_overrides.push(key_value.into());
203 self
204 }
205
206 #[must_use]
222 pub fn approval_policy(mut self, policy: impl Into<ApprovalPolicyConfig>) -> Self {
223 self.approval_policy = Some(policy.into());
224 self
225 }
226
227 #[must_use]
232 pub fn search(self) -> Self {
233 self.search_mode(WebSearchMode::Live)
234 }
235
236 #[must_use]
242 pub fn search_mode(mut self, mode: WebSearchMode) -> Self {
243 self.web_search = Some(mode);
244 self
245 }
246
247 #[must_use]
251 pub fn enable(mut self, feature: impl Into<String>) -> Self {
252 self.enabled_features.push(feature.into());
253 self
254 }
255
256 #[must_use]
260 pub fn disable(mut self, feature: impl Into<String>) -> Self {
261 self.disabled_features.push(feature.into());
262 self
263 }
264
265 #[must_use]
280 pub fn rollout_budget(mut self, budget: RolloutBudgetConfig) -> Self {
281 self.rollout_budget = Some(budget);
282 self
283 }
284
285 #[must_use]
289 pub fn image(mut self, path: impl Into<String>) -> Self {
290 self.images.push(path.into());
291 self
292 }
293
294 #[must_use]
298 pub fn model(mut self, model: impl Into<String>) -> Self {
299 let model = model.into();
300 assert!(!model.is_empty(), "model name must not be empty");
301 self.model = Some(model);
302 self
303 }
304
305 #[must_use]
307 pub fn oss(mut self) -> Self {
308 self.oss = true;
309 self
310 }
311
312 #[must_use]
314 pub fn local_provider(mut self, provider: impl Into<String>) -> Self {
315 self.local_provider = Some(provider.into());
316 self
317 }
318
319 #[must_use]
321 pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
322 self.sandbox = Some(sandbox);
323 self
324 }
325
326 #[must_use]
328 pub fn strict_config(mut self) -> Self {
329 self.strict_config = true;
330 self
331 }
332
333 #[must_use]
337 pub(crate) fn set_bypass_hook_trust(mut self) -> Self {
338 self.dangerously_bypass_hook_trust = true;
339 self
340 }
341
342 #[must_use]
344 pub fn ignore_user_config(mut self) -> Self {
345 self.ignore_user_config = true;
346 self
347 }
348
349 #[must_use]
351 pub fn ignore_rules(mut self) -> Self {
352 self.ignore_rules = true;
353 self
354 }
355
356 #[must_use]
358 pub fn profile(mut self, profile: impl Into<String>) -> Self {
359 self.profile = Some(profile.into());
360 self
361 }
362
363 #[must_use]
375 pub fn full_auto(mut self) -> Self {
376 self.full_auto = true;
377 self
378 }
379
380 #[must_use]
388 pub fn approve_for_me(mut self) -> Self {
389 self.approve_for_me = true;
390 self
391 }
392
393 #[must_use]
397 pub(crate) fn set_bypass_approvals_and_sandbox(mut self) -> Self {
398 self.dangerously_bypass_approvals_and_sandbox = true;
399 self
400 }
401
402 #[must_use]
404 pub fn cd(mut self, dir: impl Into<String>) -> Self {
405 self.cd = Some(dir.into());
406 self
407 }
408
409 #[must_use]
411 pub fn skip_git_repo_check(mut self) -> Self {
412 self.skip_git_repo_check = true;
413 self
414 }
415
416 #[must_use]
420 pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
421 self.add_dirs.push(dir.into());
422 self
423 }
424
425 #[must_use]
427 pub fn ephemeral(mut self) -> Self {
428 self.ephemeral = true;
429 self
430 }
431
432 #[must_use]
434 pub fn output_schema(mut self, path: impl Into<String>) -> Self {
435 self.output_schema = Some(path.into());
436 self
437 }
438
439 #[must_use]
441 pub fn color(mut self, color: Color) -> Self {
442 self.color = Some(color);
443 self
444 }
445
446 #[must_use]
452 pub fn json(mut self) -> Self {
453 self.json = true;
454 self
455 }
456
457 #[must_use]
459 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
460 self.output_last_message = Some(path.into());
461 self
462 }
463
464 #[must_use]
468 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
469 self.retry_policy = Some(policy);
470 self
471 }
472
473 #[cfg(feature = "json")]
496 pub async fn stream<F>(&self, codex: &Codex, handler: F) -> Result<()>
497 where
498 F: FnMut(JsonLineEvent),
499 {
500 crate::streaming::stream_exec(codex, self, handler).await
501 }
502
503 #[cfg(feature = "json")]
508 pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
509 let mut args = self.args();
510 if !self.json {
511 args.push("--json".into());
512 }
513
514 let output = if self.prompt_via_stdin {
515 let prompt = self.prompt.as_deref().unwrap_or_default();
516 exec::run_codex_with_stdin_prompt(codex, args, prompt).await?
517 } else {
518 exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?
519 };
520 parse_json_lines(&output.stdout)
521 }
522
523 #[cfg(feature = "json")]
529 pub async fn execute_json(&self, codex: &Codex) -> Result<QueryResult> {
530 let events = self.execute_json_lines(codex).await?;
531 Ok(QueryResult::from_events(events))
532 }
533}
534
535impl CodexCommand for ExecCommand {
536 type Output = CommandOutput;
537
538 fn args(&self) -> Vec<String> {
539 let mut args = vec!["exec".to_string()];
540
541 push_typed_config(&mut args, self.approval_policy, self.web_search);
542 push_repeat(&mut args, "-c", &self.config_overrides);
543 push_feature_toggles(
544 &mut args,
545 &self.enabled_features,
546 &self.disabled_features,
547 self.rollout_budget.is_some(),
548 );
549 if let Some(budget) = &self.rollout_budget {
550 args.push("-c".into());
551 args.push(budget.config_override());
552 }
553 push_repeat(&mut args, "--image", &self.images);
554
555 if let Some(model) = &self.model {
556 args.push("--model".into());
557 args.push(model.clone());
558 }
559 if self.oss {
560 args.push("--oss".into());
561 }
562 if let Some(local_provider) = &self.local_provider {
563 args.push("--local-provider".into());
564 args.push(local_provider.clone());
565 }
566 if let Some(sandbox) = effective_sandbox(self.sandbox, self.full_auto) {
567 args.push("--sandbox".into());
568 args.push(sandbox.as_arg().into());
569 }
570 if self.strict_config {
571 args.push("--strict-config".into());
572 }
573 if let Some(profile) = &self.profile {
574 args.push("--profile".into());
575 args.push(profile.clone());
576 }
577 if self.approve_for_me {
578 args.push("--approve-for-me".into());
579 }
580 if self.dangerously_bypass_approvals_and_sandbox {
581 args.push("--dangerously-bypass-approvals-and-sandbox".into());
582 }
583 if self.dangerously_bypass_hook_trust {
584 args.push("--dangerously-bypass-hook-trust".into());
585 }
586 if let Some(cd) = &self.cd {
587 args.push("--cd".into());
588 args.push(cd.clone());
589 }
590 if self.skip_git_repo_check {
591 args.push("--skip-git-repo-check".into());
592 }
593 push_repeat(&mut args, "--add-dir", &self.add_dirs);
594 if self.ephemeral {
595 args.push("--ephemeral".into());
596 }
597 if self.ignore_user_config {
598 args.push("--ignore-user-config".into());
599 }
600 if self.ignore_rules {
601 args.push("--ignore-rules".into());
602 }
603 if let Some(output_schema) = &self.output_schema {
604 args.push("--output-schema".into());
605 args.push(output_schema.clone());
606 }
607 if let Some(color) = self.color {
608 args.push("--color".into());
609 args.push(color.as_arg().into());
610 }
611 if self.json {
612 args.push("--json".into());
613 }
614 if let Some(path) = &self.output_last_message {
615 args.push("--output-last-message".into());
616 args.push(path.clone());
617 }
618 if self.prompt_via_stdin {
619 args.push("-".into());
622 } else if let Some(prompt) = &self.prompt {
623 args.push(prompt.clone());
624 }
625
626 args
627 }
628
629 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
630 if self.prompt_via_stdin {
631 let prompt = self.prompt.as_deref().unwrap_or_default();
632 return exec::run_codex_with_stdin_prompt(codex, self.args(), prompt).await;
633 }
634 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
635 }
636}
637
638#[derive(Debug, Clone)]
643pub struct ExecResumeCommand {
644 session_id: Option<String>,
645 prompt: Option<String>,
646 prompt_via_stdin: bool,
647 last: bool,
648 all: bool,
649 approval_policy: Option<ApprovalPolicyConfig>,
650 web_search: Option<WebSearchMode>,
651 config_overrides: Vec<String>,
652 enabled_features: Vec<String>,
653 disabled_features: Vec<String>,
654 rollout_budget: Option<RolloutBudgetConfig>,
655 images: Vec<String>,
656 model: Option<String>,
657 strict_config: bool,
658 dangerously_bypass_hook_trust: bool,
659 ignore_user_config: bool,
660 ignore_rules: bool,
661 output_schema: Option<String>,
662 full_auto: bool,
663 dangerously_bypass_approvals_and_sandbox: bool,
664 skip_git_repo_check: bool,
665 ephemeral: bool,
666 json: bool,
667 output_last_message: Option<String>,
668 retry_policy: Option<crate::retry::RetryPolicy>,
669}
670
671impl ExecResumeCommand {
672 #[must_use]
674 pub fn new() -> Self {
675 Self {
676 session_id: None,
677 prompt: None,
678 prompt_via_stdin: false,
679 last: false,
680 all: false,
681 approval_policy: None,
682 web_search: None,
683 config_overrides: Vec::new(),
684 enabled_features: Vec::new(),
685 disabled_features: Vec::new(),
686 rollout_budget: None,
687 images: Vec::new(),
688 model: None,
689 strict_config: false,
690 dangerously_bypass_hook_trust: false,
691 ignore_user_config: false,
692 ignore_rules: false,
693 output_schema: None,
694 full_auto: false,
695 dangerously_bypass_approvals_and_sandbox: false,
696 skip_git_repo_check: false,
697 ephemeral: false,
698 json: false,
699 output_last_message: None,
700 retry_policy: None,
701 }
702 }
703
704 #[must_use]
706 pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
707 self.session_id = Some(session_id.into());
708 self
709 }
710
711 #[must_use]
713 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
714 self.prompt = Some(prompt.into());
715 self
716 }
717
718 #[must_use]
724 pub fn from_stdin(prompt: impl Into<String>) -> Self {
725 Self::new().prompt(prompt).prompt_via_stdin()
726 }
727
728 #[must_use]
734 pub fn prompt_via_stdin(mut self) -> Self {
735 self.prompt_via_stdin = true;
736 self
737 }
738
739 #[cfg(feature = "json")]
742 pub(crate) fn stdin_prompt(&self) -> Option<&str> {
743 self.prompt_via_stdin
744 .then(|| self.prompt.as_deref().unwrap_or_default())
745 }
746
747 #[must_use]
749 pub fn last(mut self) -> Self {
750 self.last = true;
751 self
752 }
753
754 #[must_use]
756 pub fn all(mut self) -> Self {
757 self.all = true;
758 self
759 }
760
761 #[must_use]
765 pub fn model(mut self, model: impl Into<String>) -> Self {
766 let model = model.into();
767 assert!(!model.is_empty(), "model name must not be empty");
768 self.model = Some(model);
769 self
770 }
771
772 #[must_use]
776 pub fn image(mut self, path: impl Into<String>) -> Self {
777 self.images.push(path.into());
778 self
779 }
780
781 #[must_use]
783 pub fn json(mut self) -> Self {
784 self.json = true;
785 self
786 }
787
788 #[must_use]
790 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
791 self.output_last_message = Some(path.into());
792 self
793 }
794
795 #[must_use]
802 pub fn config(mut self, key_value: impl Into<String>) -> Self {
803 self.config_overrides.push(key_value.into());
804 self
805 }
806
807 #[must_use]
814 pub fn approval_policy(mut self, policy: impl Into<ApprovalPolicyConfig>) -> Self {
815 self.approval_policy = Some(policy.into());
816 self
817 }
818
819 #[must_use]
824 pub fn search(self) -> Self {
825 self.search_mode(WebSearchMode::Live)
826 }
827
828 #[must_use]
834 pub fn search_mode(mut self, mode: WebSearchMode) -> Self {
835 self.web_search = Some(mode);
836 self
837 }
838
839 #[must_use]
843 pub fn enable(mut self, feature: impl Into<String>) -> Self {
844 self.enabled_features.push(feature.into());
845 self
846 }
847
848 #[must_use]
852 pub fn disable(mut self, feature: impl Into<String>) -> Self {
853 self.disabled_features.push(feature.into());
854 self
855 }
856
857 #[must_use]
864 pub fn rollout_budget(mut self, budget: RolloutBudgetConfig) -> Self {
865 self.rollout_budget = Some(budget);
866 self
867 }
868
869 #[must_use]
871 pub fn strict_config(mut self) -> Self {
872 self.strict_config = true;
873 self
874 }
875
876 #[must_use]
880 pub(crate) fn set_bypass_hook_trust(mut self) -> Self {
881 self.dangerously_bypass_hook_trust = true;
882 self
883 }
884
885 #[must_use]
887 pub fn ignore_user_config(mut self) -> Self {
888 self.ignore_user_config = true;
889 self
890 }
891
892 #[must_use]
894 pub fn ignore_rules(mut self) -> Self {
895 self.ignore_rules = true;
896 self
897 }
898
899 #[must_use]
901 pub fn output_schema(mut self, path: impl Into<String>) -> Self {
902 self.output_schema = Some(path.into());
903 self
904 }
905
906 #[must_use]
912 pub fn full_auto(mut self) -> Self {
913 self.full_auto = true;
914 self
915 }
916
917 #[must_use]
921 pub(crate) fn set_bypass_approvals_and_sandbox(mut self) -> Self {
922 self.dangerously_bypass_approvals_and_sandbox = true;
923 self
924 }
925
926 #[must_use]
928 pub fn skip_git_repo_check(mut self) -> Self {
929 self.skip_git_repo_check = true;
930 self
931 }
932
933 #[must_use]
935 pub fn ephemeral(mut self) -> Self {
936 self.ephemeral = true;
937 self
938 }
939
940 #[must_use]
944 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
945 self.retry_policy = Some(policy);
946 self
947 }
948
949 #[cfg(feature = "json")]
954 pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
955 let mut args = self.args();
956 if !self.json {
957 args.push("--json".into());
958 }
959
960 let output = if self.prompt_via_stdin {
961 let prompt = self.prompt.as_deref().unwrap_or_default();
962 exec::run_codex_with_stdin_prompt(codex, args, prompt).await?
963 } else {
964 exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?
965 };
966 parse_json_lines(&output.stdout)
967 }
968
969 #[cfg(feature = "json")]
974 pub async fn execute_json(&self, codex: &Codex) -> Result<QueryResult> {
975 let events = self.execute_json_lines(codex).await?;
976 Ok(QueryResult::from_events(events))
977 }
978
979 #[cfg(feature = "json")]
985 pub async fn stream<F>(&self, codex: &Codex, handler: F) -> Result<()>
986 where
987 F: FnMut(JsonLineEvent),
988 {
989 crate::streaming::stream_exec_resume(codex, self, handler).await
990 }
991}
992
993impl Default for ExecResumeCommand {
994 fn default() -> Self {
995 Self::new()
996 }
997}
998
999impl CodexCommand for ExecResumeCommand {
1000 type Output = CommandOutput;
1001
1002 fn args(&self) -> Vec<String> {
1003 let mut args = vec!["exec".into(), "resume".into()];
1004 push_typed_config(&mut args, self.approval_policy, self.web_search);
1005 if self.full_auto {
1008 args.push("-c".into());
1009 args.push(format!(
1010 "sandbox_mode=\"{}\"",
1011 SandboxMode::WorkspaceWrite.as_arg()
1012 ));
1013 }
1014 push_repeat(&mut args, "-c", &self.config_overrides);
1015 push_feature_toggles(
1016 &mut args,
1017 &self.enabled_features,
1018 &self.disabled_features,
1019 self.rollout_budget.is_some(),
1020 );
1021 if let Some(budget) = &self.rollout_budget {
1022 args.push("-c".into());
1023 args.push(budget.config_override());
1024 }
1025 if self.last {
1026 args.push("--last".into());
1027 }
1028 if self.all {
1029 args.push("--all".into());
1030 }
1031 push_repeat(&mut args, "--image", &self.images);
1032 if let Some(model) = &self.model {
1033 args.push("--model".into());
1034 args.push(model.clone());
1035 }
1036 if self.strict_config {
1037 args.push("--strict-config".into());
1038 }
1039 if self.dangerously_bypass_approvals_and_sandbox {
1040 args.push("--dangerously-bypass-approvals-and-sandbox".into());
1041 }
1042 if self.dangerously_bypass_hook_trust {
1043 args.push("--dangerously-bypass-hook-trust".into());
1044 }
1045 if self.skip_git_repo_check {
1046 args.push("--skip-git-repo-check".into());
1047 }
1048 if self.ephemeral {
1049 args.push("--ephemeral".into());
1050 }
1051 if self.ignore_user_config {
1052 args.push("--ignore-user-config".into());
1053 }
1054 if self.ignore_rules {
1055 args.push("--ignore-rules".into());
1056 }
1057 if let Some(output_schema) = &self.output_schema {
1058 args.push("--output-schema".into());
1059 args.push(output_schema.clone());
1060 }
1061 if self.json {
1062 args.push("--json".into());
1063 }
1064 if let Some(path) = &self.output_last_message {
1065 args.push("--output-last-message".into());
1066 args.push(path.clone());
1067 }
1068 if let Some(session_id) = &self.session_id {
1069 args.push(session_id.clone());
1070 }
1071 if self.prompt_via_stdin {
1072 args.push("-".into());
1073 } else if let Some(prompt) = &self.prompt {
1074 args.push(prompt.clone());
1075 }
1076 args
1077 }
1078
1079 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
1080 if self.prompt_via_stdin {
1081 let prompt = self.prompt.as_deref().unwrap_or_default();
1082 return exec::run_codex_with_stdin_prompt(codex, self.args(), prompt).await;
1083 }
1084 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
1085 }
1086}
1087
1088fn push_repeat(args: &mut Vec<String>, flag: &str, values: &[String]) {
1089 for value in values {
1090 args.push(flag.into());
1091 args.push(value.clone());
1092 }
1093}
1094
1095fn push_feature_toggles(
1096 args: &mut Vec<String>,
1097 enabled: &[String],
1098 disabled: &[String],
1099 protects_rollout_budget: bool,
1100) {
1101 let keep = |feature: &&String| !protects_rollout_budget || feature.as_str() != "rollout_budget";
1102 for feature in enabled.iter().filter(keep) {
1103 args.push("--enable".into());
1104 args.push(feature.clone());
1105 }
1106 for feature in disabled.iter().filter(keep) {
1107 args.push("--disable".into());
1108 args.push(feature.clone());
1109 }
1110}
1111
1112#[cfg(feature = "json")]
1113fn parse_json_lines(stdout: &str) -> Result<Vec<JsonLineEvent>> {
1114 stdout
1115 .lines()
1116 .filter(|line| line.trim_start().starts_with('{'))
1117 .map(|line| {
1118 serde_json::from_str(line).map_err(|source| Error::Json {
1119 message: format!("failed to parse JSONL event: {line}"),
1120 source,
1121 })
1122 })
1123 .collect()
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128 use super::*;
1129 use crate::types::ApprovalPolicy;
1130
1131 #[test]
1132 fn exec_args() {
1133 let args = ExecCommand::new("fix the test")
1134 .model("gpt-5")
1135 .sandbox(SandboxMode::WorkspaceWrite)
1136 .strict_config()
1137 .skip_git_repo_check()
1138 .ephemeral()
1139 .ignore_user_config()
1140 .ignore_rules()
1141 .json()
1142 .args();
1143
1144 assert_eq!(
1145 args,
1146 vec![
1147 "exec",
1148 "--model",
1149 "gpt-5",
1150 "--sandbox",
1151 "workspace-write",
1152 "--strict-config",
1153 "--skip-git-repo-check",
1154 "--ephemeral",
1155 "--ignore-user-config",
1156 "--ignore-rules",
1157 "--json",
1158 "fix the test",
1159 ]
1160 );
1161 }
1162
1163 #[test]
1164 fn exec_args_hook_trust() {
1165 let args = ExecCommand::new("go")
1166 .set_bypass_approvals_and_sandbox()
1167 .set_bypass_hook_trust()
1168 .args();
1169
1170 assert_eq!(
1171 args,
1172 vec![
1173 "exec",
1174 "--dangerously-bypass-approvals-and-sandbox",
1175 "--dangerously-bypass-hook-trust",
1176 "go",
1177 ]
1178 );
1179 }
1180
1181 #[test]
1182 #[should_panic(expected = "model name must not be empty")]
1183 fn exec_model_empty_panics() {
1184 let _ = ExecCommand::new("prompt").model("");
1185 }
1186
1187 #[test]
1188 #[should_panic(expected = "model name must not be empty")]
1189 fn exec_resume_model_empty_panics() {
1190 let _ = ExecResumeCommand::new().model("");
1191 }
1192
1193 #[test]
1194 fn exec_resume_args() {
1195 let args = ExecResumeCommand::new()
1196 .last()
1197 .model("gpt-5")
1198 .json()
1199 .prompt("continue")
1200 .args();
1201
1202 assert_eq!(
1203 args,
1204 vec![
1205 "exec", "resume", "--last", "--model", "gpt-5", "--json", "continue",
1206 ]
1207 );
1208 }
1209
1210 #[test]
1211 fn exec_resume_new_flags() {
1212 let args = ExecResumeCommand::new()
1213 .last()
1214 .strict_config()
1215 .set_bypass_hook_trust()
1216 .args();
1217
1218 assert_eq!(
1219 args,
1220 vec![
1221 "exec",
1222 "resume",
1223 "--last",
1224 "--strict-config",
1225 "--dangerously-bypass-hook-trust",
1226 ]
1227 );
1228 }
1229
1230 #[test]
1233 fn exec_approval_and_search_emit_config_keys() {
1234 let args = ExecCommand::new("hi")
1235 .approval_policy(ApprovalPolicy::Never)
1236 .search()
1237 .args();
1238 assert_eq!(
1239 args,
1240 vec![
1241 "exec",
1242 "-c",
1243 "approval_policy=\"never\"",
1244 "-c",
1245 "web_search=\"live\"",
1246 "hi"
1247 ]
1248 );
1249 assert!(
1250 !args
1251 .iter()
1252 .any(|a| a == "--ask-for-approval" || a == "--search")
1253 );
1254 }
1255
1256 #[test]
1259 fn exec_approval_accepts_config_only_values() {
1260 let args = ExecCommand::new("hi")
1261 .approval_policy(ApprovalPolicyConfig::Granular)
1262 .args();
1263 assert_eq!(
1264 args,
1265 vec!["exec", "-c", "approval_policy=\"granular\"", "hi"]
1266 );
1267 }
1268
1269 #[test]
1270 fn exec_search_mode_variants() {
1271 for (mode, expected) in [
1272 (WebSearchMode::Disabled, "disabled"),
1273 (WebSearchMode::Cached, "cached"),
1274 (WebSearchMode::Indexed, "indexed"),
1275 (WebSearchMode::Live, "live"),
1276 ] {
1277 let args = ExecCommand::new("hi").search_mode(mode).args();
1278 assert_eq!(args[2], format!("web_search=\"{expected}\""));
1279 }
1280 }
1281
1282 #[test]
1285 fn exec_raw_config_is_emitted_after_typed_config() {
1286 let args = ExecCommand::new("hi")
1287 .approval_policy(ApprovalPolicy::Never)
1288 .config("approval_policy=\"untrusted\"")
1289 .args();
1290 let typed = args
1291 .iter()
1292 .position(|a| a == "approval_policy=\"never\"")
1293 .unwrap();
1294 let raw = args
1295 .iter()
1296 .position(|a| a == "approval_policy=\"untrusted\"")
1297 .unwrap();
1298 assert!(typed < raw, "raw override must win: {args:?}");
1299 }
1300
1301 #[test]
1302 fn native_rollout_budget_is_identical_and_final_on_open_and_resume() {
1303 let budget = RolloutBudgetConfig::builder(10_000)
1304 .reminder_at_remaining_tokens([5_000, 1_000])
1305 .sampling_token_weight(1.0)
1306 .prefill_token_weight(0.25)
1307 .build()
1308 .expect("valid budget");
1309 let expected = budget.config_override();
1310 let opening = ExecCommand::new("hi")
1311 .rollout_budget(budget.clone())
1312 .config("features.rollout_budget=false")
1313 .enable("rollout_budget")
1314 .disable("rollout_budget")
1315 .enable("keep-enabled")
1316 .disable("keep-disabled")
1317 .args();
1318 let resumed = ExecResumeCommand::new()
1319 .session_id("thread")
1320 .rollout_budget(budget)
1321 .config("features.rollout_budget=false")
1322 .enable("rollout_budget")
1323 .disable("rollout_budget")
1324 .enable("keep-enabled")
1325 .disable("keep-disabled")
1326 .args();
1327
1328 for args in [opening, resumed] {
1329 let budget_at = args.iter().position(|arg| arg == &expected).unwrap();
1330 let raw_at = args
1331 .iter()
1332 .position(|arg| arg == "features.rollout_budget=false")
1333 .unwrap();
1334 assert!(
1335 raw_at < budget_at,
1336 "native budget must beat raw config: {args:?}"
1337 );
1338 assert!(
1339 !args.windows(2).any(|pair| {
1340 matches!(pair[0].as_str(), "--enable" | "--disable")
1341 && pair[1] == "rollout_budget"
1342 }),
1343 "native budget must suppress conflicting feature toggles: {args:?}"
1344 );
1345 assert!(
1346 args.windows(2)
1347 .any(|pair| pair == ["--enable", "keep-enabled"])
1348 );
1349 assert!(
1350 args.windows(2)
1351 .any(|pair| pair == ["--disable", "keep-disabled"])
1352 );
1353 }
1354 }
1355
1356 #[test]
1358 fn exec_full_auto_emits_sandbox_workspace_write() {
1359 let args = ExecCommand::new("hi").full_auto().args();
1360 assert_eq!(args, vec!["exec", "--sandbox", "workspace-write", "hi"]);
1361 assert!(!args.iter().any(|a| a == "--full-auto"));
1362 }
1363
1364 #[test]
1365 fn exec_explicit_sandbox_wins_over_full_auto() {
1366 let args = ExecCommand::new("hi")
1367 .full_auto()
1368 .sandbox(SandboxMode::ReadOnly)
1369 .args();
1370 assert_eq!(args, vec!["exec", "--sandbox", "read-only", "hi"]);
1371 }
1372
1373 #[test]
1376 fn exec_resume_full_auto_emits_sandbox_config_key() {
1377 let args = ExecResumeCommand::new().last().full_auto().args();
1378 assert_eq!(
1379 args,
1380 vec![
1381 "exec",
1382 "resume",
1383 "-c",
1384 "sandbox_mode=\"workspace-write\"",
1385 "--last"
1386 ]
1387 );
1388 assert!(!args.iter().any(|a| a == "--full-auto"));
1389 }
1390
1391 #[test]
1392 fn exec_resume_approval_and_search_emit_config_keys() {
1393 let args = ExecResumeCommand::new()
1394 .last()
1395 .approval_policy(ApprovalPolicyConfig::OnFailure)
1396 .search_mode(WebSearchMode::Cached)
1397 .args();
1398 assert_eq!(
1399 args,
1400 vec![
1401 "exec",
1402 "resume",
1403 "-c",
1404 "approval_policy=\"on-failure\"",
1405 "-c",
1406 "web_search=\"cached\"",
1407 "--last"
1408 ]
1409 );
1410 }
1411
1412 #[test]
1415 fn exec_resume_ignore_and_output_schema_args() {
1416 let args = ExecResumeCommand::new()
1417 .last()
1418 .ignore_user_config()
1419 .ignore_rules()
1420 .output_schema("/tmp/schema.json")
1421 .args();
1422 assert_eq!(
1423 args,
1424 vec![
1425 "exec",
1426 "resume",
1427 "--last",
1428 "--ignore-user-config",
1429 "--ignore-rules",
1430 "--output-schema",
1431 "/tmp/schema.json"
1432 ]
1433 );
1434 }
1435
1436 #[cfg(all(unix, feature = "json"))]
1441 #[tokio::test]
1442 async fn stdin_prompt_reaches_the_child() {
1443 let codex = echoing_stdin_codex();
1444 let prompt = "a prompt too awkward for argv\nwith a second line";
1445
1446 let result = ExecCommand::from_stdin(prompt)
1447 .execute_json(&codex)
1448 .await
1449 .unwrap();
1450
1451 assert_eq!(result.result, prompt);
1452 }
1453
1454 #[cfg(unix)]
1455 #[tokio::test]
1456 async fn resume_stdin_prompt_reaches_the_child_for_raw_execution() {
1457 let codex = echoing_stdin_codex();
1458 let prompt = "raw resumed stdin prompt";
1459
1460 let output = ExecResumeCommand::from_stdin(prompt)
1461 .session_id("thread-1")
1462 .execute(&codex)
1463 .await
1464 .unwrap();
1465
1466 assert!(output.stdout.contains(prompt));
1467 }
1468
1469 #[cfg(all(unix, feature = "json"))]
1470 #[tokio::test]
1471 async fn resume_stdin_prompt_reaches_the_child_for_json_execution() {
1472 let codex = echoing_stdin_codex();
1473 let prompt = "json resumed stdin prompt";
1474
1475 let result = ExecResumeCommand::from_stdin(prompt)
1476 .session_id("thread-1")
1477 .execute_json(&codex)
1478 .await
1479 .unwrap();
1480
1481 assert_eq!(result.result, prompt);
1482 }
1483
1484 #[cfg(all(unix, feature = "json"))]
1486 #[tokio::test]
1487 async fn stdin_prompt_reaches_the_child_when_streaming() {
1488 let codex = echoing_stdin_codex();
1489 let prompt = "streamed stdin prompt";
1490 let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1491 let sink = std::sync::Arc::clone(&seen);
1492
1493 ExecCommand::from_stdin(prompt)
1494 .stream(&codex, move |event| {
1495 if let Some(text) = event.agent_message_text() {
1496 sink.lock().unwrap().push(text);
1497 }
1498 })
1499 .await
1500 .unwrap();
1501
1502 assert_eq!(seen.lock().unwrap().as_slice(), &[prompt.to_string()]);
1503 }
1504
1505 #[cfg(all(unix, feature = "json"))]
1506 #[tokio::test]
1507 async fn resume_stdin_prompt_reaches_the_child_when_streaming() {
1508 let codex = echoing_stdin_codex();
1509 let prompt = "streamed resumed stdin prompt";
1510 let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1511 let sink = std::sync::Arc::clone(&seen);
1512
1513 ExecResumeCommand::from_stdin(prompt)
1514 .session_id("thread-1")
1515 .stream(&codex, move |event| {
1516 if let Some(text) = event.agent_message_text() {
1517 sink.lock().unwrap().push(text);
1518 }
1519 })
1520 .await
1521 .unwrap();
1522
1523 assert_eq!(seen.lock().unwrap().as_slice(), &[prompt.to_string()]);
1524 }
1525
1526 #[cfg(all(unix, feature = "json"))]
1529 #[tokio::test]
1530 async fn a_prompt_larger_than_the_pipe_buffer_still_completes() {
1531 let codex = echoing_stdin_codex();
1532 let prompt = "x".repeat(512 * 1024);
1534
1535 let result = ExecCommand::from_stdin(&prompt)
1536 .execute_json(&codex)
1537 .await
1538 .unwrap();
1539
1540 assert_eq!(result.result.len(), prompt.len());
1541 }
1542
1543 #[cfg(unix)]
1544 fn echoing_stdin_codex() -> Codex {
1545 let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1546 .join("tests")
1547 .join("fake-codex-echo-stdin.sh");
1548 Codex::builder()
1549 .binary("/bin/bash")
1550 .arg(script.to_str().unwrap())
1551 .build()
1552 .expect("bash must exist")
1553 }
1554
1555 #[test]
1556 fn from_stdin_emits_the_dash_positional_not_the_prompt() {
1557 let args = ExecCommand::from_stdin("secret prompt").ephemeral().args();
1558 assert_eq!(args, vec!["exec", "--ephemeral", "-"]);
1559 assert!(!args.iter().any(|a| a.contains("secret")));
1562 }
1563
1564 #[test]
1565 fn prompt_via_stdin_converts_an_existing_prompt() {
1566 let args = ExecCommand::new("hello").prompt_via_stdin().args();
1567 assert_eq!(args, vec!["exec", "-"]);
1568 }
1569
1570 #[test]
1571 fn resume_from_stdin_emits_the_dash_positional_not_the_prompt() {
1572 let args = ExecResumeCommand::from_stdin("secret prompt")
1573 .session_id("thread-1")
1574 .ephemeral()
1575 .args();
1576 assert_eq!(args, vec!["exec", "resume", "--ephemeral", "thread-1", "-"]);
1577 assert!(!args.iter().any(|arg| arg.contains("secret")));
1578 }
1579
1580 #[test]
1581 fn resume_prompt_via_stdin_converts_an_existing_prompt() {
1582 let args = ExecResumeCommand::new()
1583 .session_id("thread-1")
1584 .prompt("hello")
1585 .prompt_via_stdin()
1586 .args();
1587 assert_eq!(args, vec!["exec", "resume", "thread-1", "-"]);
1588 }
1589}