use crate::Codex;
use crate::command::CodexCommand;
#[cfg(feature = "json")]
use crate::error::Error;
use crate::error::Result;
use crate::exec::{self, CommandOutput};
use crate::types::{ApprovalPolicyConfig, Color, SandboxMode, WebSearchMode};
#[cfg(feature = "json")]
use crate::types::{JsonLineEvent, QueryResult};
pub(crate) fn push_typed_config(
args: &mut Vec<String>,
approval_policy: Option<ApprovalPolicyConfig>,
web_search: Option<WebSearchMode>,
) {
if let Some(policy) = approval_policy {
args.push("-c".into());
args.push(format!("approval_policy=\"{}\"", policy.as_config_value()));
}
if let Some(mode) = web_search {
args.push("-c".into());
args.push(format!("web_search=\"{}\"", mode.as_config_value()));
}
}
pub(crate) fn effective_sandbox(
sandbox: Option<SandboxMode>,
full_auto: bool,
) -> Option<SandboxMode> {
sandbox.or(full_auto.then_some(SandboxMode::WorkspaceWrite))
}
#[derive(Debug, Clone)]
pub struct ExecCommand {
approve_for_me: bool,
prompt: Option<String>,
prompt_via_stdin: bool,
approval_policy: Option<ApprovalPolicyConfig>,
web_search: Option<WebSearchMode>,
config_overrides: Vec<String>,
enabled_features: Vec<String>,
disabled_features: Vec<String>,
images: Vec<String>,
model: Option<String>,
oss: bool,
local_provider: Option<String>,
sandbox: Option<SandboxMode>,
strict_config: bool,
dangerously_bypass_hook_trust: bool,
ignore_user_config: bool,
ignore_rules: bool,
profile: Option<String>,
full_auto: bool,
dangerously_bypass_approvals_and_sandbox: bool,
cd: Option<String>,
skip_git_repo_check: bool,
add_dirs: Vec<String>,
ephemeral: bool,
output_schema: Option<String>,
color: Option<Color>,
json: bool,
output_last_message: Option<String>,
retry_policy: Option<crate::retry::RetryPolicy>,
}
impl ExecCommand {
#[must_use]
pub fn new(prompt: impl Into<String>) -> Self {
Self {
approve_for_me: false,
prompt: Some(prompt.into()),
prompt_via_stdin: false,
approval_policy: None,
web_search: None,
config_overrides: Vec::new(),
enabled_features: Vec::new(),
disabled_features: Vec::new(),
images: Vec::new(),
model: None,
oss: false,
local_provider: None,
sandbox: None,
strict_config: false,
dangerously_bypass_hook_trust: false,
ignore_user_config: false,
ignore_rules: false,
profile: None,
full_auto: false,
dangerously_bypass_approvals_and_sandbox: false,
cd: None,
skip_git_repo_check: false,
add_dirs: Vec::new(),
ephemeral: false,
output_schema: None,
color: None,
json: false,
output_last_message: None,
retry_policy: None,
}
}
#[must_use]
pub fn from_stdin(prompt: impl Into<String>) -> Self {
Self::new(prompt).prompt_via_stdin()
}
#[must_use]
pub fn prompt_via_stdin(mut self) -> Self {
self.prompt_via_stdin = true;
self
}
#[cfg(feature = "json")]
pub(crate) fn stdin_prompt(&self) -> Option<&str> {
self.prompt_via_stdin
.then(|| self.prompt.as_deref().unwrap_or_default())
}
#[must_use]
pub fn config(mut self, key_value: impl Into<String>) -> Self {
self.config_overrides.push(key_value.into());
self
}
#[must_use]
pub fn approval_policy(mut self, policy: impl Into<ApprovalPolicyConfig>) -> Self {
self.approval_policy = Some(policy.into());
self
}
#[must_use]
pub fn search(self) -> Self {
self.search_mode(WebSearchMode::Live)
}
#[must_use]
pub fn search_mode(mut self, mode: WebSearchMode) -> Self {
self.web_search = Some(mode);
self
}
#[must_use]
pub fn enable(mut self, feature: impl Into<String>) -> Self {
self.enabled_features.push(feature.into());
self
}
#[must_use]
pub fn disable(mut self, feature: impl Into<String>) -> Self {
self.disabled_features.push(feature.into());
self
}
#[must_use]
pub fn image(mut self, path: impl Into<String>) -> Self {
self.images.push(path.into());
self
}
#[must_use]
pub fn model(mut self, model: impl Into<String>) -> Self {
let model = model.into();
assert!(!model.is_empty(), "model name must not be empty");
self.model = Some(model);
self
}
#[must_use]
pub fn oss(mut self) -> Self {
self.oss = true;
self
}
#[must_use]
pub fn local_provider(mut self, provider: impl Into<String>) -> Self {
self.local_provider = Some(provider.into());
self
}
#[must_use]
pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
self.sandbox = Some(sandbox);
self
}
#[must_use]
pub fn strict_config(mut self) -> Self {
self.strict_config = true;
self
}
#[must_use]
pub(crate) fn set_bypass_hook_trust(mut self) -> Self {
self.dangerously_bypass_hook_trust = true;
self
}
#[must_use]
pub fn ignore_user_config(mut self) -> Self {
self.ignore_user_config = true;
self
}
#[must_use]
pub fn ignore_rules(mut self) -> Self {
self.ignore_rules = true;
self
}
#[must_use]
pub fn profile(mut self, profile: impl Into<String>) -> Self {
self.profile = Some(profile.into());
self
}
#[must_use]
pub fn full_auto(mut self) -> Self {
self.full_auto = true;
self
}
#[must_use]
pub fn approve_for_me(mut self) -> Self {
self.approve_for_me = true;
self
}
#[must_use]
pub(crate) fn set_bypass_approvals_and_sandbox(mut self) -> Self {
self.dangerously_bypass_approvals_and_sandbox = true;
self
}
#[must_use]
pub fn cd(mut self, dir: impl Into<String>) -> Self {
self.cd = Some(dir.into());
self
}
#[must_use]
pub fn skip_git_repo_check(mut self) -> Self {
self.skip_git_repo_check = true;
self
}
#[must_use]
pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
self.add_dirs.push(dir.into());
self
}
#[must_use]
pub fn ephemeral(mut self) -> Self {
self.ephemeral = true;
self
}
#[must_use]
pub fn output_schema(mut self, path: impl Into<String>) -> Self {
self.output_schema = Some(path.into());
self
}
#[must_use]
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
#[must_use]
pub fn json(mut self) -> Self {
self.json = true;
self
}
#[must_use]
pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
self.output_last_message = Some(path.into());
self
}
#[must_use]
pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
self.retry_policy = Some(policy);
self
}
#[cfg(feature = "json")]
pub async fn stream<F>(&self, codex: &Codex, handler: F) -> Result<()>
where
F: FnMut(JsonLineEvent),
{
crate::streaming::stream_exec(codex, self, handler).await
}
#[cfg(feature = "json")]
pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
let mut args = self.args();
if !self.json {
args.push("--json".into());
}
let output = if self.prompt_via_stdin {
let prompt = self.prompt.as_deref().unwrap_or_default();
exec::run_codex_with_stdin_prompt(codex, args, prompt).await?
} else {
exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?
};
parse_json_lines(&output.stdout)
}
#[cfg(feature = "json")]
pub async fn execute_json(&self, codex: &Codex) -> Result<QueryResult> {
let events = self.execute_json_lines(codex).await?;
Ok(QueryResult::from_events(events))
}
}
impl CodexCommand for ExecCommand {
type Output = CommandOutput;
fn args(&self) -> Vec<String> {
let mut args = vec!["exec".to_string()];
push_typed_config(&mut args, self.approval_policy, self.web_search);
push_repeat(&mut args, "-c", &self.config_overrides);
push_repeat(&mut args, "--enable", &self.enabled_features);
push_repeat(&mut args, "--disable", &self.disabled_features);
push_repeat(&mut args, "--image", &self.images);
if let Some(model) = &self.model {
args.push("--model".into());
args.push(model.clone());
}
if self.oss {
args.push("--oss".into());
}
if let Some(local_provider) = &self.local_provider {
args.push("--local-provider".into());
args.push(local_provider.clone());
}
if let Some(sandbox) = effective_sandbox(self.sandbox, self.full_auto) {
args.push("--sandbox".into());
args.push(sandbox.as_arg().into());
}
if self.strict_config {
args.push("--strict-config".into());
}
if let Some(profile) = &self.profile {
args.push("--profile".into());
args.push(profile.clone());
}
if self.approve_for_me {
args.push("--approve-for-me".into());
}
if self.dangerously_bypass_approvals_and_sandbox {
args.push("--dangerously-bypass-approvals-and-sandbox".into());
}
if self.dangerously_bypass_hook_trust {
args.push("--dangerously-bypass-hook-trust".into());
}
if let Some(cd) = &self.cd {
args.push("--cd".into());
args.push(cd.clone());
}
if self.skip_git_repo_check {
args.push("--skip-git-repo-check".into());
}
push_repeat(&mut args, "--add-dir", &self.add_dirs);
if self.ephemeral {
args.push("--ephemeral".into());
}
if self.ignore_user_config {
args.push("--ignore-user-config".into());
}
if self.ignore_rules {
args.push("--ignore-rules".into());
}
if let Some(output_schema) = &self.output_schema {
args.push("--output-schema".into());
args.push(output_schema.clone());
}
if let Some(color) = self.color {
args.push("--color".into());
args.push(color.as_arg().into());
}
if self.json {
args.push("--json".into());
}
if let Some(path) = &self.output_last_message {
args.push("--output-last-message".into());
args.push(path.clone());
}
if self.prompt_via_stdin {
args.push("-".into());
} else if let Some(prompt) = &self.prompt {
args.push(prompt.clone());
}
args
}
async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
if self.prompt_via_stdin {
let prompt = self.prompt.as_deref().unwrap_or_default();
return exec::run_codex_with_stdin_prompt(codex, self.args(), prompt).await;
}
exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
}
}
#[derive(Debug, Clone)]
pub struct ExecResumeCommand {
session_id: Option<String>,
prompt: Option<String>,
last: bool,
all: bool,
approval_policy: Option<ApprovalPolicyConfig>,
web_search: Option<WebSearchMode>,
config_overrides: Vec<String>,
enabled_features: Vec<String>,
disabled_features: Vec<String>,
images: Vec<String>,
model: Option<String>,
strict_config: bool,
dangerously_bypass_hook_trust: bool,
ignore_user_config: bool,
ignore_rules: bool,
output_schema: Option<String>,
full_auto: bool,
dangerously_bypass_approvals_and_sandbox: bool,
skip_git_repo_check: bool,
ephemeral: bool,
json: bool,
output_last_message: Option<String>,
retry_policy: Option<crate::retry::RetryPolicy>,
}
impl ExecResumeCommand {
#[must_use]
pub fn new() -> Self {
Self {
session_id: None,
prompt: None,
last: false,
all: false,
approval_policy: None,
web_search: None,
config_overrides: Vec::new(),
enabled_features: Vec::new(),
disabled_features: Vec::new(),
images: Vec::new(),
model: None,
strict_config: false,
dangerously_bypass_hook_trust: false,
ignore_user_config: false,
ignore_rules: false,
output_schema: None,
full_auto: false,
dangerously_bypass_approvals_and_sandbox: false,
skip_git_repo_check: false,
ephemeral: false,
json: false,
output_last_message: None,
retry_policy: None,
}
}
#[must_use]
pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
#[must_use]
pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
self.prompt = Some(prompt.into());
self
}
#[must_use]
pub fn last(mut self) -> Self {
self.last = true;
self
}
#[must_use]
pub fn all(mut self) -> Self {
self.all = true;
self
}
#[must_use]
pub fn model(mut self, model: impl Into<String>) -> Self {
let model = model.into();
assert!(!model.is_empty(), "model name must not be empty");
self.model = Some(model);
self
}
#[must_use]
pub fn image(mut self, path: impl Into<String>) -> Self {
self.images.push(path.into());
self
}
#[must_use]
pub fn json(mut self) -> Self {
self.json = true;
self
}
#[must_use]
pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
self.output_last_message = Some(path.into());
self
}
#[must_use]
pub fn config(mut self, key_value: impl Into<String>) -> Self {
self.config_overrides.push(key_value.into());
self
}
#[must_use]
pub fn approval_policy(mut self, policy: impl Into<ApprovalPolicyConfig>) -> Self {
self.approval_policy = Some(policy.into());
self
}
#[must_use]
pub fn search(self) -> Self {
self.search_mode(WebSearchMode::Live)
}
#[must_use]
pub fn search_mode(mut self, mode: WebSearchMode) -> Self {
self.web_search = Some(mode);
self
}
#[must_use]
pub fn enable(mut self, feature: impl Into<String>) -> Self {
self.enabled_features.push(feature.into());
self
}
#[must_use]
pub fn disable(mut self, feature: impl Into<String>) -> Self {
self.disabled_features.push(feature.into());
self
}
#[must_use]
pub fn strict_config(mut self) -> Self {
self.strict_config = true;
self
}
#[must_use]
pub(crate) fn set_bypass_hook_trust(mut self) -> Self {
self.dangerously_bypass_hook_trust = true;
self
}
#[must_use]
pub fn ignore_user_config(mut self) -> Self {
self.ignore_user_config = true;
self
}
#[must_use]
pub fn ignore_rules(mut self) -> Self {
self.ignore_rules = true;
self
}
#[must_use]
pub fn output_schema(mut self, path: impl Into<String>) -> Self {
self.output_schema = Some(path.into());
self
}
#[must_use]
pub fn full_auto(mut self) -> Self {
self.full_auto = true;
self
}
#[must_use]
pub(crate) fn set_bypass_approvals_and_sandbox(mut self) -> Self {
self.dangerously_bypass_approvals_and_sandbox = true;
self
}
#[must_use]
pub fn skip_git_repo_check(mut self) -> Self {
self.skip_git_repo_check = true;
self
}
#[must_use]
pub fn ephemeral(mut self) -> Self {
self.ephemeral = true;
self
}
#[must_use]
pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
self.retry_policy = Some(policy);
self
}
#[cfg(feature = "json")]
pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
let mut args = self.args();
if !self.json {
args.push("--json".into());
}
let output = exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?;
parse_json_lines(&output.stdout)
}
#[cfg(feature = "json")]
pub async fn execute_json(&self, codex: &Codex) -> Result<QueryResult> {
let events = self.execute_json_lines(codex).await?;
Ok(QueryResult::from_events(events))
}
#[cfg(feature = "json")]
pub async fn stream<F>(&self, codex: &Codex, handler: F) -> Result<()>
where
F: FnMut(JsonLineEvent),
{
crate::streaming::stream_exec_resume(codex, self, handler).await
}
}
impl Default for ExecResumeCommand {
fn default() -> Self {
Self::new()
}
}
impl CodexCommand for ExecResumeCommand {
type Output = CommandOutput;
fn args(&self) -> Vec<String> {
let mut args = vec!["exec".into(), "resume".into()];
push_typed_config(&mut args, self.approval_policy, self.web_search);
if self.full_auto {
args.push("-c".into());
args.push(format!(
"sandbox_mode=\"{}\"",
SandboxMode::WorkspaceWrite.as_arg()
));
}
push_repeat(&mut args, "-c", &self.config_overrides);
push_repeat(&mut args, "--enable", &self.enabled_features);
push_repeat(&mut args, "--disable", &self.disabled_features);
if self.last {
args.push("--last".into());
}
if self.all {
args.push("--all".into());
}
push_repeat(&mut args, "--image", &self.images);
if let Some(model) = &self.model {
args.push("--model".into());
args.push(model.clone());
}
if self.strict_config {
args.push("--strict-config".into());
}
if self.dangerously_bypass_approvals_and_sandbox {
args.push("--dangerously-bypass-approvals-and-sandbox".into());
}
if self.dangerously_bypass_hook_trust {
args.push("--dangerously-bypass-hook-trust".into());
}
if self.skip_git_repo_check {
args.push("--skip-git-repo-check".into());
}
if self.ephemeral {
args.push("--ephemeral".into());
}
if self.ignore_user_config {
args.push("--ignore-user-config".into());
}
if self.ignore_rules {
args.push("--ignore-rules".into());
}
if let Some(output_schema) = &self.output_schema {
args.push("--output-schema".into());
args.push(output_schema.clone());
}
if self.json {
args.push("--json".into());
}
if let Some(path) = &self.output_last_message {
args.push("--output-last-message".into());
args.push(path.clone());
}
if let Some(session_id) = &self.session_id {
args.push(session_id.clone());
}
if let Some(prompt) = &self.prompt {
args.push(prompt.clone());
}
args
}
async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
}
}
fn push_repeat(args: &mut Vec<String>, flag: &str, values: &[String]) {
for value in values {
args.push(flag.into());
args.push(value.clone());
}
}
#[cfg(feature = "json")]
fn parse_json_lines(stdout: &str) -> Result<Vec<JsonLineEvent>> {
stdout
.lines()
.filter(|line| line.trim_start().starts_with('{'))
.map(|line| {
serde_json::from_str(line).map_err(|source| Error::Json {
message: format!("failed to parse JSONL event: {line}"),
source,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ApprovalPolicy;
#[test]
fn exec_args() {
let args = ExecCommand::new("fix the test")
.model("gpt-5")
.sandbox(SandboxMode::WorkspaceWrite)
.strict_config()
.skip_git_repo_check()
.ephemeral()
.ignore_user_config()
.ignore_rules()
.json()
.args();
assert_eq!(
args,
vec![
"exec",
"--model",
"gpt-5",
"--sandbox",
"workspace-write",
"--strict-config",
"--skip-git-repo-check",
"--ephemeral",
"--ignore-user-config",
"--ignore-rules",
"--json",
"fix the test",
]
);
}
#[test]
fn exec_args_hook_trust() {
let args = ExecCommand::new("go")
.set_bypass_approvals_and_sandbox()
.set_bypass_hook_trust()
.args();
assert_eq!(
args,
vec![
"exec",
"--dangerously-bypass-approvals-and-sandbox",
"--dangerously-bypass-hook-trust",
"go",
]
);
}
#[test]
#[should_panic(expected = "model name must not be empty")]
fn exec_model_empty_panics() {
let _ = ExecCommand::new("prompt").model("");
}
#[test]
#[should_panic(expected = "model name must not be empty")]
fn exec_resume_model_empty_panics() {
let _ = ExecResumeCommand::new().model("");
}
#[test]
fn exec_resume_args() {
let args = ExecResumeCommand::new()
.last()
.model("gpt-5")
.json()
.prompt("continue")
.args();
assert_eq!(
args,
vec![
"exec", "resume", "--last", "--model", "gpt-5", "--json", "continue",
]
);
}
#[test]
fn exec_resume_new_flags() {
let args = ExecResumeCommand::new()
.last()
.strict_config()
.set_bypass_hook_trust()
.args();
assert_eq!(
args,
vec![
"exec",
"resume",
"--last",
"--strict-config",
"--dangerously-bypass-hook-trust",
]
);
}
#[test]
fn exec_approval_and_search_emit_config_keys() {
let args = ExecCommand::new("hi")
.approval_policy(ApprovalPolicy::Never)
.search()
.args();
assert_eq!(
args,
vec![
"exec",
"-c",
"approval_policy=\"never\"",
"-c",
"web_search=\"live\"",
"hi"
]
);
assert!(
!args
.iter()
.any(|a| a == "--ask-for-approval" || a == "--search")
);
}
#[test]
fn exec_approval_accepts_config_only_values() {
let args = ExecCommand::new("hi")
.approval_policy(ApprovalPolicyConfig::Granular)
.args();
assert_eq!(
args,
vec!["exec", "-c", "approval_policy=\"granular\"", "hi"]
);
}
#[test]
fn exec_search_mode_variants() {
for (mode, expected) in [
(WebSearchMode::Disabled, "disabled"),
(WebSearchMode::Cached, "cached"),
(WebSearchMode::Indexed, "indexed"),
(WebSearchMode::Live, "live"),
] {
let args = ExecCommand::new("hi").search_mode(mode).args();
assert_eq!(args[2], format!("web_search=\"{expected}\""));
}
}
#[test]
fn exec_raw_config_is_emitted_after_typed_config() {
let args = ExecCommand::new("hi")
.approval_policy(ApprovalPolicy::Never)
.config("approval_policy=\"untrusted\"")
.args();
let typed = args
.iter()
.position(|a| a == "approval_policy=\"never\"")
.unwrap();
let raw = args
.iter()
.position(|a| a == "approval_policy=\"untrusted\"")
.unwrap();
assert!(typed < raw, "raw override must win: {args:?}");
}
#[test]
fn exec_full_auto_emits_sandbox_workspace_write() {
let args = ExecCommand::new("hi").full_auto().args();
assert_eq!(args, vec!["exec", "--sandbox", "workspace-write", "hi"]);
assert!(!args.iter().any(|a| a == "--full-auto"));
}
#[test]
fn exec_explicit_sandbox_wins_over_full_auto() {
let args = ExecCommand::new("hi")
.full_auto()
.sandbox(SandboxMode::ReadOnly)
.args();
assert_eq!(args, vec!["exec", "--sandbox", "read-only", "hi"]);
}
#[test]
fn exec_resume_full_auto_emits_sandbox_config_key() {
let args = ExecResumeCommand::new().last().full_auto().args();
assert_eq!(
args,
vec![
"exec",
"resume",
"-c",
"sandbox_mode=\"workspace-write\"",
"--last"
]
);
assert!(!args.iter().any(|a| a == "--full-auto"));
}
#[test]
fn exec_resume_approval_and_search_emit_config_keys() {
let args = ExecResumeCommand::new()
.last()
.approval_policy(ApprovalPolicyConfig::OnFailure)
.search_mode(WebSearchMode::Cached)
.args();
assert_eq!(
args,
vec![
"exec",
"resume",
"-c",
"approval_policy=\"on-failure\"",
"-c",
"web_search=\"cached\"",
"--last"
]
);
}
#[test]
fn exec_resume_ignore_and_output_schema_args() {
let args = ExecResumeCommand::new()
.last()
.ignore_user_config()
.ignore_rules()
.output_schema("/tmp/schema.json")
.args();
assert_eq!(
args,
vec![
"exec",
"resume",
"--last",
"--ignore-user-config",
"--ignore-rules",
"--output-schema",
"/tmp/schema.json"
]
);
}
#[cfg(all(unix, feature = "json"))]
#[tokio::test]
async fn stdin_prompt_reaches_the_child() {
let codex = echoing_stdin_codex();
let prompt = "a prompt too awkward for argv\nwith a second line";
let result = ExecCommand::from_stdin(prompt)
.execute_json(&codex)
.await
.unwrap();
assert_eq!(result.result, prompt);
}
#[cfg(all(unix, feature = "json"))]
#[tokio::test]
async fn stdin_prompt_reaches_the_child_when_streaming() {
let codex = echoing_stdin_codex();
let prompt = "streamed stdin prompt";
let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let sink = std::sync::Arc::clone(&seen);
ExecCommand::from_stdin(prompt)
.stream(&codex, move |event| {
if let Some(text) = event.agent_message_text() {
sink.lock().unwrap().push(text);
}
})
.await
.unwrap();
assert_eq!(seen.lock().unwrap().as_slice(), &[prompt.to_string()]);
}
#[cfg(all(unix, feature = "json"))]
#[tokio::test]
async fn a_prompt_larger_than_the_pipe_buffer_still_completes() {
let codex = echoing_stdin_codex();
let prompt = "x".repeat(512 * 1024);
let result = ExecCommand::from_stdin(&prompt)
.execute_json(&codex)
.await
.unwrap();
assert_eq!(result.result.len(), prompt.len());
}
#[cfg(all(unix, feature = "json"))]
fn echoing_stdin_codex() -> Codex {
let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fake-codex-echo-stdin.sh");
Codex::builder()
.binary("/bin/bash")
.arg(script.to_str().unwrap())
.build()
.expect("bash must exist")
}
#[test]
fn from_stdin_emits_the_dash_positional_not_the_prompt() {
let args = ExecCommand::from_stdin("secret prompt").ephemeral().args();
assert_eq!(args, vec!["exec", "--ephemeral", "-"]);
assert!(!args.iter().any(|a| a.contains("secret")));
}
#[test]
fn prompt_via_stdin_converts_an_existing_prompt() {
let args = ExecCommand::new("hello").prompt_via_stdin().args();
assert_eq!(args, vec!["exec", "-"]);
}
}