mirror of
https://github.com/openai/codex.git
synced 2026-05-03 02:46:39 +00:00
Handle orphan exec ends without clobbering active exploring cell (#12313)
Summary - distinguish exec end handling targets (active tracking, active orphan history, new cell) so unified exec responses don’t clobber unrelated exploring cells - ensure orphan ends flush existing exploring history when complete, insert standalone history entries, and keep active cells correct - add regression tests plus a snapshot covering the new behavior and expose the ExecCell completion result for verification Fix for https://github.com/openai/codex/issues/12278 --------- Co-authored-by: Josh McKinney <joshka@openai.com>
This commit is contained in:
@@ -3531,6 +3531,114 @@ async fn exec_end_without_begin_uses_event_command() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_end_without_begin_does_not_flush_unrelated_running_exploring_cell() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.on_task_started();
|
||||
|
||||
begin_exec(&mut chat, "call-exploring", "cat /dev/null");
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
assert!(active_blob(&chat).contains("Read null"));
|
||||
|
||||
let orphan =
|
||||
begin_unified_exec_startup(&mut chat, "call-orphan", "proc-1", "echo repro-marker");
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
|
||||
end_exec(&mut chat, orphan, "repro-marker\n", "", 0);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "only the orphan end should be inserted");
|
||||
let orphan_blob = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
orphan_blob.contains("• Ran echo repro-marker"),
|
||||
"expected orphan end to render a standalone entry: {orphan_blob:?}"
|
||||
);
|
||||
let active = active_blob(&chat);
|
||||
assert!(
|
||||
active.contains("• Exploring"),
|
||||
"expected unrelated exploring call to remain active: {active:?}"
|
||||
);
|
||||
assert!(
|
||||
active.contains("Read null"),
|
||||
"expected active exploring command to remain visible: {active:?}"
|
||||
);
|
||||
assert!(
|
||||
!active.contains("echo repro-marker"),
|
||||
"orphaned end should not replace the active exploring cell: {active:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_end_without_begin_flushes_completed_unrelated_exploring_cell() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.on_task_started();
|
||||
|
||||
let begin_ls = begin_exec(&mut chat, "call-ls", "ls -la");
|
||||
end_exec(&mut chat, begin_ls, "", "", 0);
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
assert!(active_blob(&chat).contains("ls -la"));
|
||||
|
||||
let orphan = begin_unified_exec_startup(&mut chat, "call-after", "proc-1", "echo after");
|
||||
end_exec(&mut chat, orphan, "after\n", "", 0);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(
|
||||
cells.len(),
|
||||
2,
|
||||
"completed exploring cell should flush before the orphan entry"
|
||||
);
|
||||
let first = lines_to_single_string(&cells[0]);
|
||||
let second = lines_to_single_string(&cells[1]);
|
||||
assert!(
|
||||
first.contains("• Explored"),
|
||||
"expected flushed exploring cell: {first:?}"
|
||||
);
|
||||
assert!(
|
||||
first.contains("List ls -la"),
|
||||
"expected flushed exploring cell: {first:?}"
|
||||
);
|
||||
assert!(
|
||||
second.contains("• Ran echo after"),
|
||||
"expected orphan end entry after flush: {second:?}"
|
||||
);
|
||||
assert!(
|
||||
chat.active_cell.is_none(),
|
||||
"both entries should be finalized"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn overlapping_exploring_exec_end_is_not_misclassified_as_orphan() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
let begin_ls = begin_exec(&mut chat, "call-ls", "ls -la");
|
||||
let begin_cat = begin_exec(&mut chat, "call-cat", "cat foo.txt");
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
|
||||
end_exec(&mut chat, begin_ls, "foo.txt\n", "", 0);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
cells.is_empty(),
|
||||
"tracked end inside an exploring cell should not render as an orphan"
|
||||
);
|
||||
let active = active_blob(&chat);
|
||||
assert!(
|
||||
active.contains("List ls -la"),
|
||||
"expected first command still grouped: {active:?}"
|
||||
);
|
||||
assert!(
|
||||
active.contains("Read foo.txt"),
|
||||
"expected second running command to stay in the same active cell: {active:?}"
|
||||
);
|
||||
assert!(
|
||||
active.contains("• Exploring"),
|
||||
"expected grouped exploring header to remain active: {active:?}"
|
||||
);
|
||||
|
||||
end_exec(&mut chat, begin_cat, "hello\n", "", 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_history_shows_unified_exec_startup_commands() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
@@ -3575,6 +3683,29 @@ async fn exec_history_shows_unified_exec_tool_calls() {
|
||||
assert_eq!(blob, "• Explored\n └ List ls\n");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unified_exec_unknown_end_with_active_exploring_cell_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.on_task_started();
|
||||
|
||||
begin_exec(&mut chat, "call-exploring", "cat /dev/null");
|
||||
let orphan =
|
||||
begin_unified_exec_startup(&mut chat, "call-orphan", "proc-1", "echo repro-marker");
|
||||
end_exec(&mut chat, orphan, "repro-marker\n", "", 0);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let history = cells
|
||||
.iter()
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
.collect::<String>();
|
||||
let active = active_blob(&chat);
|
||||
let snapshot = format!("History:\n{history}\nActive:\n{active}");
|
||||
assert_snapshot!(
|
||||
"unified_exec_unknown_end_with_active_exploring_cell",
|
||||
snapshot
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unified_exec_end_after_task_complete_is_suppressed() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
Reference in New Issue
Block a user