mirror of
https://github.com/openai/codex.git
synced 2026-09-08 15:50:34 +00:00
test: cover multiple used app connectors
This commit is contained in:
@@ -18,9 +18,12 @@ use wiremock::matchers::path_regex;
|
||||
|
||||
const CONNECTOR_ID: &str = "calendar";
|
||||
const CONNECTOR_NAME: &str = "Calendar";
|
||||
const GMAIL_CONNECTOR_ID: &str = "gmail";
|
||||
const GMAIL_CONNECTOR_NAME: &str = "Gmail";
|
||||
const DISCOVERABLE_CALENDAR_ID: &str = "connector_2128aebfecb84f64a069897515042a44";
|
||||
const DISCOVERABLE_GMAIL_ID: &str = "connector_68df038e0ba48191908c8434991bbac2";
|
||||
const CONNECTOR_DESCRIPTION: &str = "Plan events and manage your calendar.";
|
||||
const GMAIL_CONNECTOR_DESCRIPTION: &str = "Find and summarize email threads.";
|
||||
const CODEX_APPS_META_KEY: &str = "_codex_apps";
|
||||
const PROTOCOL_VERSION: &str = "2025-11-25";
|
||||
const SERVER_NAME: &str = "codex-apps-test";
|
||||
@@ -28,6 +31,7 @@ const SERVER_VERSION: &str = "1.0.0";
|
||||
const SEARCHABLE_TOOL_COUNT: usize = 100;
|
||||
const CALENDAR_CREATE_EVENT_TOOL_NAME: &str = "calendar_create_event";
|
||||
const CALENDAR_APP_ONLY_TOOL_NAME: &str = "calendar_app_only_action";
|
||||
const GMAIL_SEARCH_EMAIL_TOOL_NAME: &str = "gmail_search_email";
|
||||
pub const CALENDAR_EXTRACT_TEXT_TOOL_NAME: &str = "calendar_extract_text";
|
||||
const CALENDAR_LIST_EVENTS_TOOL_NAME: &str = "calendar_list_events";
|
||||
pub const DIRECT_CALENDAR_CREATE_EVENT_TOOL: &str = "mcp__codex_apps__calendar__create_event";
|
||||
@@ -39,6 +43,8 @@ pub const SEARCH_CALENDAR_APP_ONLY_TOOL: &str = "_app_only_action";
|
||||
pub const SEARCH_CALENDAR_CREATE_TOOL: &str = "_create_event";
|
||||
pub const SEARCH_CALENDAR_EXTRACT_TEXT_TOOL: &str = "_extract_text";
|
||||
pub const SEARCH_CALENDAR_LIST_TOOL: &str = "_list_events";
|
||||
pub const SEARCH_GMAIL_NAMESPACE: &str = "mcp__codex_apps__gmail";
|
||||
pub const SEARCH_GMAIL_SEARCH_EMAIL_TOOL: &str = "_search_email";
|
||||
pub const CALENDAR_CREATE_EVENT_RESOURCE_URI: &str =
|
||||
"connector://calendar/tools/calendar_create_event";
|
||||
pub const CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI: &str =
|
||||
@@ -46,6 +52,7 @@ pub const CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI: &str =
|
||||
const CALENDAR_LIST_EVENTS_RESOURCE_URI: &str = "connector://calendar/tools/calendar_list_events";
|
||||
pub const DOCUMENT_EXTRACT_TEXT_RESOURCE_URI: &str =
|
||||
"connector://calendar/tools/calendar_extract_text";
|
||||
pub const GMAIL_SEARCH_EMAIL_RESOURCE_URI: &str = "connector://gmail/tools/gmail_search_email";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppsTestServer {
|
||||
@@ -72,6 +79,7 @@ impl AppsTestServer {
|
||||
CONNECTOR_DESCRIPTION.to_string(),
|
||||
/*searchable*/ true,
|
||||
/*include_app_only_tool*/ false,
|
||||
/*include_gmail_tool*/ false,
|
||||
)
|
||||
.await;
|
||||
Ok(Self {
|
||||
@@ -91,6 +99,24 @@ impl AppsTestServer {
|
||||
CONNECTOR_DESCRIPTION.to_string(),
|
||||
/*searchable*/ false,
|
||||
/*include_app_only_tool*/ false,
|
||||
/*include_gmail_tool*/ false,
|
||||
)
|
||||
.await;
|
||||
Ok(Self {
|
||||
chatgpt_base_url: server.uri(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn mount_with_gmail(server: &MockServer) -> Result<Self> {
|
||||
mount_oauth_metadata(server).await;
|
||||
mount_connectors_directory(server).await;
|
||||
mount_streamable_http_json_rpc(
|
||||
server,
|
||||
CONNECTOR_NAME.to_string(),
|
||||
CONNECTOR_DESCRIPTION.to_string(),
|
||||
/*searchable*/ false,
|
||||
/*include_app_only_tool*/ false,
|
||||
/*include_gmail_tool*/ true,
|
||||
)
|
||||
.await;
|
||||
Ok(Self {
|
||||
@@ -110,6 +136,7 @@ impl AppsTestServer {
|
||||
CONNECTOR_DESCRIPTION.to_string(),
|
||||
matches!(tool_loading, AppsTestToolLoading::Searchable),
|
||||
/*include_app_only_tool*/ true,
|
||||
/*include_gmail_tool*/ false,
|
||||
)
|
||||
.await;
|
||||
Ok(Self {
|
||||
@@ -264,6 +291,7 @@ async fn mount_streamable_http_json_rpc(
|
||||
connector_description: String,
|
||||
searchable: bool,
|
||||
include_app_only_tool: bool,
|
||||
include_gmail_tool: bool,
|
||||
) {
|
||||
Mock::given(method("POST"))
|
||||
.and(path_regex("^/api/codex/apps/?$"))
|
||||
@@ -272,6 +300,7 @@ async fn mount_streamable_http_json_rpc(
|
||||
connector_description,
|
||||
searchable,
|
||||
include_app_only_tool,
|
||||
include_gmail_tool,
|
||||
})
|
||||
.mount(server)
|
||||
.await;
|
||||
@@ -282,6 +311,7 @@ struct CodexAppsJsonRpcResponder {
|
||||
connector_description: String,
|
||||
searchable: bool,
|
||||
include_app_only_tool: bool,
|
||||
include_gmail_tool: bool,
|
||||
}
|
||||
|
||||
impl Respond for CodexAppsJsonRpcResponder {
|
||||
@@ -475,6 +505,38 @@ impl Respond for CodexAppsJsonRpcResponder {
|
||||
}
|
||||
}));
|
||||
}
|
||||
if self.include_gmail_tool
|
||||
&& let Some(tools) = response
|
||||
.pointer_mut("/result/tools")
|
||||
.and_then(Value::as_array_mut)
|
||||
{
|
||||
tools.push(json!({
|
||||
"name": GMAIL_SEARCH_EMAIL_TOOL_NAME,
|
||||
"description": "Search Gmail messages.",
|
||||
"annotations": {
|
||||
"readOnlyHint": true
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": { "type": "string" },
|
||||
"limit": { "type": "integer" }
|
||||
},
|
||||
"required": ["query"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"_meta": {
|
||||
"connector_id": GMAIL_CONNECTOR_ID,
|
||||
"connector_name": GMAIL_CONNECTOR_NAME,
|
||||
"connector_description": GMAIL_CONNECTOR_DESCRIPTION,
|
||||
"_codex_apps": {
|
||||
"resource_uri": GMAIL_SEARCH_EMAIL_RESOURCE_URI,
|
||||
"contains_mcp_source": true,
|
||||
"connector_id": GMAIL_CONNECTOR_ID
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
ResponseTemplate::new(200).set_body_json(response)
|
||||
}
|
||||
"tools/call" => {
|
||||
|
||||
@@ -22,10 +22,13 @@ use core_test_support::apps_test_server::CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_
|
||||
use core_test_support::apps_test_server::CALENDAR_CREATE_EVENT_RESOURCE_URI;
|
||||
use core_test_support::apps_test_server::DIRECT_CALENDAR_CREATE_EVENT_TOOL as CALENDAR_CREATE_TOOL;
|
||||
use core_test_support::apps_test_server::DIRECT_CALENDAR_LIST_EVENTS_TOOL as CALENDAR_LIST_TOOL;
|
||||
use core_test_support::apps_test_server::GMAIL_SEARCH_EMAIL_RESOURCE_URI;
|
||||
use core_test_support::apps_test_server::SEARCH_CALENDAR_APP_ONLY_TOOL;
|
||||
use core_test_support::apps_test_server::SEARCH_CALENDAR_CREATE_TOOL;
|
||||
use core_test_support::apps_test_server::SEARCH_CALENDAR_LIST_TOOL;
|
||||
use core_test_support::apps_test_server::SEARCH_CALENDAR_NAMESPACE;
|
||||
use core_test_support::apps_test_server::SEARCH_GMAIL_NAMESPACE;
|
||||
use core_test_support::apps_test_server::SEARCH_GMAIL_SEARCH_EMAIL_TOOL;
|
||||
use core_test_support::apps_test_server::apps_enabled_builder;
|
||||
use core_test_support::apps_test_server::configure_search_capable_apps;
|
||||
use core_test_support::apps_test_server::configure_search_capable_model;
|
||||
@@ -221,6 +224,135 @@ async fn always_defer_feature_hides_small_app_tool_sets() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn app_tool_calls_track_multiple_used_connectors_e2e() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let apps_server = AppsTestServer::mount_with_gmail(&server).await?;
|
||||
let calendar_args = serde_json::to_string(&json!({
|
||||
"title": "Lunch",
|
||||
"starts_at": "2026-03-10T12:00:00Z",
|
||||
}))?;
|
||||
let gmail_args = serde_json::to_string(&json!({
|
||||
"query": "lunch",
|
||||
"limit": 5,
|
||||
}))?;
|
||||
let mock = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call_with_namespace(
|
||||
"calendar-call-1",
|
||||
SEARCH_CALENDAR_NAMESPACE,
|
||||
SEARCH_CALENDAR_CREATE_TOOL,
|
||||
&calendar_args,
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_function_call_with_namespace(
|
||||
"gmail-call-1",
|
||||
SEARCH_GMAIL_NAMESPACE,
|
||||
SEARCH_GMAIL_SEARCH_EMAIL_TOOL,
|
||||
&gmail_args,
|
||||
),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-3"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-3"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder =
|
||||
apps_enabled_builder(apps_server.chatgpt_base_url.clone()).with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Sqlite)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
test.submit_turn_with_approval_and_permission_profile(
|
||||
"Use Calendar, then search Gmail.",
|
||||
AskForApproval::Never,
|
||||
PermissionProfile::Disabled,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let requests = mock.requests();
|
||||
assert_eq!(requests.len(), 3);
|
||||
assert!(
|
||||
namespace_child_tool(
|
||||
&requests[0].body_json(),
|
||||
SEARCH_CALENDAR_NAMESPACE,
|
||||
SEARCH_CALENDAR_CREATE_TOOL
|
||||
)
|
||||
.is_some(),
|
||||
"first request should expose the Calendar tool"
|
||||
);
|
||||
assert!(
|
||||
namespace_child_tool(
|
||||
&requests[0].body_json(),
|
||||
SEARCH_GMAIL_NAMESPACE,
|
||||
SEARCH_GMAIL_SEARCH_EMAIL_TOOL
|
||||
)
|
||||
.is_some(),
|
||||
"first request should expose the Gmail tool"
|
||||
);
|
||||
|
||||
let calendar_tool_call = recorded_apps_tool_call_by_call_id(&server, "calendar-call-1").await;
|
||||
assert_eq!(
|
||||
calendar_tool_call.pointer("/params/_meta/_codex_apps"),
|
||||
Some(&json!({
|
||||
"call_id": "calendar-call-1",
|
||||
"resource_uri": CALENDAR_CREATE_EVENT_RESOURCE_URI,
|
||||
"contains_mcp_source": true,
|
||||
"connector_id": "calendar",
|
||||
"used_connector_ids": [],
|
||||
}))
|
||||
);
|
||||
|
||||
let gmail_tool_call = recorded_apps_tool_call_by_call_id(&server, "gmail-call-1").await;
|
||||
assert_eq!(
|
||||
gmail_tool_call.pointer("/params/_meta/_codex_apps"),
|
||||
Some(&json!({
|
||||
"call_id": "gmail-call-1",
|
||||
"resource_uri": GMAIL_SEARCH_EMAIL_RESOURCE_URI,
|
||||
"contains_mcp_source": true,
|
||||
"connector_id": "gmail",
|
||||
"used_connector_ids": ["calendar"],
|
||||
}))
|
||||
);
|
||||
|
||||
let state_db = test.codex.state_db().expect("state db enabled");
|
||||
let mut metadata = None;
|
||||
for _ in 0..100 {
|
||||
metadata = state_db
|
||||
.get_thread(test.session_configured.thread_id)
|
||||
.await?;
|
||||
if metadata
|
||||
.as_ref()
|
||||
.is_some_and(|metadata| metadata.used_connector_ids.len() == 2)
|
||||
{
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
let metadata = metadata.expect("thread metadata should be persisted");
|
||||
assert_eq!(
|
||||
metadata.used_connector_ids,
|
||||
vec!["calendar".to_string(), "gmail".to_string()]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn app_only_tools_are_not_visible_or_runnable_by_direct_model_calls() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
Reference in New Issue
Block a user