mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
## 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
112 lines
4.5 KiB
Rust
112 lines
4.5 KiB
Rust
//! 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");
|
|
}
|
|
}
|
|
}
|