Codex Handbook
core/tests/suite/abort_tasks.rs 256 lines
use assert_matches::assert_matches;use std::sync::Arc;use std::time::Duration;use codex_protocol::protocol::EventMsg;use codex_protocol::protocol::Op;use codex_protocol::user_input::UserInput;use core_test_support::responses::ev_completed;use core_test_support::responses::ev_function_call;use core_test_support::responses::ev_response_created;use core_test_support::responses::mount_sse_once;use core_test_support::responses::mount_sse_sequence;use core_test_support::responses::sse;use core_test_support::responses::start_mock_server;use core_test_support::test_codex::test_codex;use core_test_support::wait_for_event;use regex_lite::Regex;use serde_json::json;/// Integration test: spawn a long‑running shell_command tool via a mocked Responses SSE/// function call, then interrupt the session and expect TurnAborted.#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn interrupt_long_running_tool_emits_turn_aborted() {    let command = "sleep 60";    let args = json!({        "command": command,        "timeout_ms": 60_000    })    .to_string();    let body = sse(vec![        ev_function_call("call_sleep", "shell_command", &args),        ev_completed("done"),    ]);    let server = start_mock_server().await;    mount_sse_once(&server, body).await;    let codex = test_codex()        .with_model("gpt-5.4")        .build(&server)        .await        .unwrap()        .codex;    // Kick off a turn that triggers the function call.    codex        .submit(Op::UserInput {            items: vec![UserInput::Text {                text: "start sleep".into(),                text_elements: Vec::new(),            }],            final_output_json_schema: None,            responsesapi_client_metadata: None,            additional_context: Default::default(),            thread_settings: Default::default(),        })        .await        .unwrap();    // Wait until the exec begins to avoid a race, then interrupt.    wait_for_event(&codex, |ev| matches!(ev, EventMsg::ExecCommandBegin(_))).await;    codex.submit(Op::Interrupt).await.unwrap();    // Expect TurnAborted soon after.    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnAborted(_))).await;}/// After an interrupt we expect the next request to the model to include both/// the original tool call and an `"aborted"` `function_call_output`. This test/// exercises the follow-up flow: it sends another user turn, inspects the mock/// responses server, and ensures the model receives the synthesized abort.#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn interrupt_tool_records_history_entries() {    let command = "sleep 60";    let call_id = "call-history";    let args = json!({        "command": command,        "timeout_ms": 60_000    })    .to_string();    let first_body = sse(vec![        ev_response_created("resp-history"),        ev_function_call(call_id, "shell_command", &args),        ev_completed("resp-history"),    ]);    let follow_up_body = sse(vec![        ev_response_created("resp-followup"),        ev_completed("resp-followup"),    ]);    let server = start_mock_server().await;    let response_mock = mount_sse_sequence(&server, vec![first_body, follow_up_body]).await;    let fixture = test_codex()        .with_model("gpt-5.4")        .build(&server)        .await        .unwrap();    let codex = Arc::clone(&fixture.codex);    codex        .submit(Op::UserInput {            items: vec![UserInput::Text {                text: "start history recording".into(),                text_elements: Vec::new(),            }],            final_output_json_schema: None,            responsesapi_client_metadata: None,            additional_context: Default::default(),            thread_settings: Default::default(),        })        .await        .unwrap();    wait_for_event(&codex, |ev| matches!(ev, EventMsg::ExecCommandBegin(_))).await;    tokio::time::sleep(Duration::from_secs_f32(0.1)).await;    codex.submit(Op::Interrupt).await.unwrap();    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnAborted(_))).await;    codex        .submit(Op::UserInput {            items: vec![UserInput::Text {                text: "follow up".into(),                text_elements: Vec::new(),            }],            final_output_json_schema: None,            responsesapi_client_metadata: None,            additional_context: Default::default(),            thread_settings: Default::default(),        })        .await        .unwrap();    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;    let requests = response_mock.requests();    assert!(        requests.len() == 2,        "expected two calls to the responses API, got {}",        requests.len()    );    assert!(        response_mock.saw_function_call(call_id),        "function call not recorded in responses payload"    );    let output = response_mock        .function_call_output_text(call_id)        .expect("missing function_call_output text");    let re = Regex::new(r"^Wall time: ([0-9]+(?:\.[0-9])?) seconds\naborted by user$")        .expect("compile regex");    let captures = re.captures(&output);    assert_matches!(        captures.as_ref(),        Some(caps) if caps.get(1).is_some(),        "aborted message with elapsed seconds"    );    let secs: f32 = captures        .expect("aborted message with elapsed seconds")        .get(1)        .unwrap()        .as_str()        .parse()        .unwrap();    assert!(        secs >= 0.1,        "expected at least one tenth of a second of elapsed time, got {secs}"    );}/// After an interrupt we persist a model-visible `<turn_aborted>` marker in the conversation/// history. This test asserts that the marker is included in the next `/responses` request.#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn interrupt_persists_turn_aborted_marker_in_next_request() {    let command = "sleep 60";    let call_id = "call-turn-aborted-marker";    let args = json!({        "command": command,        "timeout_ms": 60_000    })    .to_string();    let first_body = sse(vec![        ev_response_created("resp-marker"),        ev_function_call(call_id, "shell_command", &args),        ev_completed("resp-marker"),    ]);    let follow_up_body = sse(vec![        ev_response_created("resp-followup"),        ev_completed("resp-followup"),    ]);    let server = start_mock_server().await;    let response_mock = mount_sse_sequence(&server, vec![first_body, follow_up_body]).await;    let fixture = test_codex()        .with_model("gpt-5.4")        .build(&server)        .await        .unwrap();    let codex = Arc::clone(&fixture.codex);    codex        .submit(Op::UserInput {            items: vec![UserInput::Text {                text: "start interrupt marker".into(),                text_elements: Vec::new(),            }],            final_output_json_schema: None,            responsesapi_client_metadata: None,            additional_context: Default::default(),            thread_settings: Default::default(),        })        .await        .unwrap();    wait_for_event(&codex, |ev| matches!(ev, EventMsg::ExecCommandBegin(_))).await;    tokio::time::sleep(Duration::from_secs_f32(0.1)).await;    codex.submit(Op::Interrupt).await.unwrap();    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnAborted(_))).await;    codex        .submit(Op::UserInput {            items: vec![UserInput::Text {                text: "follow up".into(),                text_elements: Vec::new(),            }],            final_output_json_schema: None,            responsesapi_client_metadata: None,            additional_context: Default::default(),            thread_settings: Default::default(),        })        .await        .unwrap();    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;    let requests = response_mock.requests();    assert_eq!(requests.len(), 2, "expected two calls to the responses API");    let follow_up_request = &requests[1];    let user_texts = follow_up_request.message_input_texts("user");    assert!(        user_texts            .iter()            .any(|text| text.contains("<turn_aborted>")),        "expected <turn_aborted> marker in follow-up request"    );}