mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
merge main into resume fork optimization
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::GitInfo;
|
||||
@@ -127,11 +128,12 @@ pub fn create_fake_rollout_with_source(
|
||||
model_provider,
|
||||
git_info,
|
||||
source,
|
||||
/*session_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a minimal rollout file with an explicit session source and control parent.
|
||||
/// Create a minimal rollout file with an explicit root session and control parent.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create_fake_parented_rollout_with_source(
|
||||
codex_home: &Path,
|
||||
@@ -141,6 +143,7 @@ pub fn create_fake_parented_rollout_with_source(
|
||||
model_provider: Option<&str>,
|
||||
git_info: Option<GitInfo>,
|
||||
source: SessionSource,
|
||||
session_id: SessionId,
|
||||
parent_thread_id: ThreadId,
|
||||
) -> Result<String> {
|
||||
create_fake_rollout_with_source_and_parent_thread_id(
|
||||
@@ -151,6 +154,7 @@ pub fn create_fake_parented_rollout_with_source(
|
||||
model_provider,
|
||||
git_info,
|
||||
source,
|
||||
Some(session_id),
|
||||
Some(parent_thread_id),
|
||||
)
|
||||
}
|
||||
@@ -164,11 +168,13 @@ fn create_fake_rollout_with_source_and_parent_thread_id(
|
||||
model_provider: Option<&str>,
|
||||
git_info: Option<GitInfo>,
|
||||
source: SessionSource,
|
||||
session_id: Option<SessionId>,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
) -> Result<String> {
|
||||
let uuid = Uuid::new_v4();
|
||||
let uuid_str = uuid.to_string();
|
||||
let conversation_id = ThreadId::from_string(&uuid_str)?;
|
||||
let session_id = session_id.unwrap_or_else(|| conversation_id.into());
|
||||
|
||||
let file_path = rollout_path(codex_home, filename_ts, &uuid_str);
|
||||
let dir = file_path
|
||||
@@ -178,6 +184,7 @@ fn create_fake_rollout_with_source_and_parent_thread_id(
|
||||
|
||||
// Build JSONL lines
|
||||
let meta = SessionMeta {
|
||||
session_id,
|
||||
id: conversation_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id,
|
||||
@@ -264,6 +271,7 @@ pub fn create_fake_rollout_with_text_elements(
|
||||
|
||||
// Build JSONL lines
|
||||
let meta = SessionMeta {
|
||||
session_id: conversation_id.into(),
|
||||
id: conversation_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
|
||||
@@ -5,7 +5,10 @@ use app_test_support::to_response;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use chrono::Duration;
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::Account;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::GetAccountParams;
|
||||
use codex_app_server_protocol::GetAccountResponse;
|
||||
use codex_app_server_protocol::GetAuthStatusParams;
|
||||
use codex_app_server_protocol::GetAuthStatusResponse;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
@@ -14,6 +17,7 @@ use codex_app_server_protocol::LoginAccountResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
@@ -162,7 +166,7 @@ async fn get_auth_status_with_api_key() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn get_auth_status_with_personal_access_token_omits_token() -> Result<()> {
|
||||
async fn personal_access_token_without_email_supports_auth_status_and_account_read() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path())?;
|
||||
|
||||
@@ -171,7 +175,7 @@ async fn get_auth_status_with_personal_access_token_omits_token() -> Result<()>
|
||||
.and(path("/v1/user-auth-credential/whoami"))
|
||||
.and(header("Authorization", "Bearer at-test-token"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"email": "user@example.com",
|
||||
"email": null,
|
||||
"chatgpt_user_id": "user-123",
|
||||
"chatgpt_account_id": "account-123",
|
||||
"chatgpt_plan_type": "pro",
|
||||
@@ -215,6 +219,34 @@ async fn get_auth_status_with_personal_access_token_omits_token() -> Result<()>
|
||||
}
|
||||
);
|
||||
|
||||
let request_id = mcp
|
||||
.send_get_account_request(GetAccountParams {
|
||||
refresh_token: false,
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
response
|
||||
.result
|
||||
.get("account")
|
||||
.and_then(|account| account.get("email")),
|
||||
Some(&serde_json::Value::Null),
|
||||
);
|
||||
assert_eq!(
|
||||
to_response::<GetAccountResponse>(response)?,
|
||||
GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: None,
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
);
|
||||
|
||||
server.verify().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() ->
|
||||
let thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000125")?;
|
||||
store
|
||||
.create_thread(CreateThreadParams {
|
||||
session_id: thread_id.into(),
|
||||
thread_id,
|
||||
extra_config: None,
|
||||
forked_from_id: None,
|
||||
|
||||
@@ -320,7 +320,7 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
account,
|
||||
GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: "embedded@example.com".to_string(),
|
||||
email: Some("embedded@example.com".to_string()),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
@@ -388,7 +388,7 @@ async fn account_read_refresh_token_is_noop_in_external_mode() -> Result<()> {
|
||||
account,
|
||||
GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: "embedded@example.com".to_string(),
|
||||
email: Some("embedded@example.com".to_string()),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
@@ -1817,7 +1817,7 @@ async fn get_account_with_chatgpt() -> Result<()> {
|
||||
|
||||
let expected = GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: "user@example.com".to_string(),
|
||||
email: Some("user@example.com".to_string()),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
@@ -1826,6 +1826,51 @@ async fn get_account_with_chatgpt() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_with_chatgpt_without_email() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
CreateConfigTomlParams {
|
||||
requires_openai_auth: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt").plan_type("pro"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_get_account_request(GetAccountParams {
|
||||
refresh_token: false,
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: GetAccountResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(
|
||||
received,
|
||||
GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: None,
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_omits_chatgpt_after_permanent_refresh_failure() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -1944,7 +1989,7 @@ async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result
|
||||
|
||||
let expected = GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: "user@example.com".to_string(),
|
||||
email: Some("user@example.com".to_string()),
|
||||
plan_type: AccountPlanType::Unknown,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
|
||||
@@ -63,7 +63,10 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
thread_source: Some(ThreadSource::Feature("automation".to_string())),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
@@ -75,7 +78,6 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result
|
||||
let client_metadata = HashMap::from([
|
||||
("fiber_run_id".to_string(), "fiber-start-123".to_string()),
|
||||
("origin".to_string(), "gaas".to_string()),
|
||||
("thread_source".to_string(), "client-supplied".to_string()),
|
||||
]);
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
@@ -110,7 +112,7 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result
|
||||
.expect("x-codex-turn-metadata header should be present");
|
||||
assert_eq!(metadata["fiber_run_id"].as_str(), Some("fiber-start-123"));
|
||||
assert_eq!(metadata["origin"].as_str(), Some("gaas"));
|
||||
assert_eq!(metadata["thread_source"].as_str(), Some("client-supplied"));
|
||||
assert_eq!(metadata["thread_source"].as_str(), Some("automation"));
|
||||
assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str()));
|
||||
assert!(metadata.get("installation_id").is_some());
|
||||
assert!(metadata.get("session_id").is_some());
|
||||
@@ -300,7 +302,7 @@ async fn review_start_sends_parent_lineage_in_turn_metadata_for_thread_fork_v2()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -> Result<()> {
|
||||
async fn turn_start_sends_nested_subagent_lineage_after_cold_thread_resume_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
@@ -321,6 +323,8 @@ async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -
|
||||
/*supports_websockets*/ false,
|
||||
)?;
|
||||
|
||||
let root_thread_id = CoreThreadId::new();
|
||||
let root_thread_id_str = root_thread_id.to_string();
|
||||
let parent_thread_id = CoreThreadId::new();
|
||||
let parent_thread_id_str = parent_thread_id.to_string();
|
||||
let subagent_thread_id = create_fake_parented_rollout_with_source(
|
||||
@@ -331,6 +335,7 @@ async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
SessionSource::SubAgent(SubAgentSource::Other("guardian".to_string())),
|
||||
root_thread_id.into(),
|
||||
parent_thread_id,
|
||||
)?;
|
||||
|
||||
@@ -350,6 +355,7 @@ async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -
|
||||
.await??;
|
||||
let ThreadResumeResponse { thread, .. } = to_response::<ThreadResumeResponse>(resume_resp)?;
|
||||
assert_eq!(thread.id, subagent_thread_id);
|
||||
assert_eq!(thread.session_id, root_thread_id_str);
|
||||
assert_eq!(thread.parent_thread_id, Some(parent_thread_id_str.clone()));
|
||||
assert_eq!(
|
||||
thread.source,
|
||||
@@ -390,6 +396,10 @@ async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -
|
||||
Some(parent_thread_id_str.as_str())
|
||||
);
|
||||
assert_eq!(metadata["subagent_kind"].as_str(), Some("guardian"));
|
||||
assert_eq!(
|
||||
metadata["session_id"].as_str(),
|
||||
Some(thread.session_id.as_str())
|
||||
);
|
||||
assert_eq!(metadata["thread_id"].as_str(), Some(thread.id.as_str()));
|
||||
assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str()));
|
||||
assert!(metadata.get("forked_from_thread_id").is_none());
|
||||
@@ -553,7 +563,10 @@ async fn turn_start_forwards_client_metadata_to_responses_websocket_request_body
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
thread_source: Some(ThreadSource::Feature("automation".to_string())),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
@@ -611,6 +624,7 @@ async fn turn_start_forwards_client_metadata_to_responses_websocket_request_body
|
||||
.expect("websocket x-codex-turn-metadata client metadata should be present");
|
||||
assert_eq!(metadata["fiber_run_id"].as_str(), Some("fiber-start-123"));
|
||||
assert_eq!(metadata["origin"].as_str(), Some("gaas"));
|
||||
assert_eq!(metadata["thread_source"].as_str(), Some("automation"));
|
||||
assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str()));
|
||||
assert!(metadata.get("session_id").is_some());
|
||||
assert_eq!(
|
||||
|
||||
@@ -134,12 +134,12 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()
|
||||
text: "REMOTE_COMPACT_SUMMARY".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
metadata: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
ResponseItem::Compaction {
|
||||
id: None,
|
||||
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
|
||||
metadata: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
];
|
||||
let compact_mock = responses::mount_compact_json_once(
|
||||
|
||||
@@ -38,6 +38,10 @@ use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
use wiremock::MockServer;
|
||||
|
||||
const TINY_PNG_DATA_URL: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
|
||||
const REMOTE_IMAGE_URL_ERROR: &str =
|
||||
"remote image URLs are not supported; use an inline data URL instead";
|
||||
|
||||
// macOS and Windows Bazel CI can spend tens of seconds starting app-server
|
||||
// subprocesses or processing test RPCs under load.
|
||||
#[cfg(any(target_os = "macos", windows))]
|
||||
@@ -550,15 +554,19 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensures dynamic tool call responses can include structured content items.
|
||||
#[tokio::test]
|
||||
async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<()> {
|
||||
let call_id = "dyn-call-items-1";
|
||||
struct PendingDynamicToolCall {
|
||||
mcp: TestAppServer,
|
||||
server: MockServer,
|
||||
request_id: RequestId,
|
||||
params: DynamicToolCallParams,
|
||||
}
|
||||
|
||||
async fn start_function_dynamic_tool_call(call_id: &str) -> Result<PendingDynamicToolCall> {
|
||||
let tool_name = "demo_tool";
|
||||
let tool_args = json!({ "city": "Paris" });
|
||||
let tool_call_arguments = serde_json::to_string(&tool_args)?;
|
||||
|
||||
let responses = vec![
|
||||
let response_sequence = vec![
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_function_call(call_id, tool_name, &tool_call_arguments),
|
||||
@@ -566,7 +574,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
]),
|
||||
create_final_assistant_message_sse_response("Done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let server = create_mock_responses_server_sequence_unchecked(response_sequence).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
@@ -630,27 +638,46 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
mcp.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let (request_id, params) = match request {
|
||||
let (request_id, actual_params) = match request {
|
||||
ServerRequest::DynamicToolCall { request_id, params } => (request_id, params),
|
||||
other => panic!("expected DynamicToolCall request, got {other:?}"),
|
||||
};
|
||||
|
||||
let expected = DynamicToolCallParams {
|
||||
let params = DynamicToolCallParams {
|
||||
thread_id,
|
||||
turn_id: turn_id.clone(),
|
||||
turn_id,
|
||||
call_id: call_id.to_string(),
|
||||
namespace: None,
|
||||
tool: tool_name.to_string(),
|
||||
arguments: tool_args,
|
||||
};
|
||||
assert_eq!(params, expected);
|
||||
assert_eq!(actual_params, params);
|
||||
|
||||
Ok(PendingDynamicToolCall {
|
||||
mcp,
|
||||
server,
|
||||
request_id,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/// Ensures dynamic tool call responses can include structured content items.
|
||||
#[tokio::test]
|
||||
async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<()> {
|
||||
let call_id = "dyn-call-items-1";
|
||||
let PendingDynamicToolCall {
|
||||
mut mcp,
|
||||
server,
|
||||
request_id,
|
||||
params,
|
||||
} = start_function_dynamic_tool_call(call_id).await?;
|
||||
|
||||
let response_content_items = vec![
|
||||
DynamicToolCallOutputContentItem::InputText {
|
||||
text: "dynamic-ok".to_string(),
|
||||
},
|
||||
DynamicToolCallOutputContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,AAA".to_string(),
|
||||
image_url: TINY_PNG_DATA_URL.to_string(),
|
||||
},
|
||||
];
|
||||
let content_items = response_content_items
|
||||
@@ -676,8 +703,8 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
.await?;
|
||||
|
||||
let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?;
|
||||
assert_eq!(completed.thread_id, expected.thread_id.clone());
|
||||
assert_eq!(completed.turn_id, turn_id);
|
||||
assert_eq!(completed.thread_id, params.thread_id);
|
||||
assert_eq!(completed.turn_id, params.turn_id);
|
||||
let ThreadItem::DynamicToolCall {
|
||||
status,
|
||||
content_items: completed_content_items,
|
||||
@@ -695,7 +722,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
text: "dynamic-ok".to_string(),
|
||||
},
|
||||
DynamicToolCallOutputContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,AAA".to_string(),
|
||||
image_url: TINY_PNG_DATA_URL.to_string(),
|
||||
},
|
||||
])
|
||||
);
|
||||
@@ -721,7 +748,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
},
|
||||
{
|
||||
"type": "input_image",
|
||||
"image_url": "data:image/png;base64,AAA",
|
||||
"image_url": TINY_PNG_DATA_URL,
|
||||
"detail": "high"
|
||||
}
|
||||
])
|
||||
@@ -744,6 +771,62 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dynamic_tool_remote_image_response_becomes_model_visible_error() -> Result<()> {
|
||||
let call_id = "dyn-call-remote-image";
|
||||
let PendingDynamicToolCall {
|
||||
mut mcp,
|
||||
server,
|
||||
request_id,
|
||||
params,
|
||||
} = start_function_dynamic_tool_call(call_id).await?;
|
||||
|
||||
let response = DynamicToolCallResponse {
|
||||
content_items: vec![DynamicToolCallOutputContentItem::InputImage {
|
||||
image_url: "https://example.com/tool.png".to_string(),
|
||||
}],
|
||||
success: true,
|
||||
};
|
||||
mcp.send_response(request_id, serde_json::to_value(response)?)
|
||||
.await?;
|
||||
|
||||
let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?;
|
||||
assert_eq!(completed.thread_id, params.thread_id);
|
||||
assert_eq!(completed.turn_id, params.turn_id);
|
||||
let ThreadItem::DynamicToolCall {
|
||||
status,
|
||||
content_items,
|
||||
success,
|
||||
..
|
||||
} = completed.item
|
||||
else {
|
||||
panic!("expected dynamic tool call item");
|
||||
};
|
||||
assert_eq!(status, DynamicToolCallStatus::Failed);
|
||||
assert_eq!(
|
||||
content_items,
|
||||
Some(vec![DynamicToolCallOutputContentItem::InputText {
|
||||
text: REMOTE_IMAGE_URL_ERROR.to_string(),
|
||||
}])
|
||||
);
|
||||
assert_eq!(success, Some(false));
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let output = responses_bodies(&server)
|
||||
.await?
|
||||
.iter()
|
||||
.find_map(|body| function_call_output_raw_output(body, call_id))
|
||||
.context("expected function_call_output output in follow-up request")?;
|
||||
assert_eq!(output, json!(REMOTE_IMAGE_URL_ERROR));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn responses_bodies(server: &MockServer) -> Result<Vec<Value>> {
|
||||
let requests = server
|
||||
.received_requests()
|
||||
|
||||
@@ -28,13 +28,13 @@ use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
const RESULT: &str = "cG5n";
|
||||
const RESULT: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
|
||||
const TINY_PNG_BYTES: &[u8] = &[
|
||||
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0,
|
||||
0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84, 120, 156, 99, 96, 0, 2, 0, 0, 5, 0, 1,
|
||||
122, 94, 171, 63, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
|
||||
0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84, 120, 156, 99, 248, 207, 192, 240, 31, 0,
|
||||
5, 0, 1, 255, 137, 153, 61, 29, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
|
||||
];
|
||||
const TINY_PNG_DATA_URL: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=";
|
||||
const TINY_PNG_DATA_URL: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ImagegenTestMode {
|
||||
@@ -116,7 +116,7 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Resul
|
||||
assert_eq!(status, "completed");
|
||||
assert_eq!(revised_prompt.as_deref(), Some("paint a blue whale"));
|
||||
assert_eq!(result, RESULT);
|
||||
assert_eq!(std::fs::read(&saved_path)?, b"png");
|
||||
assert_eq!(std::fs::read(&saved_path)?, TINY_PNG_BYTES);
|
||||
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
@@ -256,7 +256,7 @@ async fn standalone_image_edit_uses_attached_model_visible_image() -> Result<()>
|
||||
|
||||
#[tokio::test]
|
||||
async fn standalone_image_edit_uses_recent_pathless_image() -> Result<()> {
|
||||
let image_url = "https://example.com/reference.png";
|
||||
let image_url = TINY_PNG_DATA_URL;
|
||||
let edit_request = run_image_edit_test(|_| {
|
||||
Ok((
|
||||
json!({
|
||||
|
||||
@@ -52,6 +52,7 @@ mod remote_control;
|
||||
mod remote_thread_store;
|
||||
mod request_permissions;
|
||||
mod request_user_input;
|
||||
mod request_validation;
|
||||
mod review;
|
||||
mod safety_check_downgrade;
|
||||
mod skills_list;
|
||||
|
||||
@@ -60,22 +60,27 @@ description = "Inspect without writes."
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: "audit".to_string(),
|
||||
description: Some("Inspect without writes.".to_string()),
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: "dev".to_string(),
|
||||
description: Some("Day-to-day coding work.".to_string()),
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
next_cursor: None,
|
||||
@@ -126,14 +131,17 @@ description = "Project-scoped profile."
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
next_cursor: Some("3".to_string()),
|
||||
@@ -155,6 +163,7 @@ description = "Project-scoped profile."
|
||||
data: vec![PermissionProfileSummary {
|
||||
id: "project".to_string(),
|
||||
description: Some("Project-scoped profile.".to_string()),
|
||||
allowed: true,
|
||||
}],
|
||||
next_cursor: None,
|
||||
}
|
||||
@@ -200,18 +209,22 @@ description = "Project-scoped profile."
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(),
|
||||
description: None,
|
||||
allowed: true,
|
||||
},
|
||||
PermissionProfileSummary {
|
||||
id: "project".to_string(),
|
||||
description: Some("Project-scoped profile.".to_string()),
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
next_cursor: None,
|
||||
|
||||
@@ -753,7 +753,9 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab
|
||||
composer_icon: None,
|
||||
composer_icon_url: None,
|
||||
logo: None,
|
||||
logo_dark: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
screenshots: Vec::new(),
|
||||
screenshot_urls: Vec::new(),
|
||||
}),
|
||||
@@ -1761,6 +1763,14 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(empty_page_body))
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/plugins/featured"))
|
||||
.and(query_param("platform", "codex"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated-remote"]"#),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -1854,7 +1864,10 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
|
||||
cached_plugin_ids,
|
||||
vec!["plugins~Plugin_00000000000000000000000000000000".to_string()]
|
||||
);
|
||||
assert_eq!(response.featured_plugin_ids, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
response.featured_plugin_ids,
|
||||
vec!["linear@openai-curated-remote".to_string()]
|
||||
);
|
||||
assert!(
|
||||
!server
|
||||
.received_requests()
|
||||
|
||||
@@ -162,7 +162,8 @@ apps = true
|
||||
"short_description": "Example plugin",
|
||||
"capabilities": [],
|
||||
"default_prompt": "Use the legacy example prompt",
|
||||
"default_prompts": []
|
||||
"default_prompts": [],
|
||||
"logo_url_dark": "https://example.com/example-plugin-dark.png"
|
||||
},
|
||||
"skills": [],
|
||||
"mcp_servers": [
|
||||
@@ -273,6 +274,15 @@ apps = true
|
||||
.and_then(|interface| interface.default_prompt.clone()),
|
||||
Some(vec!["Use the legacy example prompt".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.plugin
|
||||
.summary
|
||||
.interface
|
||||
.as_ref()
|
||||
.and_then(|interface| interface.logo_url_dark.as_deref()),
|
||||
Some("https://example.com/example-plugin-dark.png")
|
||||
);
|
||||
assert_eq!(
|
||||
response.plugin.mcp_servers,
|
||||
vec!["other-server".to_string()]
|
||||
@@ -1330,6 +1340,7 @@ async fn plugin_read_returns_plugin_details_with_bundle_contents() -> Result<()>
|
||||
"brandColor": "#3B82F6",
|
||||
"composerIcon": "./assets/icon.png",
|
||||
"logo": "./assets/logo.png",
|
||||
"logoDark": "./assets/logo-dark.png",
|
||||
"screenshots": ["./assets/screenshot1.png"]
|
||||
}
|
||||
}"##,
|
||||
@@ -1508,6 +1519,18 @@ enabled = false
|
||||
"Find my next action".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.plugin
|
||||
.summary
|
||||
.interface
|
||||
.as_ref()
|
||||
.and_then(|interface| interface.logo_dark.as_ref()),
|
||||
Some(
|
||||
&AbsolutePathBuf::try_from(plugin_root.join("assets/logo-dark.png"))
|
||||
.expect("absolute dark logo path")
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
response.plugin.summary.keywords,
|
||||
vec!["api-key".to_string(), "developer tools".to_string()]
|
||||
|
||||
@@ -1357,7 +1357,9 @@ fn expected_plugin_interface() -> PluginInterface {
|
||||
composer_icon: None,
|
||||
composer_icon_url: None,
|
||||
logo: None,
|
||||
logo_dark: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
screenshots: Vec::new(),
|
||||
screenshot_urls: Vec::new(),
|
||||
}
|
||||
|
||||
@@ -204,12 +204,11 @@ async fn consume_timeout_releases_account_auth_queue() -> Result<()> {
|
||||
"rate limit reset consume timed out"
|
||||
);
|
||||
|
||||
let account_error = read_error_response(&mut mcp, account_id).await?;
|
||||
assert_eq!(account_error.error.code, INVALID_REQUEST_ERROR_CODE);
|
||||
assert_eq!(
|
||||
account_error.error.message,
|
||||
"email and plan type are required for chatgpt authentication"
|
||||
);
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(account_id)),
|
||||
)
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,7 @@ async fn thread_delete_with_non_local_thread_store_does_not_create_local_persist
|
||||
let unloaded_thread_id = ThreadId::from_string(&Uuid::new_v4().to_string())?;
|
||||
thread_store
|
||||
.create_thread(StoreCreateThreadParams {
|
||||
session_id: unloaded_thread_id.into(),
|
||||
thread_id: unloaded_thread_id,
|
||||
extra_config: None,
|
||||
forked_from_id: None,
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::ImageDetail;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const REMOTE_IMAGE_URL_ERROR: &str =
|
||||
"remote image URLs are not supported; use an inline data URL instead";
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_handlers_reject_remote_image_urls() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_mock_responses_config_toml_with_chatgpt_base_url(
|
||||
codex_home.path(),
|
||||
"http://localhost/unused",
|
||||
"http://localhost/unused",
|
||||
)?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.await?;
|
||||
let thread_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_response)?;
|
||||
let thread_id = thread.id;
|
||||
|
||||
let remote_tool_output = serde_json::to_value(ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url: "https://example.com/tool.png".to_string(),
|
||||
detail: Some(ImageDetail::High),
|
||||
},
|
||||
]),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
})?;
|
||||
let requests = [
|
||||
(
|
||||
"turn/start",
|
||||
json!({
|
||||
"threadId": thread_id,
|
||||
"input": [{
|
||||
"type": "image",
|
||||
"url": "HTTP://example.com/start.png",
|
||||
"detail": "high"
|
||||
}]
|
||||
}),
|
||||
),
|
||||
(
|
||||
"turn/steer",
|
||||
json!({
|
||||
"threadId": thread_id,
|
||||
"expectedTurnId": "turn-id",
|
||||
"input": [{
|
||||
"type": "image",
|
||||
"url": "https://example.com/steer.png",
|
||||
"detail": "high"
|
||||
}]
|
||||
}),
|
||||
),
|
||||
(
|
||||
"thread/inject_items",
|
||||
json!({
|
||||
"threadId": thread_id,
|
||||
"items": [remote_tool_output]
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (method, params) in requests {
|
||||
let request_id = mcp.send_raw_request(method, Some(params)).await?;
|
||||
let actual: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let expected = JSONRPCError {
|
||||
id: RequestId::Integer(request_id),
|
||||
error: JSONRPCErrorError {
|
||||
code: -32600,
|
||||
data: None,
|
||||
message: REMOTE_IMAGE_URL_ERROR.to_string(),
|
||||
},
|
||||
};
|
||||
assert_eq!(actual, expected, "unexpected response for {method}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -60,7 +60,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
||||
text: injected_text.to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
metadata: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
|
||||
let inject_req = mcp
|
||||
@@ -86,7 +86,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
||||
resumed_history
|
||||
.history
|
||||
.iter()
|
||||
.any(|item| matches!(item, RolloutItem::ResponseItem(response_item) if response_item == &injected_item)),
|
||||
.any(|item| matches!(item, RolloutItem::ResponseItem(response_item) if responses::strip_metadata(response_item.clone()) == injected_item)),
|
||||
"injected item should be persisted in rollout history"
|
||||
);
|
||||
|
||||
@@ -198,7 +198,7 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<()
|
||||
text: "Injected after first turn".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
metadata: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
let injected_value = serde_json::to_value(&injected_item)?;
|
||||
|
||||
|
||||
@@ -1252,6 +1252,7 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
CoreSessionSource::SubAgent(SubAgentSource::Review),
|
||||
parent_thread_id.into(),
|
||||
parent_thread_id,
|
||||
)?;
|
||||
let compact_id = create_fake_rollout_with_source(
|
||||
|
||||
@@ -1357,6 +1357,7 @@ async fn seed_pathless_store_thread(
|
||||
) -> Result<()> {
|
||||
store
|
||||
.create_thread(CreateThreadParams {
|
||||
session_id: thread_id.into(),
|
||||
thread_id,
|
||||
extra_config: None,
|
||||
forked_from_id: None,
|
||||
|
||||
@@ -2054,6 +2054,7 @@ stream_max_retries = 0
|
||||
let rollout_dir = rollout_path.parent().expect("rollout parent directory");
|
||||
std::fs::create_dir_all(rollout_dir)?;
|
||||
let session_meta = SessionMeta {
|
||||
session_id: conversation_id.into(),
|
||||
id: conversation_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
@@ -2588,7 +2589,7 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> {
|
||||
text: "history override".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
metadata: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -2735,6 +2736,7 @@ async fn thread_resume_rejects_mismatched_path_for_running_thread_id() -> Result
|
||||
"timestamp": "2025-01-01T00:00:00Z",
|
||||
"type": "session_meta",
|
||||
"payload": {
|
||||
"session_id": thread_uuid,
|
||||
"id": thread_uuid,
|
||||
"timestamp": "2025-01-01T00:00:00Z",
|
||||
"cwd": codex_home.path(),
|
||||
@@ -3596,7 +3598,7 @@ async fn thread_resume_supports_history_and_overrides() -> Result<()> {
|
||||
text: history_text.to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
metadata: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}];
|
||||
|
||||
// Resume with explicit history and override the model.
|
||||
|
||||
@@ -117,10 +117,7 @@ async fn thread_settings_update_multi_agent_mode_applies_to_future_turns() -> Re
|
||||
write_mock_responses_config_toml(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
&BTreeMap::from([
|
||||
(Feature::MultiAgentV2, true),
|
||||
(Feature::MultiAgentMode, true),
|
||||
]),
|
||||
&BTreeMap::from([(Feature::MultiAgentV2, true)]),
|
||||
/*auto_compact_limit*/ 200_000,
|
||||
/*requires_openai_auth*/ None,
|
||||
"mock_provider",
|
||||
@@ -158,7 +155,7 @@ async fn thread_settings_update_multi_agent_mode_applies_to_future_turns() -> Re
|
||||
assert_eq!(updated.thread_id, thread.id);
|
||||
assert_eq!(
|
||||
updated.thread_settings.multi_agent_mode,
|
||||
Some(MultiAgentMode::Proactive)
|
||||
MultiAgentMode::Proactive
|
||||
);
|
||||
|
||||
start_text_turn(&mut mcp, thread.id).await?;
|
||||
|
||||
@@ -208,6 +208,7 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
|
||||
let parent_thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000127")?;
|
||||
store
|
||||
.create_thread(CreateThreadParams {
|
||||
session_id: thread_id.into(),
|
||||
thread_id,
|
||||
extra_config: None,
|
||||
forked_from_id: Some(parent_thread_id),
|
||||
|
||||
@@ -100,12 +100,14 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const TEST_ORIGINATOR: &str = "codex_vscode";
|
||||
const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer.";
|
||||
const MULTI_AGENT_V2_NAMESPACE: &str = "collaboration";
|
||||
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
const TINY_PNG_BYTES: &[u8] = &[
|
||||
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0,
|
||||
0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84, 120, 156, 99, 96, 0, 2, 0, 0, 5, 0, 1,
|
||||
122, 94, 171, 63, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
|
||||
];
|
||||
const TINY_PNG_DATA_URL: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
|
||||
|
||||
fn body_contains(req: &wiremock::Request, text: &str) -> bool {
|
||||
String::from_utf8(req.body.clone())
|
||||
@@ -888,7 +890,7 @@ async fn turn_start_tracks_turn_event_analytics() -> Result<()> {
|
||||
thread_id: thread.id.clone(),
|
||||
client_user_message_id: None,
|
||||
input: vec![V2UserInput::Image {
|
||||
url: "https://example.com/a.png".to_string(),
|
||||
url: TINY_PNG_DATA_URL.to_string(),
|
||||
detail: None,
|
||||
}],
|
||||
responsesapi_client_metadata: Some(HashMap::from([(
|
||||
@@ -1768,10 +1770,7 @@ async fn turn_start_accepts_multi_agent_mode_v2() -> Result<()> {
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
"never",
|
||||
&BTreeMap::from([
|
||||
(Feature::MultiAgentV2, true),
|
||||
(Feature::MultiAgentMode, true),
|
||||
]),
|
||||
&BTreeMap::from([(Feature::MultiAgentV2, true)]),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
@@ -1845,10 +1844,7 @@ async fn thread_start_multi_agent_mode_initializes_first_turn() -> Result<()> {
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
"never",
|
||||
&BTreeMap::from([
|
||||
(Feature::MultiAgentV2, true),
|
||||
(Feature::MultiAgentMode, true),
|
||||
]),
|
||||
&BTreeMap::from([(Feature::MultiAgentV2, true)]),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
@@ -1871,7 +1867,7 @@ async fn thread_start_multi_agent_mode_initializes_first_turn() -> Result<()> {
|
||||
multi_agent_mode,
|
||||
..
|
||||
} = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
assert_eq!(multi_agent_mode, Some(MultiAgentMode::Proactive));
|
||||
assert_eq!(multi_agent_mode, MultiAgentMode::Proactive);
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
@@ -1911,27 +1907,29 @@ async fn thread_start_multi_agent_mode_initializes_first_turn() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_reports_selected_multi_agent_mode() -> Result<()> {
|
||||
async fn thread_start_reports_multi_agent_mode() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let cases = [
|
||||
(
|
||||
BTreeMap::from([(Feature::MultiAgentV2, true)]),
|
||||
Some(MultiAgentMode::Proactive),
|
||||
Some(MultiAgentMode::Proactive),
|
||||
MultiAgentMode::Proactive,
|
||||
),
|
||||
(
|
||||
BTreeMap::from([(Feature::MultiAgentV2, true)]),
|
||||
Some(MultiAgentMode::None),
|
||||
MultiAgentMode::None,
|
||||
),
|
||||
(
|
||||
BTreeMap::new(),
|
||||
Some(MultiAgentMode::Proactive),
|
||||
Some(MultiAgentMode::Proactive),
|
||||
MultiAgentMode::Proactive,
|
||||
),
|
||||
(
|
||||
BTreeMap::from([
|
||||
(Feature::MultiAgentV2, true),
|
||||
(Feature::MultiAgentMode, true),
|
||||
]),
|
||||
None,
|
||||
BTreeMap::from([(Feature::MultiAgentV2, true)]),
|
||||
None,
|
||||
MultiAgentMode::ExplicitRequestOnly,
|
||||
),
|
||||
];
|
||||
|
||||
@@ -3653,7 +3651,12 @@ async fn direct_input_to_multi_agent_v2_subagent_is_rejected() -> Result<()> {
|
||||
|req: &wiremock::Request| body_contains(req, PARENT_PROMPT),
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-parent-1"),
|
||||
responses::ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args),
|
||||
responses::ev_function_call_with_namespace(
|
||||
SPAWN_CALL_ID,
|
||||
MULTI_AGENT_V2_NAMESPACE,
|
||||
"spawn_agent",
|
||||
&spawn_args,
|
||||
),
|
||||
responses::ev_completed("resp-parent-1"),
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -156,8 +156,10 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
|
||||
search_body["input"]
|
||||
.as_array()
|
||||
.context("search input should be an array")?
|
||||
.last(),
|
||||
Some(&json!({
|
||||
.last()
|
||||
.cloned()
|
||||
.map(responses::strip_metadata_from_json),
|
||||
Some(json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "Search the web"}],
|
||||
@@ -165,7 +167,7 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
requests[1].function_call_output(call_id),
|
||||
responses::strip_metadata_from_json(requests[1].function_call_output(call_id)),
|
||||
json!({
|
||||
"type": "function_call_output",
|
||||
"call_id": call_id,
|
||||
|
||||
Reference in New Issue
Block a user