mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
a34da3b295
Summary\n- Await endpoint recommendation selection while constructing each authenticated turn, removing the first-turn cache race.\n- Snapshot and filter endpoint candidates once per turn, then use that same set for the bounded contextual user fragment, tool exposure, and exact install validation.\n- Keep recommendation selection ephemeral: do not persist recommendation state in or gate resumed threads on prior context.\n- Hide the legacy list tool in endpoint mode and preserve legacy discovery unchanged when the endpoint is disabled or unavailable.\n- Keep remote plugin and connector app identities out of model-visible context and attach them only to Codex-owned elicitation metadata.\n\nStack\n- 3/4, based on #28400.\n- Endpoint client and cache: #28399.\n- Generalized suggestion presentation: #28400.\n- Install-schema follow-up: #28403.\n\nValidation\n- \n- \n- \n- \n- Full : 2,649 passed and 88 environment-dependent tests failed because this sandbox cannot write , nest Seatbelt, or locate auxiliary test binaries.
166 lines
5.8 KiB
Rust
166 lines
5.8 KiB
Rust
use anyhow::Result;
|
|
use app_test_support::ChatGptIdTokenClaims;
|
|
use app_test_support::TestAppServer;
|
|
use app_test_support::encode_id_token;
|
|
use app_test_support::to_response;
|
|
use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url;
|
|
use codex_app_server_protocol::JSONRPCResponse;
|
|
use codex_app_server_protocol::LoginAccountResponse;
|
|
use codex_app_server_protocol::RequestId;
|
|
use codex_app_server_protocol::ThreadStartParams;
|
|
use codex_app_server_protocol::ThreadStartResponse;
|
|
use codex_app_server_protocol::TurnStartParams;
|
|
use codex_app_server_protocol::UserInput;
|
|
use core_test_support::apps_test_server::AppsTestServer;
|
|
use core_test_support::responses;
|
|
use serde_json::Value;
|
|
use serde_json::json;
|
|
use std::time::Duration;
|
|
use tempfile::TempDir;
|
|
use tokio::time::timeout;
|
|
use wiremock::Mock;
|
|
use wiremock::ResponseTemplate;
|
|
use wiremock::matchers::method;
|
|
use wiremock::matchers::path;
|
|
use wiremock::matchers::query_param;
|
|
|
|
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(20);
|
|
const WORKSPACE_ID: &str = "123e4567-e89b-42d3-a456-426614174010";
|
|
|
|
#[tokio::test]
|
|
async fn first_turn_after_external_login_waits_for_recommended_plugins() -> Result<()> {
|
|
let server = responses::start_mock_server().await;
|
|
let apps_server = AppsTestServer::mount(&server).await?;
|
|
Mock::given(method("GET"))
|
|
.and(path("/ps/plugins/suggested"))
|
|
.and(query_param("scope", "GLOBAL"))
|
|
.respond_with(
|
|
ResponseTemplate::new(200)
|
|
.set_delay(Duration::from_millis(250))
|
|
.set_body_json(json!({
|
|
"enabled": true,
|
|
"plugins": [{
|
|
"id": "plugin_github",
|
|
"name": "github",
|
|
"status": "ENABLED",
|
|
"installation_policy": "AVAILABLE",
|
|
"release": {"display_name": "GitHub"}
|
|
}]
|
|
})),
|
|
)
|
|
.expect(1)
|
|
.mount(&server)
|
|
.await;
|
|
let response = responses::sse(vec![
|
|
responses::ev_response_created("resp-1"),
|
|
responses::ev_assistant_message("msg-1", "done"),
|
|
responses::ev_completed("resp-1"),
|
|
]);
|
|
let responses_mock = responses::mount_sse_once(&server, response).await;
|
|
|
|
let codex_home = TempDir::new()?;
|
|
write_mock_responses_config_toml_with_chatgpt_base_url(
|
|
codex_home.path(),
|
|
&server.uri(),
|
|
&apps_server.chatgpt_base_url,
|
|
)?;
|
|
let config_path = codex_home.path().join("config.toml");
|
|
let config = std::fs::read_to_string(&config_path)?;
|
|
std::fs::write(
|
|
config_path,
|
|
format!(
|
|
"{config}\n[features]\napps = true\nplugins = true\nremote_plugin = true\ntool_suggest = true\n"
|
|
),
|
|
)?;
|
|
|
|
let sqlite_home = codex_home.path().to_string_lossy();
|
|
let mut app_server = TestAppServer::new_without_managed_config_with_env(
|
|
codex_home.path(),
|
|
&[("CODEX_SQLITE_HOME", Some(sqlite_home.as_ref()))],
|
|
)
|
|
.await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, app_server.initialize()).await??;
|
|
|
|
let access_token = encode_id_token(
|
|
&ChatGptIdTokenClaims::new()
|
|
.email("embedded@example.com")
|
|
.plan_type("pro")
|
|
.chatgpt_account_id(WORKSPACE_ID),
|
|
)?;
|
|
let login_id = app_server
|
|
.send_chatgpt_auth_tokens_login_request(
|
|
access_token,
|
|
WORKSPACE_ID.to_string(),
|
|
Some("pro".to_string()),
|
|
)
|
|
.await?;
|
|
let login_response: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
app_server.read_stream_until_response_message(RequestId::Integer(login_id)),
|
|
)
|
|
.await??;
|
|
assert_eq!(
|
|
to_response::<LoginAccountResponse>(login_response)?,
|
|
LoginAccountResponse::ChatgptAuthTokens {}
|
|
);
|
|
|
|
let thread_id = app_server
|
|
.send_thread_start_request(ThreadStartParams {
|
|
model: Some("mock-model".to_string()),
|
|
..Default::default()
|
|
})
|
|
.await?;
|
|
let thread_response: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
app_server.read_stream_until_response_message(RequestId::Integer(thread_id)),
|
|
)
|
|
.await??;
|
|
let ThreadStartResponse { thread, .. } = to_response(thread_response)?;
|
|
|
|
let turn_id = app_server
|
|
.send_turn_start_request(TurnStartParams {
|
|
thread_id: thread.id,
|
|
input: vec![UserInput::Text {
|
|
text: "suggest a plugin".to_string(),
|
|
text_elements: Vec::new(),
|
|
}],
|
|
..Default::default()
|
|
})
|
|
.await?;
|
|
let _: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
app_server.read_stream_until_response_message(RequestId::Integer(turn_id)),
|
|
)
|
|
.await??;
|
|
timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
app_server.read_stream_until_notification_message("turn/completed"),
|
|
)
|
|
.await??;
|
|
|
|
let requests = responses_mock.requests();
|
|
let request = requests
|
|
.iter()
|
|
.find(|request| {
|
|
request
|
|
.message_input_texts("user")
|
|
.iter()
|
|
.any(|text| text.contains("suggest a plugin"))
|
|
})
|
|
.expect("turn request");
|
|
let contextual_user_message = request.message_input_texts("user").join("\n");
|
|
assert!(contextual_user_message.contains("<recommended_plugins>"));
|
|
assert!(contextual_user_message.contains("- GitHub (github@openai-curated-remote)"));
|
|
let body = request.body_json();
|
|
let tool_names = body
|
|
.get("tools")
|
|
.and_then(Value::as_array)
|
|
.into_iter()
|
|
.flatten()
|
|
.filter_map(|tool| tool.get("name").and_then(Value::as_str))
|
|
.collect::<Vec<_>>();
|
|
assert!(tool_names.contains(&"request_plugin_install"));
|
|
assert!(!tool_names.contains(&"list_available_plugins_to_install"));
|
|
Ok(())
|
|
}
|