diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index 07602c1be8..30aedccf00 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -6,6 +6,7 @@ use std::time::Duration; use std::time::Instant; use anyhow::Result; +use anyhow::bail; use codex_features::Feature; use codex_login::CodexAuth; use codex_protocol::protocol::EventMsg; @@ -72,7 +73,8 @@ fn write_plugin_mcp_plugin(home: &TempDir, command: &str) { r#"{{ "mcpServers": {{ "sample": {{ - "command": "{command}" + "command": "{command}", + "startup_timeout_sec": 60.0 }} }} }}"# @@ -415,30 +417,58 @@ async fn plugin_mcp_tools_are_listed() -> Result<()> { write_plugin_mcp_plugin(codex_home.as_ref(), &rmcp_test_server_bin); let codex = build_plugin_test_codex(&server, codex_home).await?; - let tools_ready_deadline = Instant::now() + Duration::from_secs(30); - loop { - codex.submit(Op::ListMcpTools).await?; - let list_event = wait_for_event_with_timeout( - &codex, - |ev| matches!(ev, EventMsg::McpListToolsResponse(_)), - Duration::from_secs(10), - ) - .await; - let EventMsg::McpListToolsResponse(tool_list) = list_event else { - unreachable!("event guard guarantees McpListToolsResponse"); - }; - if tool_list.tools.contains_key("mcp__sample__echo") - && tool_list.tools.contains_key("mcp__sample__image") - { - break; - } - - let available_tools: Vec<&str> = tool_list.tools.keys().map(String::as_str).collect(); - if Instant::now() >= tools_ready_deadline { - panic!("timed out waiting for plugin MCP tools; discovered tools: {available_tools:?}"); - } - tokio::time::sleep(Duration::from_millis(200)).await; + let startup_event = wait_for_event_with_timeout( + &codex, + |ev| match ev { + EventMsg::McpStartupComplete(summary) => { + summary.ready.iter().any(|server| server == "sample") + || summary + .failed + .iter() + .any(|failure| failure.server == "sample") + || summary.cancelled.iter().any(|server| server == "sample") + } + _ => false, + }, + Duration::from_secs(70), + ) + .await; + let EventMsg::McpStartupComplete(startup) = startup_event else { + unreachable!("event guard guarantees McpStartupComplete"); + }; + if let Some(failure) = startup + .failed + .iter() + .find(|failure| failure.server == "sample") + { + let error = &failure.error; + bail!("plugin MCP server failed to start: {error}"); } + if startup.cancelled.iter().any(|server| server == "sample") { + bail!("plugin MCP server startup was cancelled"); + } + assert!( + startup.ready.iter().any(|server| server == "sample"), + "expected plugin MCP server to be ready; startup summary: {startup:?}" + ); + + codex.submit(Op::ListMcpTools).await?; + let list_event = wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::McpListToolsResponse(_)), + Duration::from_secs(10), + ) + .await; + let EventMsg::McpListToolsResponse(tool_list) = list_event else { + unreachable!("event guard guarantees McpListToolsResponse"); + }; + let mut available_tools: Vec<&str> = tool_list.tools.keys().map(String::as_str).collect(); + available_tools.sort_unstable(); + assert!( + tool_list.tools.contains_key("mcp__sample__echo") + && tool_list.tools.contains_key("mcp__sample__image"), + "expected plugin MCP tools to be listed; discovered tools: {available_tools:?}" + ); Ok(()) }