mirror of
https://github.com/openai/codex.git
synced 2026-04-04 22:41:48 +03:00
Compare commits
1 Commits
pr16585
...
jif/single
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25fefe8735 |
6
.github/scripts/run-bazel-ci.sh
vendored
6
.github/scripts/run-bazel-ci.sh
vendored
@@ -146,12 +146,6 @@ if [[ -n "${BAZEL_REPOSITORY_CACHE:-}" ]]; then
|
||||
post_config_bazel_args+=("--repository_cache=${BAZEL_REPOSITORY_CACHE}")
|
||||
fi
|
||||
|
||||
if [[ -n "${CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR:-}" ]]; then
|
||||
post_config_bazel_args+=(
|
||||
"--execution_log_compact_file=${CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR}/execution-log-${bazel_args[0]}-${GITHUB_JOB:-local}-$$.zst"
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
windows_action_env_vars=(
|
||||
INCLUDE
|
||||
|
||||
30
.github/workflows/bazel.yml
vendored
30
.github/workflows/bazel.yml
vendored
@@ -63,12 +63,6 @@ jobs:
|
||||
shell: bash
|
||||
run: ./scripts/check-module-bazel-lock.sh
|
||||
|
||||
- name: Set up Bazel execution logs
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p "${RUNNER_TEMP}/bazel-execution-logs"
|
||||
echo "CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR=${RUNNER_TEMP}/bazel-execution-logs" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: bazel test //...
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
@@ -93,15 +87,6 @@ jobs:
|
||||
-- \
|
||||
"${bazel_targets[@]}"
|
||||
|
||||
- name: Upload Bazel execution logs
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: bazel-execution-logs-test-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Save bazel repository cache explicitly; make non-fatal so cache uploading
|
||||
# never fails the overall job. Only save when key wasn't hit.
|
||||
- name: Save bazel repository cache
|
||||
@@ -141,12 +126,6 @@ jobs:
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: Set up Bazel execution logs
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p "${RUNNER_TEMP}/bazel-execution-logs"
|
||||
echo "CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR=${RUNNER_TEMP}/bazel-execution-logs" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: bazel build --config=clippy //codex-rs/...
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
@@ -164,15 +143,6 @@ jobs:
|
||||
//codex-rs/... \
|
||||
-//codex-rs/v8-poc:all
|
||||
|
||||
- name: Upload Bazel execution logs
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: bazel-execution-logs-clippy-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Save bazel repository cache explicitly; make non-fatal so cache uploading
|
||||
# never fails the overall job. Only save when key wasn't hit.
|
||||
- name: Save bazel repository cache
|
||||
|
||||
@@ -71,7 +71,6 @@ single_version_override(
|
||||
patch_strip = 1,
|
||||
patches = [
|
||||
"//patches:rules_rs_windows_gnullvm_exec.patch",
|
||||
"//patches:rules_rs_delete_git_worktree_pointer.patch",
|
||||
],
|
||||
version = "0.0.43",
|
||||
)
|
||||
|
||||
@@ -256,5 +256,60 @@ pub enum Color {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "cli_tests.rs"]
|
||||
mod tests;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn resume_parses_prompt_after_global_flags() {
|
||||
const PROMPT: &str = "echo resume-with-global-flags-after-subcommand";
|
||||
let cli = Cli::parse_from([
|
||||
"codex-exec",
|
||||
"resume",
|
||||
"--last",
|
||||
"--json",
|
||||
"--model",
|
||||
"gpt-5.2-codex",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--skip-git-repo-check",
|
||||
"--ephemeral",
|
||||
PROMPT,
|
||||
]);
|
||||
|
||||
assert!(cli.ephemeral);
|
||||
let Some(Command::Resume(args)) = cli.command else {
|
||||
panic!("expected resume command");
|
||||
};
|
||||
let effective_prompt = args.prompt.clone().or_else(|| {
|
||||
if args.last {
|
||||
args.session_id.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
assert_eq!(effective_prompt.as_deref(), Some(PROMPT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_accepts_output_last_message_flag_after_subcommand() {
|
||||
const PROMPT: &str = "echo resume-with-output-file";
|
||||
let cli = Cli::parse_from([
|
||||
"codex-exec",
|
||||
"resume",
|
||||
"session-123",
|
||||
"-o",
|
||||
"/tmp/resume-output.md",
|
||||
PROMPT,
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
cli.last_message_file,
|
||||
Some(PathBuf::from("/tmp/resume-output.md"))
|
||||
);
|
||||
let Some(Command::Resume(args)) = cli.command else {
|
||||
panic!("expected resume command");
|
||||
};
|
||||
assert_eq!(args.session_id.as_deref(), Some("session-123"));
|
||||
assert_eq!(args.prompt.as_deref(), Some(PROMPT));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn resume_parses_prompt_after_global_flags() {
|
||||
const PROMPT: &str = "echo resume-with-global-flags-after-subcommand";
|
||||
let cli = Cli::parse_from([
|
||||
"codex-exec",
|
||||
"resume",
|
||||
"--last",
|
||||
"--json",
|
||||
"--model",
|
||||
"gpt-5.2-codex",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--skip-git-repo-check",
|
||||
"--ephemeral",
|
||||
PROMPT,
|
||||
]);
|
||||
|
||||
assert!(cli.ephemeral);
|
||||
let Some(Command::Resume(args)) = cli.command else {
|
||||
panic!("expected resume command");
|
||||
};
|
||||
let effective_prompt = args.prompt.clone().or_else(|| {
|
||||
if args.last {
|
||||
args.session_id.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
assert_eq!(effective_prompt.as_deref(), Some(PROMPT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_accepts_output_last_message_flag_after_subcommand() {
|
||||
const PROMPT: &str = "echo resume-with-output-file";
|
||||
let cli = Cli::parse_from([
|
||||
"codex-exec",
|
||||
"resume",
|
||||
"session-123",
|
||||
"-o",
|
||||
"/tmp/resume-output.md",
|
||||
PROMPT,
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
cli.last_message_file,
|
||||
Some(PathBuf::from("/tmp/resume-output.md"))
|
||||
);
|
||||
let Some(Command::Resume(args)) = cli.command else {
|
||||
panic!("expected resume command");
|
||||
};
|
||||
assert_eq!(args.session_id.as_deref(), Some("session-123"));
|
||||
assert_eq!(args.prompt.as_deref(), Some(PROMPT));
|
||||
}
|
||||
@@ -564,5 +564,350 @@ fn should_print_final_message_to_tty(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "event_processor_with_human_output_tests.rs"]
|
||||
mod tests;
|
||||
mod tests {
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use owo_colors::Style;
|
||||
|
||||
use super::EventProcessorWithHumanOutput;
|
||||
use super::final_message_from_turn_items;
|
||||
use super::reasoning_text;
|
||||
use super::should_print_final_message_to_stdout;
|
||||
use super::should_print_final_message_to_tty;
|
||||
use crate::event_processor::EventProcessor;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
|
||||
#[test]
|
||||
fn suppresses_final_stdout_message_when_both_streams_are_terminals() {
|
||||
assert!(!should_print_final_message_to_stdout(
|
||||
Some("hello"),
|
||||
/*stdout_is_terminal*/ true,
|
||||
/*stderr_is_terminal*/ true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prints_final_stdout_message_when_stdout_is_not_terminal() {
|
||||
assert!(should_print_final_message_to_stdout(
|
||||
Some("hello"),
|
||||
/*stdout_is_terminal*/ false,
|
||||
/*stderr_is_terminal*/ true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prints_final_stdout_message_when_stderr_is_not_terminal() {
|
||||
assert!(should_print_final_message_to_stdout(
|
||||
Some("hello"),
|
||||
/*stdout_is_terminal*/ true,
|
||||
/*stderr_is_terminal*/ false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suppresses_final_stdout_message_when_missing() {
|
||||
assert!(!should_print_final_message_to_stdout(
|
||||
/*final_message*/ None, /*stdout_is_terminal*/ false,
|
||||
/*stderr_is_terminal*/ false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prints_final_tty_message_when_not_yet_rendered() {
|
||||
assert!(should_print_final_message_to_tty(
|
||||
Some("hello"),
|
||||
/*final_message_rendered*/ false,
|
||||
/*stdout_is_terminal*/ true,
|
||||
/*stderr_is_terminal*/ true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suppresses_final_tty_message_when_already_rendered() {
|
||||
assert!(!should_print_final_message_to_tty(
|
||||
Some("hello"),
|
||||
/*final_message_rendered*/ true,
|
||||
/*stdout_is_terminal*/ true,
|
||||
/*stderr_is_terminal*/ true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_text_prefers_summary_when_raw_reasoning_is_hidden() {
|
||||
let text = reasoning_text(
|
||||
&["summary".to_string()],
|
||||
&["raw".to_string()],
|
||||
/*show_raw_agent_reasoning*/ false,
|
||||
);
|
||||
|
||||
assert_eq!(text.as_deref(), Some("summary"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_text_uses_raw_content_when_enabled() {
|
||||
let text = reasoning_text(
|
||||
&["summary".to_string()],
|
||||
&["raw".to_string()],
|
||||
/*show_raw_agent_reasoning*/ true,
|
||||
);
|
||||
|
||||
assert_eq!(text.as_deref(), Some("raw"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_message_from_turn_items_uses_latest_agent_message() {
|
||||
let message = final_message_from_turn_items(&[
|
||||
ThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
text: "plan".to_string(),
|
||||
},
|
||||
ThreadItem::AgentMessage {
|
||||
id: "msg-2".to_string(),
|
||||
text: "second".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(message.as_deref(), Some("second"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_message_from_turn_items_falls_back_to_latest_plan() {
|
||||
let message = final_message_from_turn_items(&[
|
||||
ThreadItem::Reasoning {
|
||||
id: "reasoning-1".to_string(),
|
||||
summary: vec!["inspect".to_string()],
|
||||
content: Vec::new(),
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
text: "first plan".to_string(),
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
id: "plan-2".to_string(),
|
||||
text: "final plan".to_string(),
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(message.as_deref(), Some("final plan"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_completed_recovers_final_message_from_turn_items() {
|
||||
let mut processor = EventProcessorWithHumanOutput {
|
||||
bold: Style::new(),
|
||||
cyan: Style::new(),
|
||||
dimmed: Style::new(),
|
||||
green: Style::new(),
|
||||
italic: Style::new(),
|
||||
magenta: Style::new(),
|
||||
red: Style::new(),
|
||||
yellow: Style::new(),
|
||||
show_agent_reasoning: true,
|
||||
show_raw_agent_reasoning: false,
|
||||
last_message_path: None,
|
||||
final_message: None,
|
||||
final_message_rendered: false,
|
||||
emit_final_message_on_shutdown: false,
|
||||
last_total_token_usage: None,
|
||||
};
|
||||
|
||||
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
|
||||
codex_app_server_protocol::TurnCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: vec![ThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
crate::event_processor::CodexStatus::InitiateShutdown
|
||||
);
|
||||
assert_eq!(processor.final_message.as_deref(), Some("final answer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_completed_overwrites_stale_final_message_from_turn_items() {
|
||||
let mut processor = EventProcessorWithHumanOutput {
|
||||
bold: Style::new(),
|
||||
cyan: Style::new(),
|
||||
dimmed: Style::new(),
|
||||
green: Style::new(),
|
||||
italic: Style::new(),
|
||||
magenta: Style::new(),
|
||||
red: Style::new(),
|
||||
yellow: Style::new(),
|
||||
show_agent_reasoning: true,
|
||||
show_raw_agent_reasoning: false,
|
||||
last_message_path: None,
|
||||
final_message: Some("stale answer".to_string()),
|
||||
final_message_rendered: true,
|
||||
emit_final_message_on_shutdown: false,
|
||||
last_total_token_usage: None,
|
||||
};
|
||||
|
||||
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
|
||||
codex_app_server_protocol::TurnCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: vec![ThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
crate::event_processor::CodexStatus::InitiateShutdown
|
||||
);
|
||||
assert_eq!(processor.final_message.as_deref(), Some("final answer"));
|
||||
assert!(!processor.final_message_rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_completed_preserves_streamed_final_message_when_turn_items_are_empty() {
|
||||
let mut processor = EventProcessorWithHumanOutput {
|
||||
bold: Style::new(),
|
||||
cyan: Style::new(),
|
||||
dimmed: Style::new(),
|
||||
green: Style::new(),
|
||||
italic: Style::new(),
|
||||
magenta: Style::new(),
|
||||
red: Style::new(),
|
||||
yellow: Style::new(),
|
||||
show_agent_reasoning: true,
|
||||
show_raw_agent_reasoning: false,
|
||||
last_message_path: None,
|
||||
final_message: Some("streamed answer".to_string()),
|
||||
final_message_rendered: false,
|
||||
emit_final_message_on_shutdown: false,
|
||||
last_total_token_usage: None,
|
||||
};
|
||||
|
||||
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
|
||||
codex_app_server_protocol::TurnCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
crate::event_processor::CodexStatus::InitiateShutdown
|
||||
);
|
||||
assert_eq!(processor.final_message.as_deref(), Some("streamed answer"));
|
||||
assert!(processor.emit_final_message_on_shutdown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_failed_clears_stale_final_message() {
|
||||
let mut processor = EventProcessorWithHumanOutput {
|
||||
bold: Style::new(),
|
||||
cyan: Style::new(),
|
||||
dimmed: Style::new(),
|
||||
green: Style::new(),
|
||||
italic: Style::new(),
|
||||
magenta: Style::new(),
|
||||
red: Style::new(),
|
||||
yellow: Style::new(),
|
||||
show_agent_reasoning: true,
|
||||
show_raw_agent_reasoning: false,
|
||||
last_message_path: None,
|
||||
final_message: Some("partial answer".to_string()),
|
||||
final_message_rendered: true,
|
||||
emit_final_message_on_shutdown: true,
|
||||
last_total_token_usage: None,
|
||||
};
|
||||
|
||||
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
|
||||
codex_app_server_protocol::TurnCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Failed,
|
||||
error: None,
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
crate::event_processor::CodexStatus::InitiateShutdown
|
||||
);
|
||||
assert_eq!(processor.final_message, None);
|
||||
assert!(!processor.final_message_rendered);
|
||||
assert!(!processor.emit_final_message_on_shutdown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_interrupted_clears_stale_final_message() {
|
||||
let mut processor = EventProcessorWithHumanOutput {
|
||||
bold: Style::new(),
|
||||
cyan: Style::new(),
|
||||
dimmed: Style::new(),
|
||||
green: Style::new(),
|
||||
italic: Style::new(),
|
||||
magenta: Style::new(),
|
||||
red: Style::new(),
|
||||
yellow: Style::new(),
|
||||
show_agent_reasoning: true,
|
||||
show_raw_agent_reasoning: false,
|
||||
last_message_path: None,
|
||||
final_message: Some("partial answer".to_string()),
|
||||
final_message_rendered: true,
|
||||
emit_final_message_on_shutdown: true,
|
||||
last_total_token_usage: None,
|
||||
};
|
||||
|
||||
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
|
||||
codex_app_server_protocol::TurnCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Interrupted,
|
||||
error: None,
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
crate::event_processor::CodexStatus::InitiateShutdown
|
||||
);
|
||||
assert_eq!(processor.final_message, None);
|
||||
assert!(!processor.final_message_rendered);
|
||||
assert!(!processor.emit_final_message_on_shutdown);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use owo_colors::Style;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::EventProcessorWithHumanOutput;
|
||||
use super::final_message_from_turn_items;
|
||||
use super::reasoning_text;
|
||||
use super::should_print_final_message_to_stdout;
|
||||
use super::should_print_final_message_to_tty;
|
||||
use crate::event_processor::EventProcessor;
|
||||
|
||||
#[test]
|
||||
fn suppresses_final_stdout_message_when_both_streams_are_terminals() {
|
||||
assert!(!should_print_final_message_to_stdout(
|
||||
Some("hello"),
|
||||
/*stdout_is_terminal*/ true,
|
||||
/*stderr_is_terminal*/ true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prints_final_stdout_message_when_stdout_is_not_terminal() {
|
||||
assert!(should_print_final_message_to_stdout(
|
||||
Some("hello"),
|
||||
/*stdout_is_terminal*/ false,
|
||||
/*stderr_is_terminal*/ true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prints_final_stdout_message_when_stderr_is_not_terminal() {
|
||||
assert!(should_print_final_message_to_stdout(
|
||||
Some("hello"),
|
||||
/*stdout_is_terminal*/ true,
|
||||
/*stderr_is_terminal*/ false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suppresses_final_stdout_message_when_missing() {
|
||||
assert!(!should_print_final_message_to_stdout(
|
||||
/*final_message*/ None, /*stdout_is_terminal*/ false,
|
||||
/*stderr_is_terminal*/ false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prints_final_tty_message_when_not_yet_rendered() {
|
||||
assert!(should_print_final_message_to_tty(
|
||||
Some("hello"),
|
||||
/*final_message_rendered*/ false,
|
||||
/*stdout_is_terminal*/ true,
|
||||
/*stderr_is_terminal*/ true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suppresses_final_tty_message_when_already_rendered() {
|
||||
assert!(!should_print_final_message_to_tty(
|
||||
Some("hello"),
|
||||
/*final_message_rendered*/ true,
|
||||
/*stdout_is_terminal*/ true,
|
||||
/*stderr_is_terminal*/ true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_text_prefers_summary_when_raw_reasoning_is_hidden() {
|
||||
let text = reasoning_text(
|
||||
&["summary".to_string()],
|
||||
&["raw".to_string()],
|
||||
/*show_raw_agent_reasoning*/ false,
|
||||
);
|
||||
|
||||
assert_eq!(text.as_deref(), Some("summary"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_text_uses_raw_content_when_enabled() {
|
||||
let text = reasoning_text(
|
||||
&["summary".to_string()],
|
||||
&["raw".to_string()],
|
||||
/*show_raw_agent_reasoning*/ true,
|
||||
);
|
||||
|
||||
assert_eq!(text.as_deref(), Some("raw"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_message_from_turn_items_uses_latest_agent_message() {
|
||||
let message = final_message_from_turn_items(&[
|
||||
ThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
text: "plan".to_string(),
|
||||
},
|
||||
ThreadItem::AgentMessage {
|
||||
id: "msg-2".to_string(),
|
||||
text: "second".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(message.as_deref(), Some("second"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_message_from_turn_items_falls_back_to_latest_plan() {
|
||||
let message = final_message_from_turn_items(&[
|
||||
ThreadItem::Reasoning {
|
||||
id: "reasoning-1".to_string(),
|
||||
summary: vec!["inspect".to_string()],
|
||||
content: Vec::new(),
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
text: "first plan".to_string(),
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
id: "plan-2".to_string(),
|
||||
text: "final plan".to_string(),
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(message.as_deref(), Some("final plan"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_completed_recovers_final_message_from_turn_items() {
|
||||
let mut processor = EventProcessorWithHumanOutput {
|
||||
bold: Style::new(),
|
||||
cyan: Style::new(),
|
||||
dimmed: Style::new(),
|
||||
green: Style::new(),
|
||||
italic: Style::new(),
|
||||
magenta: Style::new(),
|
||||
red: Style::new(),
|
||||
yellow: Style::new(),
|
||||
show_agent_reasoning: true,
|
||||
show_raw_agent_reasoning: false,
|
||||
last_message_path: None,
|
||||
final_message: None,
|
||||
final_message_rendered: false,
|
||||
emit_final_message_on_shutdown: false,
|
||||
last_total_token_usage: None,
|
||||
};
|
||||
|
||||
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
|
||||
codex_app_server_protocol::TurnCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: vec![ThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
crate::event_processor::CodexStatus::InitiateShutdown
|
||||
);
|
||||
assert_eq!(processor.final_message.as_deref(), Some("final answer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_completed_overwrites_stale_final_message_from_turn_items() {
|
||||
let mut processor = EventProcessorWithHumanOutput {
|
||||
bold: Style::new(),
|
||||
cyan: Style::new(),
|
||||
dimmed: Style::new(),
|
||||
green: Style::new(),
|
||||
italic: Style::new(),
|
||||
magenta: Style::new(),
|
||||
red: Style::new(),
|
||||
yellow: Style::new(),
|
||||
show_agent_reasoning: true,
|
||||
show_raw_agent_reasoning: false,
|
||||
last_message_path: None,
|
||||
final_message: Some("stale answer".to_string()),
|
||||
final_message_rendered: true,
|
||||
emit_final_message_on_shutdown: false,
|
||||
last_total_token_usage: None,
|
||||
};
|
||||
|
||||
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
|
||||
codex_app_server_protocol::TurnCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: vec![ThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
crate::event_processor::CodexStatus::InitiateShutdown
|
||||
);
|
||||
assert_eq!(processor.final_message.as_deref(), Some("final answer"));
|
||||
assert!(!processor.final_message_rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_completed_preserves_streamed_final_message_when_turn_items_are_empty() {
|
||||
let mut processor = EventProcessorWithHumanOutput {
|
||||
bold: Style::new(),
|
||||
cyan: Style::new(),
|
||||
dimmed: Style::new(),
|
||||
green: Style::new(),
|
||||
italic: Style::new(),
|
||||
magenta: Style::new(),
|
||||
red: Style::new(),
|
||||
yellow: Style::new(),
|
||||
show_agent_reasoning: true,
|
||||
show_raw_agent_reasoning: false,
|
||||
last_message_path: None,
|
||||
final_message: Some("streamed answer".to_string()),
|
||||
final_message_rendered: false,
|
||||
emit_final_message_on_shutdown: false,
|
||||
last_total_token_usage: None,
|
||||
};
|
||||
|
||||
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
|
||||
codex_app_server_protocol::TurnCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
crate::event_processor::CodexStatus::InitiateShutdown
|
||||
);
|
||||
assert_eq!(processor.final_message.as_deref(), Some("streamed answer"));
|
||||
assert!(processor.emit_final_message_on_shutdown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_failed_clears_stale_final_message() {
|
||||
let mut processor = EventProcessorWithHumanOutput {
|
||||
bold: Style::new(),
|
||||
cyan: Style::new(),
|
||||
dimmed: Style::new(),
|
||||
green: Style::new(),
|
||||
italic: Style::new(),
|
||||
magenta: Style::new(),
|
||||
red: Style::new(),
|
||||
yellow: Style::new(),
|
||||
show_agent_reasoning: true,
|
||||
show_raw_agent_reasoning: false,
|
||||
last_message_path: None,
|
||||
final_message: Some("partial answer".to_string()),
|
||||
final_message_rendered: true,
|
||||
emit_final_message_on_shutdown: true,
|
||||
last_total_token_usage: None,
|
||||
};
|
||||
|
||||
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
|
||||
codex_app_server_protocol::TurnCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Failed,
|
||||
error: None,
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
crate::event_processor::CodexStatus::InitiateShutdown
|
||||
);
|
||||
assert_eq!(processor.final_message, None);
|
||||
assert!(!processor.final_message_rendered);
|
||||
assert!(!processor.emit_final_message_on_shutdown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_interrupted_clears_stale_final_message() {
|
||||
let mut processor = EventProcessorWithHumanOutput {
|
||||
bold: Style::new(),
|
||||
cyan: Style::new(),
|
||||
dimmed: Style::new(),
|
||||
green: Style::new(),
|
||||
italic: Style::new(),
|
||||
magenta: Style::new(),
|
||||
red: Style::new(),
|
||||
yellow: Style::new(),
|
||||
show_agent_reasoning: true,
|
||||
show_raw_agent_reasoning: false,
|
||||
last_message_path: None,
|
||||
final_message: Some("partial answer".to_string()),
|
||||
final_message_rendered: true,
|
||||
emit_final_message_on_shutdown: true,
|
||||
last_total_token_usage: None,
|
||||
};
|
||||
|
||||
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
|
||||
codex_app_server_protocol::TurnCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Interrupted,
|
||||
error: None,
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
crate::event_processor::CodexStatus::InitiateShutdown
|
||||
);
|
||||
assert_eq!(processor.final_message, None);
|
||||
assert!(!processor.final_message_rendered);
|
||||
assert!(!processor.emit_final_message_on_shutdown);
|
||||
}
|
||||
@@ -621,5 +621,59 @@ impl EventProcessor for EventProcessorWithJsonOutput {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "event_processor_with_jsonl_output_tests.rs"]
|
||||
mod tests;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn failed_turn_does_not_overwrite_output_last_message_file() {
|
||||
let tempdir = tempdir().expect("create tempdir");
|
||||
let output_path = tempdir.path().join("last-message.txt");
|
||||
std::fs::write(&output_path, "keep existing contents").expect("seed output file");
|
||||
|
||||
let mut processor = EventProcessorWithJsonOutput::new(Some(output_path.clone()));
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
codex_app_server_protocol::ItemCompletedNotification {
|
||||
item: ThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "partial answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(collected.status, CodexStatus::Running);
|
||||
assert_eq!(processor.final_message(), Some("partial answer"));
|
||||
|
||||
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
|
||||
codex_app_server_protocol::TurnCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: codex_app_server_protocol::Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Failed,
|
||||
error: Some(codex_app_server_protocol::TurnError {
|
||||
message: "turn failed".to_string(),
|
||||
additional_details: None,
|
||||
codex_error_info: None,
|
||||
}),
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(status, CodexStatus::InitiateShutdown);
|
||||
assert_eq!(processor.final_message(), None);
|
||||
|
||||
EventProcessor::print_final_output(&mut processor);
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&output_path).expect("read output file"),
|
||||
"keep existing contents"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn failed_turn_does_not_overwrite_output_last_message_file() {
|
||||
let tempdir = tempdir().expect("create tempdir");
|
||||
let output_path = tempdir.path().join("last-message.txt");
|
||||
std::fs::write(&output_path, "keep existing contents").expect("seed output file");
|
||||
|
||||
let mut processor = EventProcessorWithJsonOutput::new(Some(output_path.clone()));
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
codex_app_server_protocol::ItemCompletedNotification {
|
||||
item: ThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "partial answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(collected.status, CodexStatus::Running);
|
||||
assert_eq!(processor.final_message(), Some("partial answer"));
|
||||
|
||||
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
|
||||
codex_app_server_protocol::TurnCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: codex_app_server_protocol::Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Failed,
|
||||
error: Some(codex_app_server_protocol::TurnError {
|
||||
message: "turn failed".to_string(),
|
||||
additional_details: None,
|
||||
codex_error_info: None,
|
||||
}),
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(status, CodexStatus::InitiateShutdown);
|
||||
assert_eq!(processor.final_message(), None);
|
||||
|
||||
EventProcessor::print_final_output(&mut processor);
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&output_path).expect("read output file"),
|
||||
"keep existing contents"
|
||||
);
|
||||
}
|
||||
@@ -1657,5 +1657,407 @@ fn build_review_request(args: &ReviewArgs) -> anyhow::Result<ReviewRequest> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lib_tests.rs"]
|
||||
mod tests;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_otel::set_parent_from_w3c_trace_context;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use opentelemetry::trace::TraceContextExt;
|
||||
use opentelemetry::trace::TraceId;
|
||||
use opentelemetry::trace::TracerProvider as _;
|
||||
use opentelemetry_sdk::trace::SdkTracerProvider;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::tempdir;
|
||||
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||
|
||||
fn test_tracing_subscriber() -> impl tracing::Subscriber + Send + Sync {
|
||||
let provider = SdkTracerProvider::builder().build();
|
||||
let tracer = provider.tracer("codex-exec-tests");
|
||||
tracing_subscriber::registry().with(tracing_opentelemetry::layer().with_tracer(tracer))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_defaults_analytics_to_enabled() {
|
||||
assert_eq!(DEFAULT_ANALYTICS_ENABLED, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_root_span_can_be_parented_from_trace_context() {
|
||||
let subscriber = test_tracing_subscriber();
|
||||
let _guard = tracing::subscriber::set_default(subscriber);
|
||||
|
||||
let parent = codex_protocol::protocol::W3cTraceContext {
|
||||
traceparent: Some("00-00000000000000000000000000000077-0000000000000088-01".into()),
|
||||
tracestate: Some("vendor=value".into()),
|
||||
};
|
||||
let exec_span = exec_root_span();
|
||||
assert!(set_parent_from_w3c_trace_context(&exec_span, &parent));
|
||||
|
||||
let trace_id = exec_span.context().span().span_context().trace_id();
|
||||
assert_eq!(
|
||||
trace_id,
|
||||
TraceId::from_hex("00000000000000000000000000000077").expect("trace id")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_uncommitted_review_request() {
|
||||
let args = ReviewArgs {
|
||||
uncommitted: true,
|
||||
base: None,
|
||||
commit: None,
|
||||
commit_title: None,
|
||||
prompt: None,
|
||||
};
|
||||
let request = build_review_request(&args).expect("builds uncommitted review request");
|
||||
|
||||
let expected = ReviewRequest {
|
||||
target: ReviewTarget::UncommittedChanges,
|
||||
user_facing_hint: None,
|
||||
};
|
||||
|
||||
assert_eq!(request, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_commit_review_request_with_title() {
|
||||
let args = ReviewArgs {
|
||||
uncommitted: false,
|
||||
base: None,
|
||||
commit: Some("123456789".to_string()),
|
||||
commit_title: Some("Add review command".to_string()),
|
||||
prompt: None,
|
||||
};
|
||||
let request = build_review_request(&args).expect("builds commit review request");
|
||||
|
||||
let expected = ReviewRequest {
|
||||
target: ReviewTarget::Commit {
|
||||
sha: "123456789".to_string(),
|
||||
title: Some("Add review command".to_string()),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
};
|
||||
|
||||
assert_eq!(request, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_custom_review_request_trims_prompt() {
|
||||
let args = ReviewArgs {
|
||||
uncommitted: false,
|
||||
base: None,
|
||||
commit: None,
|
||||
commit_title: None,
|
||||
prompt: Some(" custom review instructions ".to_string()),
|
||||
};
|
||||
let request = build_review_request(&args).expect("builds custom review request");
|
||||
|
||||
let expected = ReviewRequest {
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "custom review instructions".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
};
|
||||
|
||||
assert_eq!(request, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_prompt_bytes_strips_utf8_bom() {
|
||||
let input = [0xEF, 0xBB, 0xBF, b'h', b'i', b'\n'];
|
||||
|
||||
let out = decode_prompt_bytes(&input).expect("decode utf-8 with BOM");
|
||||
|
||||
assert_eq!(out, "hi\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_prompt_bytes_decodes_utf16le_bom() {
|
||||
// UTF-16LE BOM + "hi\n"
|
||||
let input = [0xFF, 0xFE, b'h', 0x00, b'i', 0x00, b'\n', 0x00];
|
||||
|
||||
let out = decode_prompt_bytes(&input).expect("decode utf-16le with BOM");
|
||||
|
||||
assert_eq!(out, "hi\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_prompt_bytes_decodes_utf16be_bom() {
|
||||
// UTF-16BE BOM + "hi\n"
|
||||
let input = [0xFE, 0xFF, 0x00, b'h', 0x00, b'i', 0x00, b'\n'];
|
||||
|
||||
let out = decode_prompt_bytes(&input).expect("decode utf-16be with BOM");
|
||||
|
||||
assert_eq!(out, "hi\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_prompt_bytes_rejects_utf32le_bom() {
|
||||
// UTF-32LE BOM + "hi\n"
|
||||
let input = [
|
||||
0xFF, 0xFE, 0x00, 0x00, b'h', 0x00, 0x00, 0x00, b'i', 0x00, 0x00, 0x00, b'\n', 0x00,
|
||||
0x00, 0x00,
|
||||
];
|
||||
|
||||
let err = decode_prompt_bytes(&input).expect_err("utf-32le should be rejected");
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
PromptDecodeError::UnsupportedBom {
|
||||
encoding: "UTF-32LE"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_prompt_bytes_rejects_utf32be_bom() {
|
||||
// UTF-32BE BOM + "hi\n"
|
||||
let input = [
|
||||
0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, b'h', 0x00, 0x00, 0x00, b'i', 0x00, 0x00,
|
||||
0x00, b'\n',
|
||||
];
|
||||
|
||||
let err = decode_prompt_bytes(&input).expect_err("utf-32be should be rejected");
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
PromptDecodeError::UnsupportedBom {
|
||||
encoding: "UTF-32BE"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_prompt_bytes_rejects_invalid_utf8() {
|
||||
// Invalid UTF-8 sequence: 0xC3 0x28
|
||||
let input = [0xC3, 0x28];
|
||||
|
||||
let err = decode_prompt_bytes(&input).expect_err("invalid utf-8 should fail");
|
||||
|
||||
assert_eq!(err, PromptDecodeError::InvalidUtf8 { valid_up_to: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_with_stdin_context_wraps_stdin_block() {
|
||||
let combined = prompt_with_stdin_context("Summarize this concisely", "my output");
|
||||
|
||||
assert_eq!(
|
||||
combined,
|
||||
"Summarize this concisely\n\n<stdin>\nmy output\n</stdin>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_with_stdin_context_preserves_trailing_newline() {
|
||||
let combined = prompt_with_stdin_context("Summarize this concisely", "my output\n");
|
||||
|
||||
assert_eq!(
|
||||
combined,
|
||||
"Summarize this concisely\n\n<stdin>\nmy output\n</stdin>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lagged_event_warning_message_is_explicit() {
|
||||
assert_eq!(
|
||||
lagged_event_warning_message(/*skipped*/ 7),
|
||||
"in-process app-server event stream lagged; dropped 7 events".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_lookup_model_providers_filters_only_last_lookup() {
|
||||
let codex_home = tempdir().expect("create temp codex home");
|
||||
let cwd = tempdir().expect("create temp cwd");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(cwd.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("build default config");
|
||||
config.model_provider_id = "test-provider".to_string();
|
||||
|
||||
let last_args = crate::cli::ResumeArgs {
|
||||
session_id: None,
|
||||
last: true,
|
||||
all: false,
|
||||
images: vec![],
|
||||
prompt: None,
|
||||
};
|
||||
let named_args = crate::cli::ResumeArgs {
|
||||
session_id: Some("named-session".to_string()),
|
||||
last: false,
|
||||
all: false,
|
||||
images: vec![],
|
||||
prompt: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resume_lookup_model_providers(&config, &last_args),
|
||||
Some(vec!["test-provider".to_string()])
|
||||
);
|
||||
assert_eq!(resume_lookup_model_providers(&config, &named_args), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
let thread = AppServerThread {
|
||||
id: "thread-1".to_string(),
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
status: codex_app_server_protocol::ThreadStatus::Idle,
|
||||
path: None,
|
||||
cwd: PathBuf::from("/tmp/project"),
|
||||
cli_version: "0.0.0-test".to_string(),
|
||||
source: codex_app_server_protocol::SessionSource::Exec,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
git_info: None,
|
||||
name: None,
|
||||
turns: vec![
|
||||
codex_app_server_protocol::Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: vec![AppServerThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}],
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
error: None,
|
||||
},
|
||||
codex_app_server_protocol::Turn {
|
||||
id: "turn-2".to_string(),
|
||||
items: vec![AppServerThreadItem::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
text: "ship it".to_string(),
|
||||
}],
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
error: None,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
turn_items_for_thread(&thread, "turn-1"),
|
||||
Some(vec![AppServerThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}])
|
||||
);
|
||||
assert_eq!(turn_items_for_thread(&thread, "missing-turn"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canceled_mcp_server_elicitation_response_uses_cancel_action() {
|
||||
let value = canceled_mcp_server_elicitation_response()
|
||||
.expect("mcp elicitation cancel response should serialize");
|
||||
let response: McpServerElicitationRequestResponse =
|
||||
serde_json::from_value(value).expect("cancel response should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
McpServerElicitationRequestResponse {
|
||||
action: McpServerElicitationAction::Cancel,
|
||||
content: None,
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_params_include_review_policy_when_review_policy_is_manual_only() {
|
||||
let codex_home = tempdir().expect("create temp codex home");
|
||||
let cwd = tempdir().expect("create temp cwd");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
approvals_reviewer: Some(ApprovalsReviewer::User),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(cwd.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("build config with manual-only review policy");
|
||||
|
||||
let params = thread_start_params_from_config(&config);
|
||||
|
||||
assert_eq!(
|
||||
params.approvals_reviewer,
|
||||
Some(codex_app_server_protocol::ApprovalsReviewer::User)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_params_include_review_policy_when_auto_review_is_enabled() {
|
||||
let codex_home = tempdir().expect("create temp codex home");
|
||||
let cwd = tempdir().expect("create temp cwd");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
approvals_reviewer: Some(ApprovalsReviewer::GuardianSubagent),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(cwd.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("build config with guardian review policy");
|
||||
|
||||
let params = thread_start_params_from_config(&config);
|
||||
|
||||
assert_eq!(
|
||||
params.approvals_reviewer,
|
||||
Some(codex_app_server_protocol::ApprovalsReviewer::GuardianSubagent)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_configured_from_thread_response_uses_review_policy_from_response() {
|
||||
let response = ThreadStartResponse {
|
||||
thread: codex_app_server_protocol::Thread {
|
||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
status: codex_app_server_protocol::ThreadStatus::Idle,
|
||||
path: Some(PathBuf::from("/tmp/rollout.jsonl")),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: codex_app_server_protocol::SessionSource::Cli,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
git_info: None,
|
||||
name: Some("thread".to_string()),
|
||||
turns: vec![],
|
||||
},
|
||||
model: "gpt-5.4".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
approval_policy: codex_app_server_protocol::AskForApproval::OnRequest,
|
||||
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::GuardianSubagent,
|
||||
sandbox: codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: codex_app_server_protocol::ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
},
|
||||
reasoning_effort: None,
|
||||
};
|
||||
|
||||
let event = session_configured_from_thread_start_response(&response)
|
||||
.expect("build bootstrap session configured event");
|
||||
|
||||
assert_eq!(
|
||||
event.approvals_reviewer,
|
||||
ApprovalsReviewer::GuardianSubagent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
use super::*;
|
||||
use codex_otel::set_parent_from_w3c_trace_context;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use opentelemetry::trace::TraceContextExt;
|
||||
use opentelemetry::trace::TraceId;
|
||||
use opentelemetry::trace::TracerProvider as _;
|
||||
use opentelemetry_sdk::trace::SdkTracerProvider;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::tempdir;
|
||||
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||
|
||||
fn test_tracing_subscriber() -> impl tracing::Subscriber + Send + Sync {
|
||||
let provider = SdkTracerProvider::builder().build();
|
||||
let tracer = provider.tracer("codex-exec-tests");
|
||||
tracing_subscriber::registry().with(tracing_opentelemetry::layer().with_tracer(tracer))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_defaults_analytics_to_enabled() {
|
||||
assert_eq!(DEFAULT_ANALYTICS_ENABLED, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_root_span_can_be_parented_from_trace_context() {
|
||||
let subscriber = test_tracing_subscriber();
|
||||
let _guard = tracing::subscriber::set_default(subscriber);
|
||||
|
||||
let parent = codex_protocol::protocol::W3cTraceContext {
|
||||
traceparent: Some("00-00000000000000000000000000000077-0000000000000088-01".into()),
|
||||
tracestate: Some("vendor=value".into()),
|
||||
};
|
||||
let exec_span = exec_root_span();
|
||||
assert!(set_parent_from_w3c_trace_context(&exec_span, &parent));
|
||||
|
||||
let trace_id = exec_span.context().span().span_context().trace_id();
|
||||
assert_eq!(
|
||||
trace_id,
|
||||
TraceId::from_hex("00000000000000000000000000000077").expect("trace id")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_uncommitted_review_request() {
|
||||
let args = ReviewArgs {
|
||||
uncommitted: true,
|
||||
base: None,
|
||||
commit: None,
|
||||
commit_title: None,
|
||||
prompt: None,
|
||||
};
|
||||
let request = build_review_request(&args).expect("builds uncommitted review request");
|
||||
|
||||
let expected = ReviewRequest {
|
||||
target: ReviewTarget::UncommittedChanges,
|
||||
user_facing_hint: None,
|
||||
};
|
||||
|
||||
assert_eq!(request, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_commit_review_request_with_title() {
|
||||
let args = ReviewArgs {
|
||||
uncommitted: false,
|
||||
base: None,
|
||||
commit: Some("123456789".to_string()),
|
||||
commit_title: Some("Add review command".to_string()),
|
||||
prompt: None,
|
||||
};
|
||||
let request = build_review_request(&args).expect("builds commit review request");
|
||||
|
||||
let expected = ReviewRequest {
|
||||
target: ReviewTarget::Commit {
|
||||
sha: "123456789".to_string(),
|
||||
title: Some("Add review command".to_string()),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
};
|
||||
|
||||
assert_eq!(request, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_custom_review_request_trims_prompt() {
|
||||
let args = ReviewArgs {
|
||||
uncommitted: false,
|
||||
base: None,
|
||||
commit: None,
|
||||
commit_title: None,
|
||||
prompt: Some(" custom review instructions ".to_string()),
|
||||
};
|
||||
let request = build_review_request(&args).expect("builds custom review request");
|
||||
|
||||
let expected = ReviewRequest {
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "custom review instructions".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
};
|
||||
|
||||
assert_eq!(request, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_prompt_bytes_strips_utf8_bom() {
|
||||
let input = [0xEF, 0xBB, 0xBF, b'h', b'i', b'\n'];
|
||||
|
||||
let out = decode_prompt_bytes(&input).expect("decode utf-8 with BOM");
|
||||
|
||||
assert_eq!(out, "hi\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_prompt_bytes_decodes_utf16le_bom() {
|
||||
// UTF-16LE BOM + "hi\n"
|
||||
let input = [0xFF, 0xFE, b'h', 0x00, b'i', 0x00, b'\n', 0x00];
|
||||
|
||||
let out = decode_prompt_bytes(&input).expect("decode utf-16le with BOM");
|
||||
|
||||
assert_eq!(out, "hi\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_prompt_bytes_decodes_utf16be_bom() {
|
||||
// UTF-16BE BOM + "hi\n"
|
||||
let input = [0xFE, 0xFF, 0x00, b'h', 0x00, b'i', 0x00, b'\n'];
|
||||
|
||||
let out = decode_prompt_bytes(&input).expect("decode utf-16be with BOM");
|
||||
|
||||
assert_eq!(out, "hi\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_prompt_bytes_rejects_utf32le_bom() {
|
||||
// UTF-32LE BOM + "hi\n"
|
||||
let input = [
|
||||
0xFF, 0xFE, 0x00, 0x00, b'h', 0x00, 0x00, 0x00, b'i', 0x00, 0x00, 0x00, b'\n', 0x00, 0x00,
|
||||
0x00,
|
||||
];
|
||||
|
||||
let err = decode_prompt_bytes(&input).expect_err("utf-32le should be rejected");
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
PromptDecodeError::UnsupportedBom {
|
||||
encoding: "UTF-32LE"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_prompt_bytes_rejects_utf32be_bom() {
|
||||
// UTF-32BE BOM + "hi\n"
|
||||
let input = [
|
||||
0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, b'h', 0x00, 0x00, 0x00, b'i', 0x00, 0x00, 0x00,
|
||||
b'\n',
|
||||
];
|
||||
|
||||
let err = decode_prompt_bytes(&input).expect_err("utf-32be should be rejected");
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
PromptDecodeError::UnsupportedBom {
|
||||
encoding: "UTF-32BE"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_prompt_bytes_rejects_invalid_utf8() {
|
||||
// Invalid UTF-8 sequence: 0xC3 0x28
|
||||
let input = [0xC3, 0x28];
|
||||
|
||||
let err = decode_prompt_bytes(&input).expect_err("invalid utf-8 should fail");
|
||||
|
||||
assert_eq!(err, PromptDecodeError::InvalidUtf8 { valid_up_to: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_with_stdin_context_wraps_stdin_block() {
|
||||
let combined = prompt_with_stdin_context("Summarize this concisely", "my output");
|
||||
|
||||
assert_eq!(
|
||||
combined,
|
||||
"Summarize this concisely\n\n<stdin>\nmy output\n</stdin>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_with_stdin_context_preserves_trailing_newline() {
|
||||
let combined = prompt_with_stdin_context("Summarize this concisely", "my output\n");
|
||||
|
||||
assert_eq!(
|
||||
combined,
|
||||
"Summarize this concisely\n\n<stdin>\nmy output\n</stdin>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lagged_event_warning_message_is_explicit() {
|
||||
assert_eq!(
|
||||
lagged_event_warning_message(/*skipped*/ 7),
|
||||
"in-process app-server event stream lagged; dropped 7 events".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_lookup_model_providers_filters_only_last_lookup() {
|
||||
let codex_home = tempdir().expect("create temp codex home");
|
||||
let cwd = tempdir().expect("create temp cwd");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(cwd.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("build default config");
|
||||
config.model_provider_id = "test-provider".to_string();
|
||||
|
||||
let last_args = crate::cli::ResumeArgs {
|
||||
session_id: None,
|
||||
last: true,
|
||||
all: false,
|
||||
images: vec![],
|
||||
prompt: None,
|
||||
};
|
||||
let named_args = crate::cli::ResumeArgs {
|
||||
session_id: Some("named-session".to_string()),
|
||||
last: false,
|
||||
all: false,
|
||||
images: vec![],
|
||||
prompt: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resume_lookup_model_providers(&config, &last_args),
|
||||
Some(vec!["test-provider".to_string()])
|
||||
);
|
||||
assert_eq!(resume_lookup_model_providers(&config, &named_args), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
let thread = AppServerThread {
|
||||
id: "thread-1".to_string(),
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
status: codex_app_server_protocol::ThreadStatus::Idle,
|
||||
path: None,
|
||||
cwd: PathBuf::from("/tmp/project"),
|
||||
cli_version: "0.0.0-test".to_string(),
|
||||
source: codex_app_server_protocol::SessionSource::Exec,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
git_info: None,
|
||||
name: None,
|
||||
turns: vec![
|
||||
codex_app_server_protocol::Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: vec![AppServerThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}],
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
error: None,
|
||||
},
|
||||
codex_app_server_protocol::Turn {
|
||||
id: "turn-2".to_string(),
|
||||
items: vec![AppServerThreadItem::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
text: "ship it".to_string(),
|
||||
}],
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
error: None,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
turn_items_for_thread(&thread, "turn-1"),
|
||||
Some(vec![AppServerThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}])
|
||||
);
|
||||
assert_eq!(turn_items_for_thread(&thread, "missing-turn"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canceled_mcp_server_elicitation_response_uses_cancel_action() {
|
||||
let value = canceled_mcp_server_elicitation_response()
|
||||
.expect("mcp elicitation cancel response should serialize");
|
||||
let response: McpServerElicitationRequestResponse =
|
||||
serde_json::from_value(value).expect("cancel response should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
McpServerElicitationRequestResponse {
|
||||
action: McpServerElicitationAction::Cancel,
|
||||
content: None,
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_params_include_review_policy_when_review_policy_is_manual_only() {
|
||||
let codex_home = tempdir().expect("create temp codex home");
|
||||
let cwd = tempdir().expect("create temp cwd");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
approvals_reviewer: Some(ApprovalsReviewer::User),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(cwd.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("build config with manual-only review policy");
|
||||
|
||||
let params = thread_start_params_from_config(&config);
|
||||
|
||||
assert_eq!(
|
||||
params.approvals_reviewer,
|
||||
Some(codex_app_server_protocol::ApprovalsReviewer::User)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_params_include_review_policy_when_auto_review_is_enabled() {
|
||||
let codex_home = tempdir().expect("create temp codex home");
|
||||
let cwd = tempdir().expect("create temp cwd");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
approvals_reviewer: Some(ApprovalsReviewer::GuardianSubagent),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(cwd.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("build config with guardian review policy");
|
||||
|
||||
let params = thread_start_params_from_config(&config);
|
||||
|
||||
assert_eq!(
|
||||
params.approvals_reviewer,
|
||||
Some(codex_app_server_protocol::ApprovalsReviewer::GuardianSubagent)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_configured_from_thread_response_uses_review_policy_from_response() {
|
||||
let response = ThreadStartResponse {
|
||||
thread: codex_app_server_protocol::Thread {
|
||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
status: codex_app_server_protocol::ThreadStatus::Idle,
|
||||
path: Some(PathBuf::from("/tmp/rollout.jsonl")),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: codex_app_server_protocol::SessionSource::Cli,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
git_info: None,
|
||||
name: Some("thread".to_string()),
|
||||
turns: vec![],
|
||||
},
|
||||
model: "gpt-5.4".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
approval_policy: codex_app_server_protocol::AskForApproval::OnRequest,
|
||||
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::GuardianSubagent,
|
||||
sandbox: codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: codex_app_server_protocol::ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
},
|
||||
reasoning_effort: None,
|
||||
};
|
||||
|
||||
let event = session_configured_from_thread_start_response(&response)
|
||||
.expect("build bootstrap session configured event");
|
||||
|
||||
assert_eq!(
|
||||
event.approvals_reviewer,
|
||||
ApprovalsReviewer::GuardianSubagent
|
||||
);
|
||||
}
|
||||
@@ -41,5 +41,42 @@ fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "main_tests.rs"]
|
||||
mod tests;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn top_cli_parses_resume_prompt_after_config_flag() {
|
||||
const PROMPT: &str = "echo resume-with-global-flags-after-subcommand";
|
||||
let cli = TopCli::parse_from([
|
||||
"codex-exec",
|
||||
"resume",
|
||||
"--last",
|
||||
"--json",
|
||||
"--model",
|
||||
"gpt-5.2-codex",
|
||||
"--config",
|
||||
"reasoning_level=xhigh",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--skip-git-repo-check",
|
||||
PROMPT,
|
||||
]);
|
||||
|
||||
let Some(codex_exec::Command::Resume(args)) = cli.inner.command else {
|
||||
panic!("expected resume command");
|
||||
};
|
||||
let effective_prompt = args.prompt.clone().or_else(|| {
|
||||
if args.last {
|
||||
args.session_id.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
assert_eq!(effective_prompt.as_deref(), Some(PROMPT));
|
||||
assert_eq!(cli.config_overrides.raw_overrides.len(), 1);
|
||||
assert_eq!(
|
||||
cli.config_overrides.raw_overrides[0],
|
||||
"reasoning_level=xhigh"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn top_cli_parses_resume_prompt_after_config_flag() {
|
||||
const PROMPT: &str = "echo resume-with-global-flags-after-subcommand";
|
||||
let cli = TopCli::parse_from([
|
||||
"codex-exec",
|
||||
"resume",
|
||||
"--last",
|
||||
"--json",
|
||||
"--model",
|
||||
"gpt-5.2-codex",
|
||||
"--config",
|
||||
"reasoning_level=xhigh",
|
||||
"--dangerously-bypass-approvals-and-sandbox",
|
||||
"--skip-git-repo-check",
|
||||
PROMPT,
|
||||
]);
|
||||
|
||||
let Some(codex_exec::Command::Resume(args)) = cli.inner.command else {
|
||||
panic!("expected resume command");
|
||||
};
|
||||
let effective_prompt = args.prompt.clone().or_else(|| {
|
||||
if args.last {
|
||||
args.session_id.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
assert_eq!(effective_prompt.as_deref(), Some(PROMPT));
|
||||
assert_eq!(cli.config_overrides.raw_overrides.len(), 1);
|
||||
assert_eq!(
|
||||
cli.config_overrides.raw_overrides[0],
|
||||
"reasoning_level=xhigh"
|
||||
);
|
||||
}
|
||||
@@ -157,10 +157,9 @@ impl AppServerSession {
|
||||
matches!(self.client, AppServerClient::Remote(_))
|
||||
}
|
||||
|
||||
pub(crate) async fn bootstrap(&mut self, config: &Config) -> Result<AppServerBootstrap> {
|
||||
pub(crate) async fn account(&mut self) -> Result<GetAccountResponse> {
|
||||
let account_request_id = self.next_request_id();
|
||||
let account: GetAccountResponse = self
|
||||
.client
|
||||
self.client
|
||||
.request_typed(ClientRequest::GetAccount {
|
||||
request_id: account_request_id,
|
||||
params: GetAccountParams {
|
||||
@@ -168,7 +167,11 @@ impl AppServerSession {
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("account/read failed during TUI bootstrap")?;
|
||||
.wrap_err("account/read failed during TUI startup")
|
||||
}
|
||||
|
||||
pub(crate) async fn bootstrap(&mut self, config: &Config) -> Result<AppServerBootstrap> {
|
||||
let account = self.account().await?;
|
||||
let model_request_id = self.next_request_id();
|
||||
let models: ModelListResponse = self
|
||||
.client
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_app_server_client::InProcessAppServerClient;
|
||||
use codex_app_server_client::InProcessClientStartArgs;
|
||||
use codex_app_server_client::RemoteAppServerClient;
|
||||
use codex_app_server_client::RemoteAppServerConnectArgs;
|
||||
use codex_app_server_protocol::Account as AppServerAccount;
|
||||
use codex_app_server_protocol::AuthMode as AppServerAuthMode;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::Thread as AppServerThread;
|
||||
@@ -979,29 +980,32 @@ async fn run_ratatui_app(
|
||||
// Initialize high-fidelity session event logging if enabled.
|
||||
session_log::maybe_init(&initial_config);
|
||||
|
||||
let mut app_server = Some(
|
||||
match start_app_server(
|
||||
&app_server_target,
|
||||
arg0_paths.clone(),
|
||||
initial_config.clone(),
|
||||
cli_kv_overrides.clone(),
|
||||
loader_overrides.clone(),
|
||||
cloud_requirements.clone(),
|
||||
feedback.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(app_server) => AppServerSession::new(app_server),
|
||||
Err(err) => {
|
||||
terminal_restore_guard.restore_silently();
|
||||
session_log::log_session_end();
|
||||
return Err(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let should_show_trust_screen_flag = !remote_mode && should_show_trust_screen(&initial_config);
|
||||
let mut trust_decision_was_made = false;
|
||||
let needs_onboarding_app_server =
|
||||
should_show_trust_screen_flag || initial_config.model_provider.requires_openai_auth;
|
||||
let mut onboarding_app_server = if needs_onboarding_app_server {
|
||||
Some(AppServerSession::new(
|
||||
start_app_server(
|
||||
&app_server_target,
|
||||
arg0_paths.clone(),
|
||||
initial_config.clone(),
|
||||
cli_kv_overrides.clone(),
|
||||
loader_overrides.clone(),
|
||||
cloud_requirements.clone(),
|
||||
feedback.clone(),
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let login_status = if initial_config.model_provider.requires_openai_auth {
|
||||
let Some(app_server) = onboarding_app_server.as_mut() else {
|
||||
unreachable!("onboarding app server should exist when auth is required");
|
||||
let Some(app_server) = app_server.as_mut() else {
|
||||
unreachable!("app server should exist when auth is required");
|
||||
};
|
||||
get_login_status(app_server, &initial_config).await?
|
||||
} else {
|
||||
@@ -1017,13 +1021,13 @@ async fn run_ratatui_app(
|
||||
show_login_screen,
|
||||
show_trust_screen: should_show_trust_screen_flag,
|
||||
login_status,
|
||||
app_server_request_handle: onboarding_app_server
|
||||
app_server_request_handle: app_server
|
||||
.as_ref()
|
||||
.map(AppServerSession::request_handle),
|
||||
config: initial_config.clone(),
|
||||
},
|
||||
if show_login_screen {
|
||||
onboarding_app_server.take()
|
||||
app_server.as_mut()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
@@ -1031,6 +1035,7 @@ async fn run_ratatui_app(
|
||||
)
|
||||
.await?;
|
||||
if onboarding_result.should_exit {
|
||||
shutdown_app_server_if_present(app_server.take()).await;
|
||||
terminal_restore_guard.restore_silently();
|
||||
session_log::log_session_end();
|
||||
let _ = tui.terminal.clear();
|
||||
@@ -1070,10 +1075,8 @@ async fn run_ratatui_app(
|
||||
initial_config
|
||||
}
|
||||
} else {
|
||||
shutdown_app_server_if_present(onboarding_app_server.take()).await;
|
||||
initial_config
|
||||
};
|
||||
shutdown_app_server_if_present(onboarding_app_server.take()).await;
|
||||
|
||||
let mut missing_session_exit = |id_str: &str, action: &str| {
|
||||
error!("Error finding conversation path: {id_str}");
|
||||
@@ -1091,45 +1094,22 @@ async fn run_ratatui_app(
|
||||
})
|
||||
};
|
||||
|
||||
let needs_app_server_session_lookup = cli.resume_last
|
||||
|| cli.fork_last
|
||||
|| cli.resume_session_id.is_some()
|
||||
|| cli.fork_session_id.is_some()
|
||||
|| cli.resume_picker
|
||||
|| cli.fork_picker;
|
||||
let mut session_lookup_app_server = if needs_app_server_session_lookup {
|
||||
Some(AppServerSession::new(
|
||||
start_app_server(
|
||||
&app_server_target,
|
||||
arg0_paths.clone(),
|
||||
config.clone(),
|
||||
cli_kv_overrides.clone(),
|
||||
loader_overrides.clone(),
|
||||
cloud_requirements.clone(),
|
||||
feedback.clone(),
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let use_fork = cli.fork_picker || cli.fork_last || cli.fork_session_id.is_some();
|
||||
let session_selection = if use_fork {
|
||||
if let Some(id_str) = cli.fork_session_id.as_deref() {
|
||||
let Some(app_server) = session_lookup_app_server.as_mut() else {
|
||||
unreachable!("session lookup app server should be initialized for --fork <id>");
|
||||
let Some(startup_app_server) = app_server.as_mut() else {
|
||||
unreachable!("app server should be initialized for --fork <id>");
|
||||
};
|
||||
match lookup_session_target_with_app_server(app_server, id_str).await? {
|
||||
match lookup_session_target_with_app_server(startup_app_server, id_str).await? {
|
||||
Some(target_session) => resume_picker::SessionSelection::Fork(target_session),
|
||||
None => {
|
||||
shutdown_app_server_if_present(session_lookup_app_server.take()).await;
|
||||
shutdown_app_server_if_present(app_server.take()).await;
|
||||
return missing_session_exit(id_str, "fork");
|
||||
}
|
||||
}
|
||||
} else if cli.fork_last {
|
||||
let Some(app_server) = session_lookup_app_server.as_mut() else {
|
||||
unreachable!("session lookup app server should be initialized for --fork --last");
|
||||
let Some(app_server) = app_server.as_mut() else {
|
||||
unreachable!("app server should be initialized for --fork --last");
|
||||
};
|
||||
match lookup_latest_session_target_with_app_server(
|
||||
app_server, &config, /*cwd_filter*/ None,
|
||||
@@ -1141,8 +1121,8 @@ async fn run_ratatui_app(
|
||||
None => resume_picker::SessionSelection::StartFresh,
|
||||
}
|
||||
} else if cli.fork_picker {
|
||||
let Some(app_server) = session_lookup_app_server.take() else {
|
||||
unreachable!("session lookup app server should be initialized for --fork picker");
|
||||
let Some(app_server) = app_server.take() else {
|
||||
unreachable!("app server should be initialized for --fork picker");
|
||||
};
|
||||
match resume_picker::run_fork_picker_with_app_server(
|
||||
&mut tui,
|
||||
@@ -1169,13 +1149,13 @@ async fn run_ratatui_app(
|
||||
resume_picker::SessionSelection::StartFresh
|
||||
}
|
||||
} else if let Some(id_str) = cli.resume_session_id.as_deref() {
|
||||
let Some(app_server) = session_lookup_app_server.as_mut() else {
|
||||
unreachable!("session lookup app server should be initialized for --resume <id>");
|
||||
let Some(startup_app_server) = app_server.as_mut() else {
|
||||
unreachable!("app server should be initialized for --resume <id>");
|
||||
};
|
||||
match lookup_session_target_with_app_server(app_server, id_str).await? {
|
||||
match lookup_session_target_with_app_server(startup_app_server, id_str).await? {
|
||||
Some(target_session) => resume_picker::SessionSelection::Resume(target_session),
|
||||
None => {
|
||||
shutdown_app_server_if_present(session_lookup_app_server.take()).await;
|
||||
shutdown_app_server_if_present(app_server.take()).await;
|
||||
return missing_session_exit(id_str, "resume");
|
||||
}
|
||||
}
|
||||
@@ -1185,8 +1165,8 @@ async fn run_ratatui_app(
|
||||
} else {
|
||||
Some(config.cwd.as_path())
|
||||
};
|
||||
let Some(app_server) = session_lookup_app_server.as_mut() else {
|
||||
unreachable!("session lookup app server should be initialized for --resume --last");
|
||||
let Some(app_server) = app_server.as_mut() else {
|
||||
unreachable!("app server should be initialized for --resume --last");
|
||||
};
|
||||
match lookup_latest_session_target_with_app_server(
|
||||
app_server,
|
||||
@@ -1200,8 +1180,8 @@ async fn run_ratatui_app(
|
||||
None => resume_picker::SessionSelection::StartFresh,
|
||||
}
|
||||
} else if cli.resume_picker {
|
||||
let Some(app_server) = session_lookup_app_server.take() else {
|
||||
unreachable!("session lookup app server should be initialized for --resume picker");
|
||||
let Some(app_server) = app_server.take() else {
|
||||
unreachable!("app server should be initialized for --resume picker");
|
||||
};
|
||||
match resume_picker::run_resume_picker_with_app_server(
|
||||
&mut tui,
|
||||
@@ -1228,7 +1208,6 @@ async fn run_ratatui_app(
|
||||
} else {
|
||||
resume_picker::SessionSelection::StartFresh
|
||||
};
|
||||
shutdown_app_server_if_present(session_lookup_app_server.take()).await;
|
||||
|
||||
let current_cwd = config.cwd.clone();
|
||||
let allow_prompt = !remote_mode && cli.cwd.is_none();
|
||||
@@ -1314,28 +1293,31 @@ async fn run_ratatui_app(
|
||||
|
||||
let use_alt_screen = determine_alt_screen_mode(no_alt_screen, config.tui_alternate_screen);
|
||||
tui.set_alt_screen_enabled(use_alt_screen);
|
||||
let app_server = match start_app_server(
|
||||
&app_server_target,
|
||||
arg0_paths,
|
||||
config.clone(),
|
||||
cli_kv_overrides.clone(),
|
||||
loader_overrides,
|
||||
cloud_requirements.clone(),
|
||||
feedback.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(app_server) => app_server,
|
||||
Err(err) => {
|
||||
terminal_restore_guard.restore_silently();
|
||||
session_log::log_session_end();
|
||||
return Err(err);
|
||||
}
|
||||
let app_server = match app_server {
|
||||
Some(app_server) => app_server,
|
||||
None => match start_app_server(
|
||||
&app_server_target,
|
||||
arg0_paths,
|
||||
config.clone(),
|
||||
cli_kv_overrides.clone(),
|
||||
loader_overrides,
|
||||
cloud_requirements.clone(),
|
||||
feedback.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(app_server) => AppServerSession::new(app_server),
|
||||
Err(err) => {
|
||||
terminal_restore_guard.restore_silently();
|
||||
session_log::log_session_end();
|
||||
return Err(err);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let app_result = App::run(
|
||||
&mut tui,
|
||||
AppServerSession::new(app_server),
|
||||
app_server,
|
||||
config,
|
||||
cli_kv_overrides.clone(),
|
||||
overrides.clone(),
|
||||
@@ -1570,9 +1552,10 @@ async fn get_login_status(
|
||||
return Ok(LoginStatus::NotAuthenticated);
|
||||
}
|
||||
|
||||
let bootstrap = app_server.bootstrap(config).await?;
|
||||
Ok(match bootstrap.account_auth_mode {
|
||||
Some(auth_mode) => LoginStatus::AuthMode(auth_mode),
|
||||
let account = app_server.account().await?;
|
||||
Ok(match account.account {
|
||||
Some(AppServerAccount::ApiKey {}) => LoginStatus::AuthMode(AppServerAuthMode::ApiKey),
|
||||
Some(AppServerAccount::Chatgpt { .. }) => LoginStatus::AuthMode(AppServerAuthMode::Chatgpt),
|
||||
None => LoginStatus::NotAuthenticated,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -433,7 +433,7 @@ impl WidgetRef for Step {
|
||||
|
||||
pub(crate) async fn run_onboarding_app(
|
||||
args: OnboardingScreenArgs,
|
||||
mut app_server: Option<AppServerSession>,
|
||||
mut app_server: Option<&mut AppServerSession>,
|
||||
tui: &mut Tui,
|
||||
) -> Result<OnboardingResult> {
|
||||
use tokio_stream::StreamExt;
|
||||
@@ -519,9 +519,6 @@ pub(crate) async fn run_onboarding_app(
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(app_server) = app_server {
|
||||
app_server.shutdown().await.ok();
|
||||
}
|
||||
Ok(OnboardingResult {
|
||||
directory_trust_decision: onboarding_screen.directory_trust_decision(),
|
||||
should_exit: onboarding_screen.should_exit(),
|
||||
|
||||
@@ -10,7 +10,6 @@ exports_files([
|
||||
"rules_rust_repository_set_exec_constraints.patch",
|
||||
"rules_rust_windows_msvc_direct_link_args.patch",
|
||||
"rules_rust_windows_gnullvm_build_script.patch",
|
||||
"rules_rs_delete_git_worktree_pointer.patch",
|
||||
"rules_rs_windows_gnullvm_exec.patch",
|
||||
"rusty_v8_prebuilt_out_dir.patch",
|
||||
"v8_bazel_rules.patch",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# What: delete .git worktree pointer from crate git checkouts.
|
||||
# Why: the .git file contains an absolute path to the bazel output base,
|
||||
# which differs across machines. This pollutes compile_data and causes
|
||||
# action cache misses when builds run on different CI runners.
|
||||
|
||||
diff --git a/rs/private/crate_git_repository.bzl b/rs/private/crate_git_repository.bzl
|
||||
index 1234567..abcdefg 100644
|
||||
--- a/rs/private/crate_git_repository.bzl
|
||||
+++ b/rs/private/crate_git_repository.bzl
|
||||
@@ -35,6 +35,11 @@ def _crate_git_repository_implementation(rctx):
|
||||
"HEAD"
|
||||
])
|
||||
if result.return_code != 0:
|
||||
fail(result.stderr)
|
||||
|
||||
+ # Remove .git worktree pointer file. It contains an absolute path to
|
||||
+ # the bazel output base which is machine-specific and non-deterministic.
|
||||
+ # Leaving it in pollutes compile_data globs and causes AC misses.
|
||||
+ rctx.delete(root.get_child(".git"))
|
||||
+
|
||||
if strip_prefix:
|
||||
dest_link = dest_dir.get_child(strip_prefix)
|
||||
if not dest_link.exists:
|
||||
Reference in New Issue
Block a user