mirror of
https://github.com/openai/codex.git
synced 2026-11-09 08:40:33 +00:00
Respect config layers when removing plugin marketplaces (#40683)
## Why Removing a base-user marketplace must not delete a snapshot that is still referenced by another enabled configuration layer. ## What changed - Load the current configuration stack for marketplace commands in the CLI and app server. - Reject removal when a matching marketplace is defined by another enabled layer, and identify the configuration source that must be updated instead. - Allow cleanup of an installed marketplace snapshot even when no base-user entry exists. ## Testing Cover project, enterprise-managed, system, and session-flag configurations, as well as snapshot-only removal through the CLI and app server. GitOrigin-RevId: 4f0853a40c2b5fae0410696b79eee4284239c7a9
This commit is contained in:
1 parent
f1a6f7c9b3
commit
a6e63f9f32
7 files changed
+253
-47
No files matched your search
@@ -9,9 +9,12 @@ use codex_app_server_protocol::MarketplaceRemoveResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_config::MarketplaceConfigUpdate;
|
||||
use codex_config::record_user_marketplace;
|
||||
use codex_core::config::set_project_trust_level;
|
||||
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use test_case::test_case;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
@@ -43,16 +46,19 @@ fn canonicalize_path_with_existing_parent(path: &std::path::Path) -> Result<std:
|
||||
Ok(parent.canonicalize()?.join(file_name))
|
||||
}
|
||||
|
||||
#[test_case(false; "snapshot only")]
|
||||
#[test_case(true; "user entry and snapshot")]
|
||||
#[tokio::test]
|
||||
async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()> {
|
||||
async fn marketplace_remove_deletes_config_and_installed_root(user_entry: bool) -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
record_user_marketplace(codex_home.path(), "debug", &configured_marketplace_update())?;
|
||||
if user_entry {
|
||||
record_user_marketplace(codex_home.path(), "debug", &configured_marketplace_update())?;
|
||||
}
|
||||
write_installed_marketplace(codex_home.path(), "debug")?;
|
||||
let installed_root = marketplace_install_root(codex_home.path()).join("debug");
|
||||
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let response: MarketplaceRemoveResponse = mcp
|
||||
@@ -72,8 +78,13 @@ async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()> {
|
||||
canonicalize_path_with_existing_parent(&installed_root)?,
|
||||
);
|
||||
|
||||
let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
|
||||
assert!(!config.contains("[marketplaces.debug]"));
|
||||
let config_path = codex_home.path().join("config.toml");
|
||||
if user_entry {
|
||||
let config = std::fs::read_to_string(config_path)?;
|
||||
assert!(!config.contains("[marketplaces.debug]"));
|
||||
} else {
|
||||
assert!(!config_path.exists());
|
||||
}
|
||||
assert!(
|
||||
!marketplace_install_root(codex_home.path())
|
||||
.join("debug")
|
||||
@@ -111,3 +122,54 @@ async fn marketplace_remove_rejects_unknown_marketplace() -> Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(false; "project only")]
|
||||
#[test_case(true; "project and user")]
|
||||
#[tokio::test]
|
||||
async fn marketplace_remove_preserves_project_marketplace(user_entry: bool) -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
if user_entry {
|
||||
record_user_marketplace(codex_home.path(), "debug", &configured_marketplace_update())?;
|
||||
}
|
||||
// TestAppServer starts in CODEX_HOME, so make it a trusted project as well.
|
||||
std::fs::create_dir_all(codex_home.path().join(".git"))?;
|
||||
std::fs::create_dir_all(codex_home.path().join(".codex"))?;
|
||||
let project_config_path = codex_home.path().join(".codex/config.toml");
|
||||
let project_config = "[marketplaces.debug]\nsource_type = \"git\"\nsource = \"https://github.com/owner/repo.git\"\n";
|
||||
std::fs::write(&project_config_path, project_config)?;
|
||||
set_project_trust_level(codex_home.path(), codex_home.path(), TrustLevel::Trusted)?;
|
||||
write_installed_marketplace(codex_home.path(), "debug")?;
|
||||
let snapshot_path =
|
||||
marketplace_install_root(codex_home.path()).join("debug/.agents/plugins/marketplace.json");
|
||||
let user_config_path = codex_home.path().join("config.toml");
|
||||
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let user_config = std::fs::read_to_string(&user_config_path)?;
|
||||
let request_id = mcp
|
||||
.send_marketplace_remove_request(MarketplaceRemoveParams {
|
||||
marketplace_name: "debug".to_string(),
|
||||
})
|
||||
.await?;
|
||||
let err = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(err.error.code, -32600);
|
||||
assert!(
|
||||
err.error
|
||||
.message
|
||||
.starts_with("marketplace `debug` is configured in project (")
|
||||
);
|
||||
assert_eq!(std::fs::read_to_string(user_config_path)?, user_config);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(project_config_path)?,
|
||||
project_config
|
||||
);
|
||||
assert_eq!(std::fs::read_to_string(snapshot_path)?, "{}");
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user