This commit is contained in:
Jeremy Rose
2025-08-25 17:15:05 -07:00
parent 6674c6c3e4
commit 4cb61784a5
4 changed files with 229 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
#![cfg(unix)]
use std::time::Instant;
use codex_core::built_in_model_providers;
use codex_core::protocol::EventMsg;
use codex_core::ConversationManager;
use codex_login::CodexAuth;
use core_test_support::load_sse_fixture_with_id_from_str;
use tempfile::TempDir;
fn build_parallel_exec_sse() -> String {
let item1 = serde_json::json!({
"type": "response.output_item.done",
"item": {
"type": "local_shell_call",
"call_id": "c1",
"status": "in_progress",
"action": {
"type": "exec",
"command": ["/bin/sh", "-c", "echo A"],
"timeout_ms": 3000,
"working_directory": null,
"env": null,
"user": null
}
}
});
let item2 = serde_json::json!({
"type": "response.output_item.done",
"item": {
"type": "local_shell_call",
"call_id": "c2",
"status": "in_progress",
"action": {
"type": "exec",
"command": ["/bin/sh", "-c", "echo B"],
"timeout_ms": 3000,
"working_directory": null,
"env": null,
"user": null
}
}
});
let completed = serde_json::json!({
"type": "response.completed",
"response": { "id": "__ID__" }
});
let raw = serde_json::json!([
item1, item2, completed
])
.to_string();
load_sse_fixture_with_id_from_str(&raw, "resp_parallel")
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn local_shell_calls_begin_before_any_end_and_run_concurrently() {
let tmp_sse = tempfile::NamedTempFile::new().expect("tmp sse");
std::fs::write(tmp_sse.path(), build_parallel_exec_sse()).expect("write sse");
std::env::set_var("CODEX_RS_SSE_FIXTURE", tmp_sse.path());
let home = TempDir::new().unwrap();
let mut config = core_test_support::load_default_config_for_test(&home);
let providers = built_in_model_providers();
config.model_provider = providers
.get("openai")
.expect("builtin provider")
.clone();
let cm = ConversationManager::with_auth(CodexAuth::from_api_key("test"));
let conv = cm
.new_conversation(config)
.await
.expect("spawn conversation")
.conversation;
conv
.submit(codex_core::protocol::Op::UserInput {
items: vec![codex_core::protocol::InputItem::Text {
text: "go".into(),
}],
})
.await
.expect("submit");
let mut begins = 0usize;
let mut ends = 0usize;
let mut seen_first_end = false;
use tokio::time::{sleep, Duration as TokioDuration};
let deadline = Instant::now() + TokioDuration::from_secs(5);
while Instant::now() < deadline {
let ev = conv.next_event().await.expect("event");
match ev.msg {
EventMsg::ExecCommandBegin(_) => {
begins += 1;
}
EventMsg::ExecCommandEnd(_) => {
if !seen_first_end {
assert_eq!(begins, 2, "expected both begins before first end");
seen_first_end = true;
}
ends += 1;
}
EventMsg::TaskComplete(_) => break,
_ => {}
}
sleep(TokioDuration::from_millis(5)).await;
}
assert_eq!(begins, 2, "expected two parallel exec begins");
assert_eq!(ends, 2, "expected two exec completions");
}

View File

@@ -0,0 +1,7 @@
---
source: tui/src/chatwidget/tests.rs
expression: s
---
>_
✓ ⌨️ echo A
⠋ ⌨️ echo B

View File

@@ -0,0 +1,7 @@
---
source: tui/src/chatwidget/tests.rs
expression: s
---
>_
⠋ ⌨️ echo one
⠋ ⌨️ echo two

View File

@@ -214,6 +214,106 @@ fn lines_to_single_string(lines: &[ratatui::text::Line<'static>]) -> String {
s
}
#[test]
fn active_exec_segments_render_for_parallel_begins_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
// Begin first command
chat.handle_codex_event(Event {
id: "call-1".into(),
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: "call-1".into(),
command: vec!["bash".into(), "-lc".into(), "echo one".into()],
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
parsed_cmd: vec![
codex_core::parse_command::ParsedCommand::Unknown {
cmd: "echo one".into(),
}
.into(),
],
}),
});
// Begin a second command (parallel segment)
chat.handle_codex_event(Event {
id: "call-2".into(),
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: "call-2".into(),
command: vec!["bash".into(), "-lc".into(), "echo two".into()],
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
parsed_cmd: vec![
codex_core::parse_command::ParsedCommand::Unknown {
cmd: "echo two".into(),
}
.into(),
],
}),
});
// Snapshot the active exec cell (shows spinners for both segments)
let cell = chat.active_exec_cell.as_ref().expect("active exec cell");
let s = lines_to_single_string(&cell.display_lines());
assert_snapshot!(s);
}
#[test]
fn active_exec_segments_mark_completion_in_place_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
// Begin two commands
chat.handle_codex_event(Event {
id: "call-a".into(),
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: "call-a".into(),
command: vec!["bash".into(), "-lc".into(), "echo A".into()],
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
parsed_cmd: vec![
codex_core::parse_command::ParsedCommand::Unknown {
cmd: "echo A".into(),
}
.into(),
],
}),
});
chat.handle_codex_event(Event {
id: "call-b".into(),
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: "call-b".into(),
command: vec!["bash".into(), "-lc".into(), "echo B".into()],
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
parsed_cmd: vec![
codex_core::parse_command::ParsedCommand::Unknown {
cmd: "echo B".into(),
}
.into(),
],
}),
});
// Complete first command successfully; second still running
chat.handle_codex_event(Event {
id: "call-a".into(),
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: "call-a".into(),
stdout: "A".into(),
stderr: String::new(),
aggregated_output: "A".into(),
exit_code: 0,
duration: std::time::Duration::from_millis(5),
formatted_output: "A".into(),
}),
});
// Drain any history insertions (none expected yet) and snapshot active cell
let _ = drain_insert_history(&mut rx);
let cell = chat
.active_exec_cell
.as_ref()
.expect("active exec cell after first end");
let s = lines_to_single_string(&cell.display_lines());
assert_snapshot!(s);
}
fn open_fixture(name: &str) -> std::fs::File {
// 1) Prefer fixtures within this crate
{