mirror of
https://github.com/openai/codex.git
synced 2026-05-03 04:42:20 +03:00
## Why Windows Bazel runs in the permissions stack exposed that app-server integration tests were launching normal plugin startup warmups in every subprocess. Those warmups can call `https://chatgpt.com/backend-api/plugins/featured` when a test is not specifically exercising plugin startup, which adds slow background work, noisy stderr, and dependence on external network state. The relevant startup/featured-plugin behavior was introduced across #15042 and #15264. A few app-server tests also had long optional waits or unbounded cleanup paths, making failures expensive to diagnose and contributing to slow Windows shards. One external-agent config test from #18246 used a GitHub-style marketplace source, which was enough to exercise the pending remote-import path but also meant the background completion task could attempt a real clone. ## What Changed - Adds explicit `AppServerRuntimeOptions` / `PluginStartupTasks` plumbing and a hidden debug-only `--disable-plugin-startup-tasks-for-tests` app-server flag, so integration tests can suppress startup plugin warmups without adding a production env-var gate. - Has the app-server test harness pass that hidden flag by default, while opting plugin-startup coverage back in for tests that intentionally exercise startup sync and featured-plugin warmup behavior. - Lowers normal app-server subprocess logging from `info`/`debug` to `warn` to avoid multi-megabyte stderr output in Bazel logs. - Prevents the external-agent config test from attempting a real marketplace clone by using an invalid non-local source while still exercising the pending-import completion path. - Bounds optional filesystem/realtime waits and fake WebSocket test-server shutdown so failures produce targeted timeouts instead of hanging a shard. - Fixes the Unix script-resolution test in `rmcp-client` to exercise PATH resolution directly and include the actual spawn error in failures. ## Verification - `cargo check -p codex-app-server` - `cargo clippy -p codex-app-server --tests -- -D warnings` - `cargo test -p codex-rmcp-client program_resolver::tests::test_unix_executes_script_without_extension` - `cargo test -p codex-app-server --test all external_agent_config_import_sends_completion_notification_after_pending_plugins_finish -- --nocapture` - `cargo test -p codex-app-server --test all plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request -- --nocapture` - Windows Local Bazel passed with this test-hardening bundle before it was extracted from #19606. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19683). * #19395 * #19394 * #19393 * #19392 * #19606 * __->__ #19683
186 lines
5.9 KiB
Rust
186 lines
5.9 KiB
Rust
use std::time::Duration;
|
|
|
|
use anyhow::Result;
|
|
use app_test_support::McpProcess;
|
|
use app_test_support::to_response;
|
|
use codex_app_server_protocol::ExternalAgentConfigImportResponse;
|
|
use codex_app_server_protocol::JSONRPCResponse;
|
|
use codex_app_server_protocol::PluginListParams;
|
|
use codex_app_server_protocol::PluginListResponse;
|
|
use codex_app_server_protocol::RequestId;
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::TempDir;
|
|
use tokio::time::timeout;
|
|
|
|
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
|
|
|
#[tokio::test]
|
|
async fn external_agent_config_import_sends_completion_notification_for_local_plugins() -> Result<()>
|
|
{
|
|
let codex_home = TempDir::new()?;
|
|
let marketplace_root = codex_home.path().join("marketplace");
|
|
let plugin_root = marketplace_root.join("plugins").join("sample");
|
|
std::fs::create_dir_all(marketplace_root.join(".agents/plugins"))?;
|
|
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
|
|
std::fs::write(
|
|
marketplace_root.join(".agents/plugins/marketplace.json"),
|
|
r#"{
|
|
"name": "debug",
|
|
"plugins": [
|
|
{
|
|
"name": "sample",
|
|
"source": {
|
|
"source": "local",
|
|
"path": "./plugins/sample"
|
|
}
|
|
}
|
|
]
|
|
}"#,
|
|
)?;
|
|
std::fs::write(
|
|
plugin_root.join(".codex-plugin/plugin.json"),
|
|
r#"{"name":"sample","version":"0.1.0"}"#,
|
|
)?;
|
|
std::fs::create_dir_all(codex_home.path().join(".claude"))?;
|
|
let settings = serde_json::json!({
|
|
"enabledPlugins": {
|
|
"sample@debug": true
|
|
},
|
|
"extraKnownMarketplaces": {
|
|
"debug": {
|
|
"source": "local",
|
|
"path": marketplace_root,
|
|
}
|
|
}
|
|
});
|
|
std::fs::write(
|
|
codex_home.path().join(".claude").join("settings.json"),
|
|
serde_json::to_string_pretty(&settings)?,
|
|
)?;
|
|
|
|
let home_dir = codex_home.path().display().to_string();
|
|
let mut mcp =
|
|
McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?;
|
|
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
let request_id = mcp
|
|
.send_raw_request(
|
|
"externalAgentConfig/import",
|
|
Some(serde_json::json!({
|
|
"migrationItems": [{
|
|
"itemType": "PLUGINS",
|
|
"description": "Import plugins",
|
|
"cwd": null,
|
|
"details": {
|
|
"plugins": [{
|
|
"marketplaceName": "debug",
|
|
"pluginNames": ["sample"]
|
|
}]
|
|
}
|
|
}]
|
|
})),
|
|
)
|
|
.await?;
|
|
|
|
let response: JSONRPCResponse = timeout(
|
|
DEFAULT_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
let response: ExternalAgentConfigImportResponse = to_response(response)?;
|
|
|
|
assert_eq!(response, ExternalAgentConfigImportResponse {});
|
|
let notification = timeout(
|
|
DEFAULT_TIMEOUT,
|
|
mcp.read_stream_until_notification_message("externalAgentConfig/import/completed"),
|
|
)
|
|
.await??;
|
|
assert_eq!(notification.method, "externalAgentConfig/import/completed");
|
|
|
|
let request_id = mcp
|
|
.send_plugin_list_request(PluginListParams { cwds: None })
|
|
.await?;
|
|
let response: JSONRPCResponse = timeout(
|
|
DEFAULT_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
let response: PluginListResponse = to_response(response)?;
|
|
let plugin = response
|
|
.marketplaces
|
|
.iter()
|
|
.find(|marketplace| marketplace.name == "debug")
|
|
.and_then(|marketplace| {
|
|
marketplace
|
|
.plugins
|
|
.iter()
|
|
.find(|plugin| plugin.name == "sample")
|
|
})
|
|
.expect("expected imported plugin to be listed");
|
|
assert!(plugin.installed);
|
|
assert!(plugin.enabled);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn external_agent_config_import_sends_completion_notification_after_pending_plugins_finish()
|
|
-> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
std::fs::create_dir_all(codex_home.path().join(".claude"))?;
|
|
// This test only needs a pending non-local plugin import. Use an invalid
|
|
// source so the background completion path cannot make a real network clone.
|
|
std::fs::write(
|
|
codex_home.path().join(".claude").join("settings.json"),
|
|
r#"{
|
|
"enabledPlugins": {
|
|
"formatter@acme-tools": true
|
|
},
|
|
"extraKnownMarketplaces": {
|
|
"acme-tools": {
|
|
"source": "not a valid marketplace source"
|
|
}
|
|
}
|
|
}"#,
|
|
)?;
|
|
|
|
let home_dir = codex_home.path().display().to_string();
|
|
let mut mcp =
|
|
McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?;
|
|
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
let request_id = mcp
|
|
.send_raw_request(
|
|
"externalAgentConfig/import",
|
|
Some(serde_json::json!({
|
|
"migrationItems": [{
|
|
"itemType": "PLUGINS",
|
|
"description": "Import plugins",
|
|
"cwd": null,
|
|
"details": {
|
|
"plugins": [{
|
|
"marketplaceName": "acme-tools",
|
|
"pluginNames": ["formatter"]
|
|
}]
|
|
}
|
|
}]
|
|
})),
|
|
)
|
|
.await?;
|
|
|
|
let response: JSONRPCResponse = timeout(
|
|
DEFAULT_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
let response: ExternalAgentConfigImportResponse = to_response(response)?;
|
|
assert_eq!(response, ExternalAgentConfigImportResponse {});
|
|
let notification = timeout(
|
|
DEFAULT_TIMEOUT,
|
|
mcp.read_stream_until_notification_message("externalAgentConfig/import/completed"),
|
|
)
|
|
.await??;
|
|
assert_eq!(notification.method, "externalAgentConfig/import/completed");
|
|
|
|
Ok(())
|
|
}
|