mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
chore: move codex-exec unit tests into sibling files (#16581)
## Why `codex-rs/exec/src/lib.rs` already keeps unit tests in a sibling `lib_tests.rs` module so the implementation stays top-heavy and easier to read. This applies that same layout to the rest of `codex-rs/exec/src` so each production file keeps its entry points and helpers ahead of test code. ## What - Move inline unit tests out of `cli.rs`, `main.rs`, `event_processor_with_human_output.rs`, and `event_processor_with_jsonl_output.rs` into sibling `*_tests.rs` files. - Keep test modules wired through `#[cfg(test)]` plus `#[path = "..."] mod tests;`, matching the `lib.rs` pattern. - Preserve the existing test coverage and assertions while making this a source-layout-only refactor. ## Verification - `cargo test -p codex-exec`
This commit is contained in:
committed by
GitHub
Unverified
parent
a098834148
commit
95b0b5a204
@@ -256,60 +256,5 @@ pub enum Color {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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));
|
||||
}
|
||||
}
|
||||
#[path = "cli_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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,350 +564,5 @@ fn should_print_final_message_to_tty(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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);
|
||||
}
|
||||
}
|
||||
#[path = "event_processor_with_human_output_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
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,59 +621,5 @@ impl EventProcessor for EventProcessorWithJsonOutput {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
#[path = "event_processor_with_jsonl_output_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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"
|
||||
);
|
||||
}
|
||||
+2
-404
@@ -1657,407 +1657,5 @@ fn build_review_request(args: &ReviewArgs) -> anyhow::Result<ReviewRequest> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
#[path = "lib_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
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,42 +41,5 @@ fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
#[path = "main_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
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"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user