use anyhow::Result; use codex_config::CONFIG_TOML_FILE; use codex_config::MarketplaceConfigUpdate; use codex_config::record_user_marketplace; use predicates::str::contains; use std::path::Path; use tempfile::TempDir; fn codex_command(codex_home: &Path) -> Result { let mut cmd = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?); cmd.env("CODEX_HOME", codex_home); Ok(cmd) } fn codex_command_in(codex_home: &Path, current_dir: &Path) -> Result { let mut cmd = codex_command(codex_home)?; cmd.current_dir(current_dir); Ok(cmd) } fn configured_local_marketplace(source: &str) -> MarketplaceConfigUpdate<'_> { MarketplaceConfigUpdate { last_updated: "2026-05-06T00:00:00Z", last_revision: None, source_type: "local", source, ref_name: None, sparse_paths: &[], } } fn write_plugins_enabled_config(codex_home: &Path) -> Result<()> { std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[features] plugins = true "#, )?; Ok(()) } fn write_marketplace_source(source: &Path) -> Result<()> { std::fs::create_dir_all(source.join(".agents/plugins"))?; std::fs::create_dir_all(source.join("plugins/sample/.codex-plugin"))?; std::fs::write( source.join(".agents/plugins/marketplace.json"), r#"{ "name": "debug", "plugins": [ { "name": "sample", "source": { "source": "local", "path": "./plugins/sample" } } ] }"#, )?; std::fs::write( source.join("plugins/sample/.codex-plugin/plugin.json"), r#"{"name":"sample","description":"Sample plugin"}"#, )?; Ok(()) } fn setup_local_marketplace() -> Result<(TempDir, TempDir)> { let codex_home = TempDir::new()?; let source = TempDir::new()?; write_plugins_enabled_config(codex_home.path())?; write_marketplace_source(source.path())?; let source_path = source.path().to_string_lossy().into_owned(); record_user_marketplace( codex_home.path(), "debug", &configured_local_marketplace(&source_path), )?; Ok((codex_home, source)) } fn setup_unconfigured_local_marketplace() -> Result<(TempDir, TempDir)> { let codex_home = TempDir::new()?; let source = TempDir::new()?; write_plugins_enabled_config(codex_home.path())?; write_marketplace_source(source.path())?; Ok((codex_home, source)) } fn setup_configured_marketplace_without_manifest() -> Result<(TempDir, TempDir)> { let codex_home = TempDir::new()?; let source = TempDir::new()?; write_plugins_enabled_config(codex_home.path())?; let source_path = source.path().to_string_lossy().into_owned(); record_user_marketplace( codex_home.path(), "debug", &configured_local_marketplace(&source_path), )?; Ok((codex_home, source)) } fn setup_configured_marketplace_with_malformed_manifest() -> Result<(TempDir, TempDir)> { let codex_home = TempDir::new()?; let source = TempDir::new()?; write_plugins_enabled_config(codex_home.path())?; std::fs::create_dir_all(source.path().join(".agents/plugins"))?; std::fs::write( source.path().join(".agents/plugins/marketplace.json"), "{not valid json", )?; let source_path = source.path().to_string_lossy().into_owned(); record_user_marketplace( codex_home.path(), "debug", &configured_local_marketplace(&source_path), )?; Ok((codex_home, source)) } #[tokio::test] async fn marketplace_list_shows_configured_marketplace_names() -> Result<()> { let (codex_home, source) = setup_local_marketplace()?; codex_command(codex_home.path())? .args(["plugin", "marketplace", "list"]) .assert() .success() .stdout(contains("debug")) .stdout(contains(source.path().display().to_string())); Ok(()) } #[tokio::test] async fn plugin_list_shows_plugins_grouped_by_marketplace() -> Result<()> { let (codex_home, _source) = setup_local_marketplace()?; codex_command(codex_home.path())? .args(["plugin", "list"]) .assert() .success() .stdout(contains("Marketplace `debug`")) .stdout(contains("sample@debug (not installed)")); Ok(()) } #[tokio::test] async fn plugin_list_excludes_unconfigured_repo_local_marketplaces() -> Result<()> { let (codex_home, source) = setup_unconfigured_local_marketplace()?; codex_command_in(codex_home.path(), source.path())? .args(["plugin", "list"]) .assert() .success() .stdout(contains("No marketplace plugins found.")) .stdout(predicates::str::is_match("sample@debug").unwrap().not()); Ok(()) } #[tokio::test] async fn plugin_list_fails_when_configured_marketplace_snapshot_is_missing() -> Result<()> { let (codex_home, source) = setup_configured_marketplace_without_manifest()?; codex_command(codex_home.path())? .args(["plugin", "list"]) .assert() .failure() .stderr(contains( "failed to load configured marketplace snapshot(s):", )) .stderr(contains("`debug`")) .stderr(contains(source.path().display().to_string())) .stderr(contains( "marketplace root does not contain a supported manifest", )); Ok(()) } #[tokio::test] async fn plugin_add_and_remove_updates_installed_plugin_config() -> Result<()> { let (codex_home, _source) = setup_local_marketplace()?; codex_command(codex_home.path())? .args(["plugin", "add", "sample@debug"]) .assert() .success() .stdout(contains("Added plugin `sample` from marketplace `debug`.")); let config = std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE))?; assert!(config.contains("[plugins.\"sample@debug\"]")); codex_command(codex_home.path())? .args(["plugin", "remove", "sample", "--marketplace", "debug"]) .assert() .success() .stdout(contains( "Removed plugin `sample` from marketplace `debug`.", )); let config = std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE))?; assert!(!config.contains("[plugins.\"sample@debug\"]")); Ok(()) } #[tokio::test] async fn plugin_add_rejects_unconfigured_repo_local_marketplaces() -> Result<()> { let (codex_home, source) = setup_unconfigured_local_marketplace()?; codex_command_in(codex_home.path(), source.path())? .args(["plugin", "add", "sample@debug"]) .assert() .failure() .stderr(contains( "plugin `sample` was not found in marketplace `debug`", )); Ok(()) } #[tokio::test] async fn plugin_add_fails_when_configured_marketplace_snapshot_is_malformed() -> Result<()> { let (codex_home, _source) = setup_configured_marketplace_with_malformed_manifest()?; codex_command(codex_home.path())? .args(["plugin", "add", "sample@debug"]) .assert() .failure() .stderr(contains( "failed to load configured marketplace snapshot(s):", )) .stderr(contains("`debug`")) .stderr(contains("invalid marketplace file")) .stderr(contains("key must be a string")); Ok(()) } #[tokio::test] async fn plugin_add_reinstalls_from_configured_marketplace_snapshot() -> Result<()> { let (codex_home, _source) = setup_local_marketplace()?; codex_command(codex_home.path())? .args(["plugin", "add", "sample@debug"]) .assert() .success(); codex_command(codex_home.path())? .args(["plugin", "add", "sample@debug"]) .assert() .success() .stdout(contains("Added plugin `sample` from marketplace `debug`.")); assert!( codex_home .path() .join("plugins/cache/debug/sample/local/.codex-plugin/plugin.json") .is_file() ); Ok(()) } #[tokio::test] async fn plugin_remove_works_after_marketplace_is_removed() -> Result<()> { let (codex_home, _source) = setup_local_marketplace()?; codex_command(codex_home.path())? .args(["plugin", "add", "sample", "--marketplace", "debug"]) .assert() .success(); codex_command(codex_home.path())? .args(["plugin", "marketplace", "remove", "debug"]) .assert() .success(); codex_command(codex_home.path())? .args(["plugin", "remove", "sample@debug"]) .assert() .success() .stdout(contains( "Removed plugin `sample` from marketplace `debug`.", )); let config = std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE))?; assert!(!config.contains("[plugins.\"sample@debug\"]")); Ok(()) } #[tokio::test] async fn plugin_reload_command_help_uses_singular_plugin_name() -> Result<()> { let codex_home = TempDir::new()?; codex_command(codex_home.path())? .args(["plugin", "reload", "--help"]) .assert() .success() .stdout(contains("Usage: codex plugin reload")) .stdout(contains( "Reload Codex's local installed-plugin state from marketplaces", )) .stdout(contains("Clears Codex's local plugin and skill caches")) .stdout(contains( "Updated plugin context is picked up in new CLI sessions and other fresh loads", )) .stdout(contains( "Existing app-server-backed threads are not refreshed", )) .stdout(contains( "Plugin instructions and skills already loaded into an existing thread are not reread", )); Ok(()) } #[tokio::test] async fn plugin_add_rejects_cached_plugins_without_authorizing_marketplace_snapshot() -> Result<()> { let (codex_home, _source) = setup_local_marketplace()?; codex_command(codex_home.path())? .args(["plugin", "add", "sample@debug"]) .assert() .success(); codex_command(codex_home.path())? .args(["plugin", "marketplace", "remove", "debug"]) .assert() .success(); assert!( codex_home .path() .join("plugins/cache/debug/sample/local/.codex-plugin/plugin.json") .is_file() ); codex_command(codex_home.path())? .args(["plugin", "add", "sample@debug"]) .assert() .failure() .stderr(contains( "plugin `sample` was not found in marketplace `debug`", )); Ok(()) }