Files
codex/codex-rs/codex-mcp/src/runtime/apps_tool_refresh_tests.rs
T
Dmitriy Kovalenko 3f797555cd Refresh Apps tools outside the MCP runtime gate (#60639)
## Why

Holding the session MCP gate during `tools/list` blocks other runtime access. Releasing it requires preventing stale refresh results from publishing after configuration or authentication changes.

## What changed

- Capture the Apps client under the session gate, then release the gate before fetching tools.
- Validate runtime and authentication state before publishing the shared cache and client catalog; retry a superseded refresh once.
- Wait for in-flight Apps refreshes before capturing model tools, preparing Apps calls, or shutting down the runtime.

## Testing

Add regression coverage for stale refreshes after runtime invalidation or logout, publication validation after call preparation drains, and a model step that waits for refreshed Apps tools while MCP status queries remain available.

GitOrigin-RevId: 42d4be8c4a9218933f8e3d07fc7f32f2da9cbd9d
2026-11-10 18:33:49 +00:00

92 lines
3.0 KiB
Rust

#![cfg(test)]
//! Verifies that an in-flight Apps refresh cannot publish after invalidation.
use super::*;
use tokio::sync::Notify;
#[tokio::test]
async fn stale_apps_refresh_does_not_publish() -> anyhow::Result<()> {
let home = tempfile::tempdir()?;
let auth = CodexAuth::from_external_chatgpt_tokens(
"header.e30.token",
"account",
/*chatgpt_plan_type*/ None,
)?;
let auth_manager =
AuthManager::from_auth_for_testing_with_home(auth.clone(), home.path().to_path_buf());
let cache = ConnectorRuntimeManager::<ToolInfo>::default().context(
home.path().to_path_buf(),
ConnectorRuntimeContextKey::personal(
/*account_id*/ None, /*chatgpt_user_id*/ None,
),
);
let started = Arc::new(Notify::new());
let release = Arc::new(Notify::new());
let connections = crate::connection_manager::tests::create_test_manager_with_ready_apps_client(
cache.clone(),
"search",
Some(started.clone()),
Some(release.clone()),
)
.await?;
connections.list_all_tools().await;
let revision = connections
.stable_catalog_revisions(/*required_servers*/ &[], &HashSet::new())
.await
.expect("Apps catalog is ready");
let runtime = McpRuntime::empty(/*prefix_mcp_tool_names*/ true);
runtime.current.store(Arc::new(PublishedMcpRuntime {
connections: connections.clone(),
config: Some(Arc::new(crate::mcp::tests::test_mcp_config(
home.path().to_path_buf(),
))),
auth_token: Some(auth.get_token()?),
auth: Some(auth),
auth_manager: Some(auth_manager.clone()),
auth_owner: None,
auth_generation: Arc::new(()),
plugins_available: false,
ready_selected_capability_roots: Vec::new(),
environment_selections: Arc::default(),
ready_environments: HashMap::new(),
cached_binding: Mutex::new(None),
}));
let refresh = runtime.capture_apps_tool_refresh();
tokio::pin!(refresh);
tokio::select! {
_ = started.notified() => {}
_ = &mut refresh => panic!("refresh completed before tools/list was released"),
}
runtime.invalidate_apps_tool_refresh();
release.notify_one();
assert!(
refresh.await?.is_none(),
"runtime invalidation supersedes the fetch"
);
let refresh = runtime.capture_apps_tool_refresh();
tokio::pin!(refresh);
tokio::select! {
_ = started.notified() => {}
_ = &mut refresh => panic!("refresh completed before tools/list was released"),
}
auth_manager.logout().await?;
release.notify_one();
assert!(refresh.await?.is_none(), "logout supersedes the fetch");
assert!(
!cache.has_current_tools(),
"stale tools reached the shared cache"
);
assert!(
connections
.stable_catalog_revisions(/*required_servers*/ &[], &HashSet::new())
.await
.as_ref()
== Some(&revision),
"a stale fetch invalidated the existing client catalog"
);
Ok(())
}