mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Centralize remote plugin mutations in PluginsManager (#42114)
## What changed - Move remote plugin install and uninstall orchestration from the app server into shared `PluginsManager` APIs. - Keep cache and backend mutations coordinated by the installed-plugin sync gate, and retain install outcomes long enough to protect newly materialized bundles during downstream setup. - Return structured operation errors and outcomes so callers can preserve JSON-RPC error mapping, telemetry, cache refreshes, and OAuth setup. ## Testing - Add regression coverage that verifies uninstall holds the mutation gate, preserves the local cache when the backend operation fails, and refreshes installed state after a successful uninstall. GitOrigin-RevId: de39f19a4e61c6e9c76ddc2c65d2ac130a4b7f88
This commit is contained in:
committed by
copyberry
parent
1f4c47343a
commit
ef76e6ac30
@@ -0,0 +1,111 @@
|
||||
//! Regression coverage for the shared mutation gate, refresh, and backend failure handling.
|
||||
|
||||
use super::*;
|
||||
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use crate::test_support::load_plugins_config;
|
||||
use crate::test_support::test_auth_manager;
|
||||
use crate::test_support::test_plugins_manager_with_auth_manager;
|
||||
use crate::test_support::write_file;
|
||||
use codex_protocol::auth::AuthMode;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use tempfile::TempDir;
|
||||
use tokio::sync::Notify;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
#[tokio::test]
|
||||
async fn uninstall_serializes_backend_mutations_and_preserves_cache_on_failure() {
|
||||
for uninstall_fails in [false, true] {
|
||||
let home = TempDir::new().unwrap();
|
||||
let server = MockServer::start().await;
|
||||
let mut config = load_plugins_config(home.path(), home.path()).await;
|
||||
config.chatgpt_base_url = format!("{}/backend-api", server.uri());
|
||||
let auth_manager = test_auth_manager(Some(AuthMode::Chatgpt));
|
||||
let auth = auth_manager.auth().await;
|
||||
let manager = Arc::new(test_plugins_manager_with_auth_manager(
|
||||
home.path().to_path_buf(),
|
||||
/*restriction_product*/ None,
|
||||
auth_manager,
|
||||
));
|
||||
let remote_id = "b1234567-89ab-4cde-8f01-234567890abc";
|
||||
let plugin_id = PluginId::new(
|
||||
"sample".to_string(),
|
||||
REMOTE_GLOBAL_MARKETPLACE_NAME.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
let cache = manager.store.plugin_base_root(&plugin_id);
|
||||
write_file(
|
||||
cache.join("1.0.0/.codex-plugin/plugin.json").as_path(),
|
||||
r#"{"name":"sample","version":"1.0.0"}"#,
|
||||
);
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!("/backend-api/ps/plugins/{remote_id}")))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"id": remote_id, "name": "sample", "scope": "GLOBAL",
|
||||
"installation_policy": "AVAILABLE", "authentication_policy": "ON_USE",
|
||||
"release": {"display_name": "Sample", "description": "Sample plugin", "interface": {}}
|
||||
}))).mount(&server).await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/ps/plugins/installed"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_json(json!({"plugins": [], "pagination": {"next_page_token": null}})),
|
||||
)
|
||||
.expect(if uninstall_fails { 0 } else { 1 })
|
||||
.mount(&server)
|
||||
.await;
|
||||
let gate_held = Arc::new(AtomicBool::new(false));
|
||||
let gate_held_at_post = Arc::clone(&gate_held);
|
||||
let manager_at_post = Arc::clone(&manager);
|
||||
Mock::given(method("POST"))
|
||||
.and(path(format!(
|
||||
"/backend-api/ps/plugins/{remote_id}/uninstall"
|
||||
)))
|
||||
.respond_with(move |_: &wiremock::Request| {
|
||||
gate_held_at_post.store(
|
||||
manager_at_post
|
||||
.remote_installed_plugin_bundle_sync_gate
|
||||
.available_permits()
|
||||
== 0,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
ResponseTemplate::new(if uninstall_fails { 400 } else { 200 })
|
||||
.set_body_json(json!({"id": remote_id, "enabled": false}))
|
||||
})
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let refreshed = Arc::new(Notify::new());
|
||||
let refreshed_callback = Arc::clone(&refreshed);
|
||||
let outcome = manager
|
||||
.uninstall_remote_plugin(
|
||||
&config,
|
||||
auth.as_ref(),
|
||||
remote_id,
|
||||
Some(Arc::new(move |_| refreshed_callback.notify_one())),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
(
|
||||
gate_held.load(Ordering::SeqCst),
|
||||
manager
|
||||
.remote_installed_plugin_bundle_sync_gate
|
||||
.available_permits(),
|
||||
cache.as_path().exists(),
|
||||
outcome.is_err()
|
||||
),
|
||||
(true, 1, uninstall_fails, uninstall_fails),
|
||||
);
|
||||
if !uninstall_fails {
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), refreshed.notified())
|
||||
.await
|
||||
.expect("successful uninstall should refresh installed state");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user