Codex Handbook
cli/tests/plugin_cli.rs 1114 lines
use anyhow::Result;use codex_config::CONFIG_TOML_FILE;use codex_config::MarketplaceConfigUpdate;use codex_config::record_user_marketplace;use codex_utils_absolute_path::canonicalize_existing_preserving_symlinks;use predicates::prelude::PredicateBooleanExt;use predicates::str::contains;use pretty_assertions::assert_eq;use serde_json::json;use std::path::Path;use tempfile::TempDir;const MARKETPLACE_HEADER: &str = "MARKETPLACE";const MARKETPLACE_LIST_HEADER: &str = "MARKETPLACE  ROOT";fn marketplace_list_row(marketplace_name: &str, root: &Path) -> String {    format!(        "{marketplace_name:<width$}  {}",        root.display(),        width = MARKETPLACE_HEADER.len()    )}fn codex_command(codex_home: &Path) -> Result<assert_cmd::Command> {    let mut cmd = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);    cmd.env("CODEX_HOME", codex_home);    cmd.env("HOME", codex_home);    Ok(cmd)}fn codex_command_in(codex_home: &Path, current_dir: &Path) -> Result<assert_cmd::Command> {    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_with_manifest(source: &Path, marketplace_manifest: &str) -> Result<()> {    std::fs::create_dir_all(source.join(".agents").join("plugins"))?;    std::fs::create_dir_all(source.join("plugins").join("sample").join(".codex-plugin"))?;    std::fs::write(        source            .join(".agents")            .join("plugins")            .join("marketplace.json"),        marketplace_manifest,    )?;    std::fs::write(        source            .join("plugins")            .join("sample")            .join(".codex-plugin")            .join("plugin.json"),        r#"{"name":"sample","version":"1.2.3","description":"Sample plugin"}"#,    )?;    Ok(())}fn write_marketplace_source(source: &Path) -> Result<()> {    write_marketplace_source_with_manifest(        source,        r#"{  "name": "debug",  "plugins": [    {      "name": "sample",      "source": {        "source": "local",        "path": "./plugins/sample"      }    }  ]}"#,    )}fn write_marketplace_source_with_explicit_empty_products(source: &Path) -> Result<()> {    write_marketplace_source_with_manifest(        source,        r#"{  "name": "debug",  "plugins": [    {      "name": "sample",      "source": {        "source": "local",        "path": "./plugins/sample"      },      "policy": {        "products": []      }    }  ]}"#,    )}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_local_marketplace_with_explicit_empty_products() -> Result<(TempDir, TempDir)> {    let codex_home = TempDir::new()?;    let source = TempDir::new()?;    write_plugins_enabled_config(codex_home.path())?;    write_marketplace_source_with_explicit_empty_products(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_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").join("plugins"))?;    std::fs::write(        source            .path()            .join(".agents")            .join("plugins")            .join("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))}fn setup_local_marketplace_with_implicit_system_roots() -> Result<(TempDir, TempDir, TempDir)> {    let (codex_home, source) = setup_local_marketplace()?;    let bundled_root = codex_home        .path()        .join(".tmp")        .join("bundled-marketplaces")        .join("openai-bundled");    std::fs::create_dir_all(&bundled_root)?;    let bundled_source = bundled_root.display().to_string();    record_user_marketplace(        codex_home.path(),        "openai-bundled",        &configured_local_marketplace(&bundled_source),    )?;    let cache_home = TempDir::new()?;    let runtime_root = cache_home        .path()        .join("codex-runtimes")        .join("codex-primary-runtime")        .join("plugins")        .join("openai-primary-runtime");    std::fs::create_dir_all(&runtime_root)?;    let runtime_source = runtime_root.display().to_string();    record_user_marketplace(        codex_home.path(),        "openai-primary-runtime",        &configured_local_marketplace(&runtime_source),    )?;    Ok((codex_home, source, cache_home))}fn setup_custom_marketplace_under_implicit_system_root() -> Result<(TempDir, std::path::PathBuf)> {    let codex_home = TempDir::new()?;    write_plugins_enabled_config(codex_home.path())?;    let custom_root = codex_home        .path()        .join(".tmp")        .join("bundled-marketplaces")        .join("custom-marketplace");    std::fs::create_dir_all(&custom_root)?;    let custom_source = custom_root.display().to_string();    record_user_marketplace(        codex_home.path(),        "custom-marketplace",        &configured_local_marketplace(&custom_source),    )?;    Ok((codex_home, custom_root))}fn remove_installed_plugin_config(codex_home: &Path, plugin_key: &str) -> Result<()> {    let config_path = codex_home.join(CONFIG_TOML_FILE);    let plugin_header = format!("[plugins.\"{plugin_key}\"]");    let config = std::fs::read_to_string(&config_path)?;    let mut rewritten = Vec::new();    let mut skipping = false;    for line in config.lines() {        if line == plugin_header {            skipping = true;            continue;        }        if skipping && line.starts_with('[') {            skipping = false;        }        if !skipping {            rewritten.push(line);        }    }    std::fs::write(config_path, format!("{}\n", rewritten.join("\n")))?;    Ok(())}fn setup_configured_local_marketplace_with_missing_source() -> Result<TempDir> {    let codex_home = TempDir::new()?;    std::fs::write(        codex_home.path().join(CONFIG_TOML_FILE),        r#"[features]plugins = true[marketplaces.debug]source_type = "local""#,    )?;    Ok(codex_home)}fn setup_configured_local_marketplace_with_invalid_name() -> Result<TempDir> {    let codex_home = TempDir::new()?;    std::fs::write(        codex_home.path().join(CONFIG_TOML_FILE),        r#"[features]plugins = true[marketplaces."bad/name"]source_type = "local"source = "/tmp/debug""#,    )?;    Ok(codex_home)}fn assert_configured_marketplace_snapshot_failure(    assert: assert_cmd::assert::Assert,    source: &Path,    detail: &str,) {    assert        .failure()        .stderr(contains(            "failed to load configured marketplace snapshot(s):",        ))        .stderr(contains("`debug`"))        .stderr(contains(source.display().to_string()))        .stderr(contains(detail));}fn assert_marketplace_failure(    assert: assert_cmd::assert::Assert,    marketplace_name: &str,    source: &Path,    detail: &str,) {    assert        .failure()        .stderr(contains("failed to load marketplace(s):"))        .stderr(contains(format!("`{marketplace_name}`")))        .stderr(contains(source.display().to_string()))        .stderr(contains(detail));}#[tokio::test]async fn marketplace_list_shows_configured_marketplace_names() -> Result<()> {    let (codex_home, source) = setup_local_marketplace()?;    let expected_row = marketplace_list_row("debug", source.path());    codex_command(codex_home.path())?        .args(["plugin", "marketplace", "list"])        .assert()        .success()        .stdout(contains(MARKETPLACE_LIST_HEADER))        .stdout(contains(&expected_row))        .stdout(contains("\t").not());    Ok(())}#[tokio::test]async fn marketplace_list_json_prints_configured_marketplaces() -> Result<()> {    let (codex_home, source) = setup_local_marketplace()?;    let source_path = source.path().display().to_string();    let assert = codex_command(codex_home.path())?        .args(["plugin", "marketplace", "list", "--json"])        .assert()        .success();    let stdout = assert.get_output().stdout.as_slice();    let actual: serde_json::Value = serde_json::from_slice(stdout)?;    assert_eq!(        actual,        json!({            "marketplaces": [                {                    "name": "debug",                    "root": source_path,                    "marketplaceSource": {                        "sourceType": "local",                        "source": source_path,                    },                },            ],        })    );    Ok(())}#[tokio::test]async fn marketplace_list_json_includes_configured_git_marketplace_source() -> Result<()> {    let codex_home = TempDir::new()?;    let marketplace_root = codex_home        .path()        .join(".tmp")        .join("marketplaces")        .join("debug");    write_plugins_enabled_config(codex_home.path())?;    write_marketplace_source(&marketplace_root)?;    let update = MarketplaceConfigUpdate {        last_updated: "2026-06-04T08:39:49Z",        last_revision: Some("abc123"),        source_type: "git",        source: "https://example.com/acme/agent-skills.git",        ref_name: None,        sparse_paths: &[],    };    record_user_marketplace(codex_home.path(), "debug", &update)?;    let normalized_root = canonicalize_existing_preserving_symlinks(&marketplace_root)?;    let assert = codex_command(codex_home.path())?        .args(["plugin", "marketplace", "list", "--json"])        .assert()        .success();    let stdout = assert.get_output().stdout.as_slice();    let actual: serde_json::Value = serde_json::from_slice(stdout)?;    assert_eq!(        actual,        json!({            "marketplaces": [                {                    "name": "debug",                    "root": normalized_root.display().to_string(),                    "marketplaceSource": {                        "sourceType": "git",                        "source": "https://example.com/acme/agent-skills.git",                    },                },            ],        })    );    Ok(())}#[tokio::test]async fn marketplace_list_json_keys_configured_source_by_root() -> Result<()> {    let codex_home = TempDir::new()?;    let home = TempDir::new()?;    let marketplace_root = codex_home        .path()        .join(".tmp")        .join("marketplaces")        .join("debug");    write_plugins_enabled_config(codex_home.path())?;    write_marketplace_source(home.path())?;    write_marketplace_source(&marketplace_root)?;    let update = MarketplaceConfigUpdate {        last_updated: "2026-06-04T08:39:49Z",        last_revision: Some("abc123"),        source_type: "git",        source: "https://example.com/acme/agent-skills.git",        ref_name: None,        sparse_paths: &[],    };    record_user_marketplace(codex_home.path(), "debug", &update)?;    let normalized_root = canonicalize_existing_preserving_symlinks(&marketplace_root)?;    let assert = codex_command(codex_home.path())?        .env("HOME", home.path())        .args(["plugin", "marketplace", "list", "--json"])        .assert()        .success();    let stdout = assert.get_output().stdout.as_slice();    let actual: serde_json::Value = serde_json::from_slice(stdout)?;    assert_eq!(        actual,        json!({            "marketplaces": [                {                    "name": "debug",                    "root": home.path().display().to_string(),                },                {                    "name": "debug",                    "root": normalized_root.display().to_string(),                    "marketplaceSource": {                        "sourceType": "git",                        "source": "https://example.com/acme/agent-skills.git",                    },                },            ],        })    );    Ok(())}#[tokio::test]async fn marketplace_list_includes_home_marketplace_when_present() -> Result<()> {    let codex_home = TempDir::new()?;    let home = TempDir::new()?;    write_marketplace_source(home.path())?;    write_plugins_enabled_config(codex_home.path())?;    let expected_row = marketplace_list_row("debug", home.path());    codex_command(codex_home.path())?        .env("HOME", home.path())        .args(["plugin", "marketplace", "list"])        .assert()        .success()        .stdout(contains(MARKETPLACE_LIST_HEADER))        .stdout(contains(&expected_row))        .stdout(contains("\t").not());    Ok(())}#[tokio::test]async fn marketplace_list_includes_root_when_plugins_are_filtered_out() -> Result<()> {    let (codex_home, source) = setup_local_marketplace_with_explicit_empty_products()?;    let expected_row = marketplace_list_row("debug", source.path());    codex_command(codex_home.path())?        .args(["plugin", "marketplace", "list"])        .assert()        .success()        .stdout(contains(MARKETPLACE_LIST_HEADER))        .stdout(contains(&expected_row));    Ok(())}#[tokio::test]async fn marketplace_list_fails_when_configured_marketplace_snapshot_is_missing() -> Result<()> {    let (codex_home, source) = setup_configured_marketplace_without_manifest()?;    assert_marketplace_failure(        codex_command(codex_home.path())?            .args(["plugin", "marketplace", "list"])            .assert(),        "debug",        source.path(),        "marketplace root does not contain a supported manifest",    );    Ok(())}#[tokio::test]async fn marketplace_list_fails_when_configured_marketplace_name_is_invalid() -> Result<()> {    let codex_home = setup_configured_local_marketplace_with_invalid_name()?;    assert_marketplace_failure(        codex_command(codex_home.path())?            .args(["plugin", "marketplace", "list"])            .assert(),        "bad/name",        Path::new("<invalid config>"),        "marketplace name",    );    Ok(())}#[tokio::test]async fn marketplace_list_fails_when_configured_local_marketplace_source_is_missing() -> Result<()>{    let codex_home = setup_configured_local_marketplace_with_missing_source()?;    codex_command(codex_home.path())?        .args(["plugin", "marketplace", "list"])        .assert()        .failure()        .stderr(contains("failed to load marketplace(s):"))        .stderr(contains("`debug`"))        .stderr(contains("<invalid source>"))        .stderr(contains(            "configured local marketplace source is missing or empty",        ));    Ok(())}#[tokio::test]async fn marketplace_list_fails_when_home_marketplace_is_malformed() -> Result<()> {    let codex_home = TempDir::new()?;    let home = TempDir::new()?;    write_plugins_enabled_config(codex_home.path())?;    std::fs::create_dir_all(home.path().join(".agents/plugins"))?;    let home_marketplace_path = home        .path()        .join(".agents")        .join("plugins")        .join("marketplace.json");    std::fs::write(&home_marketplace_path, "{not valid json")?;    codex_command(codex_home.path())?        .env("HOME", home.path())        .args(["plugin", "marketplace", "list"])        .assert()        .failure()        .stderr(contains("failed to load marketplace(s):"))        .stderr(contains(home_marketplace_path.display().to_string()))        .stderr(contains("key must be a string"));    Ok(())}#[tokio::test]async fn marketplace_list_fails_when_configured_marketplace_snapshot_is_malformed() -> Result<()> {    let (codex_home, source) = setup_configured_marketplace_with_malformed_manifest()?;    assert_marketplace_failure(        codex_command(codex_home.path())?            .args(["plugin", "marketplace", "list"])            .assert(),        "debug",        source.path(),        "key must be a string",    );    Ok(())}#[tokio::test]async fn plugin_list_prints_plugins_in_a_table() -> Result<()> {    let (codex_home, source) = setup_local_marketplace()?;    let marketplace_manifest = source        .path()        .join(".agents")        .join("plugins")        .join("marketplace.json");    let plugin_path = source.path().join("plugins").join("sample");    codex_command(codex_home.path())?        .args(["plugin", "list"])        .assert()        .success()        .stdout(contains("Marketplace `debug`"))        .stdout(contains("PLUGIN"))        .stdout(contains("STATUS"))        .stdout(contains("VERSION"))        .stdout(contains("PATH"))        .stdout(contains(marketplace_manifest.display().to_string()))        .stdout(contains("sample@debug"))        .stdout(contains("not installed"))        .stdout(contains(plugin_path.display().to_string()));    Ok(())}#[tokio::test]async fn plugin_list_json_prints_available_plugins_when_requested() -> Result<()> {    let (codex_home, source) = setup_local_marketplace()?;    let plugin_path = source.path().join("plugins").join("sample");    let source_path = source.path().to_string_lossy().into_owned();    let assert = codex_command(codex_home.path())?        .args(["plugin", "list", "--available", "--json"])        .assert()        .success();    let stdout = assert.get_output().stdout.as_slice();    let actual: serde_json::Value = serde_json::from_slice(stdout)?;    assert_eq!(        actual,        json!({            "installed": [],            "available": [                {                    "pluginId": "sample@debug",                    "name": "sample",                    "marketplaceName": "debug",                    "version": "1.2.3",                    "installed": false,                    "enabled": false,                    "source": {                        "source": "local",                        "path": plugin_path.display().to_string(),                    },                    "marketplaceSource": {                        "sourceType": "local",                        "source": source_path,                    },                    "installPolicy": "AVAILABLE",                    "authPolicy": "ON_INSTALL",                },            ],        })    );    Ok(())}#[tokio::test]async fn plugin_list_json_includes_configured_git_marketplace_source() -> Result<()> {    let codex_home = TempDir::new()?;    let marketplace_root = codex_home        .path()        .join(".tmp")        .join("marketplaces")        .join("debug");    write_plugins_enabled_config(codex_home.path())?;    write_marketplace_source(&marketplace_root)?;    let update = MarketplaceConfigUpdate {        last_updated: "2026-06-04T08:39:49Z",        last_revision: Some("abc123"),        source_type: "git",        source: "https://example.com/acme/agent-skills.git",        ref_name: None,        sparse_paths: &[],    };    record_user_marketplace(codex_home.path(), "debug", &update)?;    let plugin_path = marketplace_root.join("plugins").join("sample");    let normalized_plugin_path = canonicalize_existing_preserving_symlinks(&plugin_path)?;    let assert = codex_command(codex_home.path())?        .args(["plugin", "list", "--available", "--json"])        .assert()        .success();    let stdout = assert.get_output().stdout.as_slice();    let actual: serde_json::Value = serde_json::from_slice(stdout)?;    assert_eq!(        actual,        json!({            "installed": [],            "available": [                {                    "pluginId": "sample@debug",                    "name": "sample",                    "marketplaceName": "debug",                    "version": "1.2.3",                    "installed": false,                    "enabled": false,                    "source": {                        "source": "local",                        "path": normalized_plugin_path.display().to_string(),                    },                    "marketplaceSource": {                        "sourceType": "git",                        "source": "https://example.com/acme/agent-skills.git",                    },                    "installPolicy": "AVAILABLE",                    "authPolicy": "ON_INSTALL",                },            ],        })    );    Ok(())}#[tokio::test]async fn plugin_list_json_prints_installed_plugins() -> Result<()> {    let (codex_home, source) = setup_local_marketplace()?;    let plugin_path = source.path().join("plugins").join("sample");    let source_path = source.path().to_string_lossy().into_owned();    codex_command(codex_home.path())?        .args(["plugin", "add", "sample@debug"])        .assert()        .success();    let assert = codex_command(codex_home.path())?        .args(["plugin", "list", "--json"])        .assert()        .success();    let stdout = assert.get_output().stdout.as_slice();    let actual: serde_json::Value = serde_json::from_slice(stdout)?;    assert_eq!(        actual,        json!({            "installed": [                {                    "pluginId": "sample@debug",                    "name": "sample",                    "marketplaceName": "debug",                    "version": "1.2.3",                    "installed": true,                    "enabled": true,                    "source": {                        "source": "local",                        "path": plugin_path.display().to_string(),                    },                    "marketplaceSource": {                        "sourceType": "local",                        "source": source_path,                    },                    "installPolicy": "AVAILABLE",                    "authPolicy": "ON_INSTALL",                },            ],            "available": [],        })    );    Ok(())}#[tokio::test]async fn plugin_list_available_requires_json() -> Result<()> {    let (codex_home, _source) = setup_local_marketplace()?;    codex_command(codex_home.path())?        .args(["plugin", "list", "--available"])        .assert()        .failure()        .stderr(contains(            "the following required arguments were not provided",        ))        .stderr(contains("--json"));    Ok(())}#[tokio::test]async fn plugin_list_shows_installed_version_when_plugin_is_installed() -> 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", "list"])        .assert()        .success()        .stdout(contains("sample@debug"))        .stdout(contains("1.2.3"))        .stdout(contains("installed, enabled"));    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", "--marketplace", "debug"])        .assert()        .success()        .stdout(contains("No plugins found in marketplace `debug`."))        .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()?;    assert_configured_marketplace_snapshot_failure(        codex_command(codex_home.path())?            .args(["plugin", "list"])            .assert(),        source.path(),        "marketplace root does not contain a supported manifest",    );    Ok(())}#[tokio::test]async fn plugin_list_ignores_implicit_system_marketplace_roots_without_manifests() -> Result<()> {    let (codex_home, source, cache_home) = setup_local_marketplace_with_implicit_system_roots()?;    codex_command(codex_home.path())?        .env("XDG_CACHE_HOME", cache_home.path())        .args(["plugin", "list"])        .assert()        .success()        .stdout(contains("Marketplace `debug`"))        .stdout(contains(            source                .path()                .join(".agents")                .join("plugins")                .join("marketplace.json")                .display()                .to_string(),        ))        .stderr(            predicates::str::contains("failed to load configured marketplace snapshot(s):").not(),        );    Ok(())}#[tokio::test]async fn plugin_list_fails_for_custom_marketplace_under_system_root() -> Result<()> {    let (codex_home, custom_root) = setup_custom_marketplace_under_implicit_system_root()?;    codex_command(codex_home.path())?        .args(["plugin", "list"])        .assert()        .failure()        .stderr(contains(            "failed to load configured marketplace snapshot(s):",        ))        .stderr(contains("`custom-marketplace`"))        .stderr(contains(custom_root.display().to_string()))        .stderr(contains(            "marketplace root does not contain a supported manifest",        ));    Ok(())}#[tokio::test]async fn plugin_list_hides_version_for_cached_but_unconfigured_plugin() -> Result<()> {    let (codex_home, _source) = setup_local_marketplace()?;    codex_command(codex_home.path())?        .args(["plugin", "add", "sample@debug"])        .assert()        .success();    remove_installed_plugin_config(codex_home.path(), "sample@debug")?;    codex_command(codex_home.path())?        .args(["plugin", "list"])        .assert()        .success()        .stdout(contains("sample@debug"))        .stdout(contains("not installed"))        .stdout(predicates::str::contains("1.2.3").not());    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_json_prints_install_outcome() -> Result<()> {    let (codex_home, _source) = setup_local_marketplace()?;    let assert = codex_command(codex_home.path())?        .args(["plugin", "add", "sample@debug", "--json"])        .assert()        .success();    let stdout = assert.get_output().stdout.as_slice();    let actual: serde_json::Value = serde_json::from_slice(stdout)?;    let installed_path = codex_home.path().join("plugins/cache/debug/sample/1.2.3");    let normalized_installed_path = canonicalize_existing_preserving_symlinks(&installed_path)?;    assert_eq!(        actual,        json!({            "pluginId": "sample@debug",            "name": "sample",            "marketplaceName": "debug",            "version": "1.2.3",            "installedPath": normalized_installed_path.display().to_string(),            "authPolicy": "ON_INSTALL",        })    );    Ok(())}#[tokio::test]async fn plugin_remove_json_prints_remove_outcome() -> Result<()> {    let (codex_home, _source) = setup_local_marketplace()?;    codex_command(codex_home.path())?        .args(["plugin", "add", "sample@debug"])        .assert()        .success();    let assert = codex_command(codex_home.path())?        .args([            "plugin",            "remove",            "sample",            "--marketplace",            "debug",            "--json",        ])        .assert()        .success();    let stdout = assert.get_output().stdout.as_slice();    let actual: serde_json::Value = serde_json::from_slice(stdout)?;    assert_eq!(        actual,        json!({            "pluginId": "sample@debug",            "name": "sample",            "marketplaceName": "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()?;    assert_configured_marketplace_snapshot_failure(        codex_command(codex_home.path())?            .args(["plugin", "add", "sample@debug"])            .assert(),        source.path(),        "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/1.2.3/.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_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/1.2.3/.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(())}