mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
Add test
This commit is contained in:
@@ -19,7 +19,9 @@ use core_test_support::wait_for_event;
|
||||
use serde_json::Value;
|
||||
use tokio::test;
|
||||
use wiremock::matchers::any;
|
||||
use wiremock::matchers::body_string_contains;
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
async fn collect_tool_names(model: &str) -> Result<Vec<String>> {
|
||||
let server = start_mock_server().await;
|
||||
let model_owned = model.to_string();
|
||||
@@ -115,3 +117,182 @@ async fn gpt5_codex_models_do_not_expose_subsession_tools() -> Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn subsession_can_apply_patch_to_workspace() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
// Ensure subsession tools are exposed and apply_patch is available in child.
|
||||
config.model = "test-gpt-5-codex".to_string();
|
||||
config.model_family =
|
||||
find_family_for_model("test-gpt-5-codex").expect("model family available for test");
|
||||
config.include_apply_patch_tool = true;
|
||||
});
|
||||
let TestCodex {
|
||||
codex,
|
||||
cwd,
|
||||
session_configured,
|
||||
..
|
||||
} = builder.build(&server).await?;
|
||||
|
||||
// Parent turn 1: ask to spawn a subsession to create a file.
|
||||
// The parent model will call create_session with a prompt instructing the child.
|
||||
let parent_first = sse(vec![
|
||||
ev_response_created("resp-parent-1"),
|
||||
responses::ev_function_call(
|
||||
"create-session-1",
|
||||
"create_session",
|
||||
&serde_json::json!({
|
||||
"session_type": "default",
|
||||
"prompt": "Create a file named subsession.txt with the exact contents 'Hello from subsession'",
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
ev_completed("resp-parent-1"),
|
||||
]);
|
||||
responses::mount_sse_once_match(&server, any(), parent_first).await;
|
||||
|
||||
// Child turn 1: upon spawn, the child will call apply_patch to create the file.
|
||||
// Match on the subsession instructions to route this to the child conversation.
|
||||
let child_first = sse(vec![
|
||||
ev_response_created("resp-child-1"),
|
||||
responses::ev_apply_patch_function_call(
|
||||
"apply-patch-child-1",
|
||||
r#"*** Begin Patch
|
||||
*** Add File: subsession.txt
|
||||
+Hello from subsession
|
||||
*** End Patch"#,
|
||||
),
|
||||
ev_completed("resp-child-1"),
|
||||
]);
|
||||
responses::mount_sse_once_match(
|
||||
&server,
|
||||
body_string_contains("You are a compact subsession assistant"),
|
||||
child_first,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Parent follow-up: after the tool result is returned, the parent may send a
|
||||
// subsequent request. Provide a simple assistant message to close the turn.
|
||||
let parent_second = sse(vec![
|
||||
ev_assistant_message("msg-parent-2", "subsession started"),
|
||||
ev_completed("resp-parent-2"),
|
||||
]);
|
||||
responses::mount_sse_once_match(
|
||||
&server,
|
||||
body_string_contains("\"function_call_output\""),
|
||||
parent_second,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Child follow-up: after apply_patch executes, the child continues and then finishes.
|
||||
let child_second = sse(vec![
|
||||
ev_assistant_message("msg-child-2", "done"),
|
||||
ev_completed("resp-child-2"),
|
||||
]);
|
||||
responses::mount_sse_once_match(
|
||||
&server,
|
||||
body_string_contains("You are a compact subsession assistant"),
|
||||
child_second,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Kick off the parent turn which should spawn the subsession.
|
||||
let session_model = session_configured.model.clone();
|
||||
codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![InputItem::Text {
|
||||
text: "please spawn a subsession to create a file".into(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
summary: ReasoningSummary::Auto,
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Capture the child session id from the background event.
|
||||
let mut child_id: Option<String> = None;
|
||||
wait_for_event(&codex, |event| match event {
|
||||
EventMsg::BackgroundEvent(ev) => {
|
||||
if let Some((_, after)) = ev.message.split_once("spawned child session ")
|
||||
&& let Some((id, _)) = after.split_once(' ')
|
||||
{
|
||||
child_id = Some(id.to_string());
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
|
||||
// Wait for the child to complete and emit its final background event.
|
||||
// This makes the file write deterministic before we assert.
|
||||
let _ = wait_for_event(&codex, |event| match event {
|
||||
EventMsg::BackgroundEvent(ev) => {
|
||||
if let Some(id) = child_id.as_deref() {
|
||||
return ev
|
||||
.message
|
||||
.contains(&format!("child session {id} completed"));
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
|
||||
// Debug: inspect recorded requests to confirm routing during failures.
|
||||
if let Some(requests) = server.received_requests().await {
|
||||
for (i, req) in requests.iter().enumerate() {
|
||||
if let Ok(body) = req.body_json::<Value>() {
|
||||
let instr = body
|
||||
.get("instructions")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
let has_apply = body
|
||||
.get("tools")
|
||||
.and_then(Value::as_array)
|
||||
.map(|a| {
|
||||
a.iter().any(|t| {
|
||||
t.get("function")
|
||||
.and_then(|f| f.get("name"))
|
||||
.and_then(Value::as_str)
|
||||
== Some("apply_patch")
|
||||
|| t.get("name").and_then(Value::as_str) == Some("apply_patch")
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let has_fn_output = body
|
||||
.get("input")
|
||||
.and_then(Value::as_array)
|
||||
.map(|a| {
|
||||
a.iter().any(|it| {
|
||||
it.get("type").and_then(Value::as_str) == Some("function_call_output")
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
eprintln!(
|
||||
"req#{i}: instr_has_subsession_prompt={} tools_include_apply_patch={} has_fn_output={}",
|
||||
instr.contains("compact subsession assistant"),
|
||||
has_apply,
|
||||
has_fn_output,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the file created by the subsession exists with the expected contents.
|
||||
let created_path = cwd.path().join("subsession.txt");
|
||||
let contents = std::fs::read_to_string(&created_path)
|
||||
.unwrap_or_else(|e| panic!("failed reading {}: {e}", created_path.display()));
|
||||
assert_eq!(contents, "Hello from subsession\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user