Handle partial remote installed plugin failures

This commit is contained in:
xli-oai
2026-05-29 20:27:01 -07:00
parent 00ca857d3f
commit a8d7eff801
2 changed files with 180 additions and 6 deletions
@@ -2226,6 +2226,140 @@ plugin_sharing = true
Ok(())
}
#[tokio::test]
async fn plugin_installed_keeps_global_remote_plugins_when_workspace_fetch_fails() -> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
std::fs::write(
codex_home.path().join("config.toml"),
format!(
r#"chatgpt_base_url = "{}/backend-api/"
[features]
plugins = true
remote_plugin = true
plugin_sharing = false
"#,
server.uri()
),
)?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.chatgpt_user_id("user-123")
.chatgpt_account_id("account-123"),
AuthCredentialsStoreMode::File,
)?;
let global_installed_body = remote_installed_plugin_body("", "1.2.3", /*enabled*/ true);
mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await;
mount_remote_installed_plugins_error(&server, "WORKSPACE").await;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_installed_request(PluginInstalledParams {
cwds: None,
install_suggestion_plugin_names: None,
})
.await?;
let response: PluginInstalledResponse = to_response(
timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??,
)?;
assert_eq!(response.marketplaces.len(), 1);
assert_eq!(response.marketplaces[0].name, "openai-curated-remote");
assert_eq!(
response.marketplaces[0]
.plugins
.iter()
.map(|plugin| (plugin.id.clone(), plugin.installed, plugin.enabled))
.collect::<Vec<_>>(),
vec![("linear@openai-curated-remote".to_string(), true, true)]
);
wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;
wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;
Ok(())
}
#[tokio::test]
async fn plugin_installed_keeps_workspace_remote_plugins_when_global_fetch_fails() -> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
std::fs::write(
codex_home.path().join("config.toml"),
format!(
r#"chatgpt_base_url = "{}/backend-api/"
[features]
plugins = true
remote_plugin = false
plugin_sharing = true
"#,
server.uri()
),
)?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.chatgpt_user_id("user-123")
.chatgpt_account_id("account-123"),
AuthCredentialsStoreMode::File,
)?;
let workspace_installed_body = workspace_remote_plugin_page_body(
"plugins~Plugin_22222222222222222222222222222222",
"shared-linear",
"Shared Linear",
"PRIVATE",
/*enabled*/ Some(true),
);
mount_remote_installed_plugins_error(&server, "GLOBAL").await;
mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_installed_request(PluginInstalledParams {
cwds: None,
install_suggestion_plugin_names: None,
})
.await?;
let response: PluginInstalledResponse = to_response(
timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??,
)?;
assert_eq!(response.marketplaces.len(), 1);
assert_eq!(response.marketplaces[0].name, "workspace-shared-with-me");
assert_eq!(
response.marketplaces[0]
.plugins
.iter()
.map(|plugin| (plugin.id.clone(), plugin.installed, plugin.enabled))
.collect::<Vec<_>>(),
vec![(
"shared-linear@workspace-shared-with-me".to_string(),
true,
true
)]
);
wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;
wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;
Ok(())
}
#[tokio::test]
async fn plugin_installed_starts_remote_installed_bundle_sync() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -3049,6 +3183,17 @@ async fn mount_remote_installed_plugins(server: &MockServer, scope: &str, body:
.await;
}
async fn mount_remote_installed_plugins_error(server: &MockServer, scope: &str) {
Mock::given(method("GET"))
.and(path("/backend-api/ps/plugins/installed"))
.and(query_param("scope", scope))
.and(header("authorization", "Bearer chatgpt-token"))
.and(header("chatgpt-account-id", "account-123"))
.respond_with(ResponseTemplate::new(500).set_body_string("remote installed fetch failed"))
.mount(server)
.await;
}
fn empty_remote_installed_plugins_body() -> &'static str {
r#"{
"plugins": [],
+35 -6
View File
@@ -689,12 +689,41 @@ pub(crate) async fn fetch_remote_installed_plugins(
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
};
let (global, workspace) = tokio::try_join!(global, workspace)?;
let mut installed_plugins = [global, workspace]
.into_iter()
.flat_map(|(_scope, plugins)| plugins)
.map(|plugin| remote_installed_plugin_to_cache_entry(&plugin))
.collect::<Result<Vec<_>, _>>()?;
let (global, workspace) = tokio::join!(global, workspace);
let mut installed_plugins = Vec::new();
let mut first_error = None;
let mut any_scope_succeeded = false;
for (scope, result) in [
(RemotePluginScope::Global, global),
(RemotePluginScope::Workspace, workspace),
] {
match result.and_then(|(_scope, plugins)| {
plugins
.into_iter()
.map(|plugin| remote_installed_plugin_to_cache_entry(&plugin))
.collect::<Result<Vec<_>, _>>()
}) {
Ok(scope_plugins) => {
any_scope_succeeded = true;
installed_plugins.extend(scope_plugins);
}
Err(err) => {
tracing::warn!(
scope = scope.api_value(),
error = %err,
"failed to fetch remote installed plugins for scope"
);
if first_error.is_none() {
first_error = Some(err);
}
}
}
}
if !any_scope_succeeded && let Some(err) = first_error {
return Err(err);
}
installed_plugins.sort_by(|left, right| {
left.marketplace_name
.cmp(&right.marketplace_name)