Codex Handbook
app-server/tests/suite/v2/plugin_list.rs 4130 lines
use std::time::Duration;use anyhow::Result;use anyhow::bail;use app_test_support::ChatGptAuthFixture;use app_test_support::TestAppServer;use app_test_support::to_response;use app_test_support::write_chatgpt_auth;use codex_app_server_protocol::JSONRPCResponse;use codex_app_server_protocol::PluginAuthPolicy;use codex_app_server_protocol::PluginInstallPolicy;use codex_app_server_protocol::PluginInstalledParams;use codex_app_server_protocol::PluginInstalledResponse;use codex_app_server_protocol::PluginListMarketplaceKind;use codex_app_server_protocol::PluginListParams;use codex_app_server_protocol::PluginListResponse;use codex_app_server_protocol::PluginMarketplaceEntry;use codex_app_server_protocol::PluginShareDiscoverability;use codex_app_server_protocol::PluginSource;use codex_app_server_protocol::PluginSummary;use codex_app_server_protocol::RequestId;use codex_config::types::AuthCredentialsStoreMode;use codex_core::config::set_project_trust_level;use codex_login::AuthKeyringBackendKind;use codex_login::login_with_api_key;use codex_protocol::config_types::TrustLevel;use codex_utils_absolute_path::AbsolutePathBuf;use flate2::Compression;use flate2::write::GzEncoder;use pretty_assertions::assert_eq;use tempfile::TempDir;use tokio::time::timeout;use wiremock::Mock;use wiremock::MockServer;use wiremock::ResponseTemplate;use wiremock::matchers::header;use wiremock::matchers::method;use wiremock::matchers::path;use wiremock::matchers::query_param;use wiremock::matchers::query_param_is_missing;const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567";const TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS: &str =    "CODEX_TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS";const ALTERNATE_MARKETPLACE_RELATIVE_PATH: &str = ".claude-plugin/marketplace.json";const ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH: &str = ".claude-plugin/plugin.json";fn write_plugins_enabled_config(codex_home: &std::path::Path) -> std::io::Result<()> {    std::fs::write(        codex_home.join("config.toml"),        r#"[features]plugins = true"#,    )}fn write_plugins_enabled_config_with_base_url(    codex_home: &std::path::Path,    base_url: &str,) -> std::io::Result<()> {    std::fs::write(        codex_home.join("config.toml"),        format!(            r#"chatgpt_base_url = "{base_url}"[features]plugins = true"#,        ),    )}#[tokio::test]async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Result<()> {    let codex_home = TempDir::new()?;    let repo_root = TempDir::new()?;    std::fs::create_dir_all(repo_root.path().join(".git"))?;    std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;    write_plugins_enabled_config(codex_home.path())?;    let marketplace_path =        AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;    std::fs::write(marketplace_path.as_path(), "{not json")?;    let home = codex_home.path().to_string_lossy().into_owned();    let mut mcp = TestAppServer::new_with_env(        codex_home.path(),        &[            ("HOME", Some(home.as_str())),            ("USERPROFILE", Some(home.as_str())),        ],    )    .await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert!(        response            .marketplaces            .iter()            .all(|marketplace| { marketplace.path.as_ref() != Some(&marketplace_path) }),        "invalid marketplace should be skipped"    );    assert_eq!(response.marketplace_load_errors.len(), 1);    assert_eq!(        response.marketplace_load_errors[0].marketplace_path,        marketplace_path    );    assert!(        response.marketplace_load_errors[0]            .message            .contains("invalid marketplace file"),        "unexpected error: {:?}",        response.marketplace_load_errors    );    Ok(())}#[tokio::test]async fn plugin_installed_includes_installed_plugins_and_explicit_install_suggestions() -> Result<()>{    let codex_home = TempDir::new()?;    write_openai_curated_marketplace(        codex_home.path(),        &["linear", "computer-use", "not-mentioned"],    )?;    write_installed_plugin(&codex_home, "openai-curated", "linear")?;    std::fs::write(        codex_home.path().join("config.toml"),        r#"[features]plugins = true[plugins."linear@openai-curated"]enabled = true"#,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_installed_request(PluginInstalledParams {            cwds: None,            install_suggestion_plugin_names: Some(vec!["computer-use".to_string()]),        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginInstalledResponse = to_response(response)?;    assert_eq!(response.marketplaces.len(), 1);    assert_eq!(response.marketplaces[0].name, "openai-curated");    assert_eq!(        response.marketplaces[0]            .plugins            .iter()            .map(|plugin| (plugin.id.clone(), plugin.installed, plugin.enabled))            .collect::<Vec<_>>(),        vec![            ("linear@openai-curated".to_string(), true, true),            ("computer-use@openai-curated".to_string(), false, false),        ]    );    assert_eq!(response.marketplace_load_errors, Vec::new());    Ok(())}#[tokio::test]async fn plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_enabled() -> Result<()>{    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_openai_curated_marketplace(codex_home.path(), &["linear", "calendar"])?;    write_installed_plugin(&codex_home, "openai-curated", "linear")?;    write_installed_plugin(&codex_home, "openai-curated", "calendar")?;    std::fs::write(        codex_home.path().join("config.toml"),        format!(            r#"chatgpt_base_url = "{}/backend-api/"[features]plugins = trueremote_plugin = trueplugin_sharing = false[plugins."linear@openai-curated"]enabled = true[plugins."calendar@openai-curated"]enabled = true"#,            server.uri()        ),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let mut global_installed_body: serde_json::Value = serde_json::from_str(        &remote_installed_plugin_body("", "1.2.3", /*enabled*/ true),    )?;    let mut remote_only = global_installed_body["plugins"][0].clone();    remote_only["id"] = serde_json::json!("plugins~Plugin_11111111111111111111111111111111");    remote_only["name"] = serde_json::json!("remote-only");    remote_only["release"]["display_name"] = serde_json::json!("Remote Only");    global_installed_body["plugins"]        .as_array_mut()        .expect("installed plugins should be an array")        .push(remote_only);    let global_installed_body = serde_json::to_string(&global_installed_body)?;    mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await;    mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())        .await;    mount_empty_user_installed_plugins(&server).await;    let mut app_server = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, app_server.initialize()).await??;    let request_id = app_server        .send_plugin_installed_request(PluginInstalledParams {            cwds: None,            install_suggestion_plugin_names: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        app_server.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginInstalledResponse = to_response(response)?;    let local_marketplace = response        .marketplaces        .iter()        .find(|marketplace| marketplace.name == "openai-curated")        .expect("expected openai-curated marketplace entry");    assert_eq!(        local_marketplace            .plugins            .iter()            .map(|plugin| plugin.id.clone())            .collect::<Vec<_>>(),        vec!["calendar@openai-curated".to_string()]    );    let remote_marketplace = response        .marketplaces        .iter()        .find(|marketplace| marketplace.name == "openai-curated-remote")        .expect("expected openai-curated-remote marketplace entry");    assert_eq!(        remote_marketplace            .plugins            .iter()            .map(|plugin| plugin.id.clone())            .collect::<Vec<_>>(),        vec![            "linear@openai-curated-remote".to_string(),            "remote-only@openai-curated-remote".to_string(),        ]    );    assert_eq!(response.marketplace_load_errors, Vec::new());    Ok(())}#[tokio::test]async fn plugin_installed_ignores_local_cache_without_catalog() -> Result<()> {    let codex_home = TempDir::new()?;    write_installed_plugin(&codex_home, "openai-curated", "linear")?;    std::fs::write(        codex_home.path().join("config.toml"),        r#"[features]plugins = true[plugins."linear@openai-curated"]enabled = true"#,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_installed_request(PluginInstalledParams {            cwds: None,            install_suggestion_plugin_names: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginInstalledResponse = to_response(response)?;    assert_eq!(response.marketplaces, Vec::new());    assert_eq!(response.marketplace_load_errors, Vec::new());    Ok(())}#[tokio::test]async fn plugin_list_rejects_relative_cwds() -> Result<()> {    let codex_home = TempDir::new()?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_raw_request(            "plugin/list",            Some(serde_json::json!({                "cwds": ["relative-root"],            })),        )        .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.contains("Invalid request"));    Ok(())}#[tokio::test]async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_load() -> Result<()>{    let codex_home = TempDir::new()?;    let valid_repo_root = TempDir::new()?;    let invalid_repo_root = TempDir::new()?;    std::fs::create_dir_all(valid_repo_root.path().join(".git"))?;    std::fs::create_dir_all(valid_repo_root.path().join(".agents/plugins"))?;    std::fs::create_dir_all(        valid_repo_root            .path()            .join("plugins/valid-plugin/.codex-plugin"),    )?;    std::fs::create_dir_all(invalid_repo_root.path().join(".git"))?;    std::fs::create_dir_all(invalid_repo_root.path().join(".agents/plugins"))?;    write_plugins_enabled_config(codex_home.path())?;    let valid_marketplace_path = AbsolutePathBuf::try_from(        valid_repo_root            .path()            .join(".agents/plugins/marketplace.json"),    )?;    let invalid_marketplace_path = AbsolutePathBuf::try_from(        invalid_repo_root            .path()            .join(".agents/plugins/marketplace.json"),    )?;    let valid_plugin_path =        AbsolutePathBuf::try_from(valid_repo_root.path().join("plugins/valid-plugin"))?;    std::fs::write(        valid_marketplace_path.as_path(),        r#"{  "name": "valid-marketplace",  "plugins": [    {      "name": "valid-plugin",      "source": {        "source": "local",        "path": "./plugins/valid-plugin"      }    }  ]}"#,    )?;    std::fs::write(        valid_repo_root            .path()            .join("plugins/valid-plugin/.codex-plugin/plugin.json"),        r#"{"name":"valid-plugin","keywords":["api-key","developer tools"]}"#,    )?;    std::fs::write(invalid_marketplace_path.as_path(), "{not json")?;    let home = codex_home.path().to_string_lossy().into_owned();    let mut mcp = TestAppServer::new_with_env(        codex_home.path(),        &[            ("HOME", Some(home.as_str())),            ("USERPROFILE", Some(home.as_str())),        ],    )    .await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: Some(vec![                AbsolutePathBuf::try_from(valid_repo_root.path())?,                AbsolutePathBuf::try_from(invalid_repo_root.path())?,            ]),            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert_eq!(        response.marketplaces,        vec![PluginMarketplaceEntry {            name: "valid-marketplace".to_string(),            path: Some(valid_marketplace_path),            interface: None,            plugins: vec![PluginSummary {                id: "valid-plugin@valid-marketplace".to_string(),                remote_plugin_id: None,                local_version: None,                name: "valid-plugin".to_string(),                share_context: None,                source: PluginSource::Local {                    path: valid_plugin_path,                },                installed: false,                enabled: false,                install_policy: PluginInstallPolicy::Available,                auth_policy: PluginAuthPolicy::OnInstall,                availability: codex_app_server_protocol::PluginAvailability::Available,                interface: None,                keywords: vec!["api-key".to_string(), "developer tools".to_string()],            }],        }]    );    assert_eq!(response.marketplace_load_errors.len(), 1);    assert_eq!(        response.marketplace_load_errors[0].marketplace_path,        invalid_marketplace_path    );    assert!(        response.marketplace_load_errors[0]            .message            .contains("invalid marketplace file"),        "unexpected error: {:?}",        response.marketplace_load_errors    );    assert!(response.featured_plugin_ids.is_empty());    Ok(())}#[tokio::test]async fn plugin_list_returns_empty_when_workspace_codex_plugins_disabled() -> Result<()> {    let codex_home = TempDir::new()?;    let repo_root = TempDir::new()?;    let server = MockServer::start().await;    std::fs::create_dir_all(repo_root.path().join(".git"))?;    std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;    write_plugins_enabled_config_with_base_url(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123")            .plan_type("team"),        AuthCredentialsStoreMode::File,    )?;    std::fs::write(        repo_root.path().join(".agents/plugins/marketplace.json"),        r#"{  "name": "codex-curated",  "plugins": [    {      "name": "demo-plugin",      "source": {        "source": "local",        "path": "./demo-plugin"      }    }  ]}"#,    )?;    Mock::given(method("GET"))        .and(path("/backend-api/accounts/account-123/settings"))        .and(header("authorization", "Bearer chatgpt-token"))        .and(header("chatgpt-account-id", "account-123"))        .respond_with(            ResponseTemplate::new(200)                .set_body_string(r#"{"beta_settings":{"enable_plugins":false}}"#),        )        .mount(&server)        .await;    let home = codex_home.path().to_string_lossy().into_owned();    let mut mcp = TestAppServer::new_without_managed_config_with_env(        codex_home.path(),        &[            ("HOME", Some(home.as_str())),            ("USERPROFILE", Some(home.as_str())),        ],    )    .await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert_eq!(        response,        PluginListResponse {            marketplaces: Vec::new(),            marketplace_load_errors: Vec::new(),            featured_plugin_ids: Vec::new(),        }    );    Ok(())}#[tokio::test]async fn plugin_list_reuses_cached_workspace_codex_plugins_setting() -> Result<()> {    let codex_home = TempDir::new()?;    let repo_root = TempDir::new()?;    let server = MockServer::start().await;    std::fs::create_dir_all(repo_root.path().join(".git"))?;    std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;    std::fs::create_dir_all(repo_root.path().join("demo-plugin/.codex-plugin"))?;    write_plugins_enabled_config_with_base_url(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123")            .plan_type("team"),        AuthCredentialsStoreMode::File,    )?;    std::fs::write(        repo_root.path().join(".agents/plugins/marketplace.json"),        r#"{  "name": "local-marketplace",  "plugins": [    {      "name": "demo-plugin",      "source": {        "source": "local",        "path": "./demo-plugin"      }    }  ]}"#,    )?;    std::fs::write(        repo_root            .path()            .join("demo-plugin/.codex-plugin/plugin.json"),        r#"{"name":"demo-plugin"}"#,    )?;    Mock::given(method("GET"))        .and(path("/backend-api/accounts/account-123/settings"))        .and(header("authorization", "Bearer chatgpt-token"))        .and(header("chatgpt-account-id", "account-123"))        .respond_with(            ResponseTemplate::new(200)                .set_body_string(r#"{"beta_settings":{"enable_plugins":true}}"#),        )        .mount(&server)        .await;    let home = codex_home.path().to_string_lossy().into_owned();    let mut mcp = TestAppServer::new_without_managed_config_with_env(        codex_home.path(),        &[            ("HOME", Some(home.as_str())),            ("USERPROFILE", Some(home.as_str())),        ],    )    .await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    for _ in 0..2 {        let request_id = mcp            .send_plugin_list_request(PluginListParams {                cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),                marketplace_kinds: None,            })            .await?;        let response: JSONRPCResponse = timeout(            DEFAULT_TIMEOUT,            mcp.read_stream_until_response_message(RequestId::Integer(request_id)),        )        .await??;        let response: PluginListResponse = to_response(response)?;        assert_eq!(response.marketplaces.len(), 1);        assert_eq!(response.marketplaces[0].name, "local-marketplace");    }    wait_for_workspace_settings_request_count(&server, /*expected_count*/ 1).await?;    Ok(())}#[tokio::test]async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverable_plugins()-> Result<()> {    let codex_home = TempDir::new()?;    let repo_root = TempDir::new()?;    let valid_plugin_root = repo_root.path().join("plugins/valid-plugin");    std::fs::create_dir_all(repo_root.path().join(".git"))?;    std::fs::create_dir_all(        repo_root            .path()            .join(ALTERNATE_MARKETPLACE_RELATIVE_PATH)            .parent()            .unwrap(),    )?;    std::fs::create_dir_all(        valid_plugin_root            .join(ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH)            .parent()            .unwrap(),    )?;    write_plugins_enabled_config(codex_home.path())?;    let marketplace_path =        AbsolutePathBuf::try_from(repo_root.path().join(ALTERNATE_MARKETPLACE_RELATIVE_PATH))?;    let valid_plugin_path = AbsolutePathBuf::try_from(valid_plugin_root.clone())?;    std::fs::write(        marketplace_path.as_path(),        r#"{  "name": "alternate-marketplace",  "plugins": [    {      "name": "valid-plugin",      "source": "./plugins/valid-plugin"    },    {      "name": "missing-plugin",      "source": "./plugins/missing-plugin"    }  ]}"#,    )?;    std::fs::write(        valid_plugin_root.join(ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH),        r#"{  "name": "valid-plugin",  "interface": {    "displayName": "Valid Plugin"  }}"#,    )?;    let home = codex_home.path().to_string_lossy().into_owned();    let mut mcp = TestAppServer::new_with_env(        codex_home.path(),        &[            ("HOME", Some(home.as_str())),            ("USERPROFILE", Some(home.as_str())),        ],    )    .await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert_eq!(        response.marketplaces,        vec![PluginMarketplaceEntry {            name: "alternate-marketplace".to_string(),            path: Some(marketplace_path),            interface: None,            plugins: vec![                PluginSummary {                    id: "valid-plugin@alternate-marketplace".to_string(),                    remote_plugin_id: None,                    local_version: None,                    name: "valid-plugin".to_string(),                    share_context: None,                    source: PluginSource::Local {                        path: valid_plugin_path,                    },                    installed: false,                    enabled: false,                    install_policy: PluginInstallPolicy::Available,                    auth_policy: PluginAuthPolicy::OnInstall,                    availability: codex_app_server_protocol::PluginAvailability::Available,                    interface: Some(codex_app_server_protocol::PluginInterface {                        display_name: Some("Valid Plugin".to_string()),                        short_description: None,                        long_description: None,                        developer_name: None,                        category: None,                        capabilities: Vec::new(),                        website_url: None,                        privacy_policy_url: None,                        terms_of_service_url: None,                        default_prompt: None,                        brand_color: None,                        composer_icon: None,                        composer_icon_url: None,                        logo: None,                        logo_url: None,                        screenshots: Vec::new(),                        screenshot_urls: Vec::new(),                    }),                    keywords: Vec::new(),                },                PluginSummary {                    id: "missing-plugin@alternate-marketplace".to_string(),                    remote_plugin_id: None,                    local_version: None,                    name: "missing-plugin".to_string(),                    share_context: None,                    source: PluginSource::Local {                        path: AbsolutePathBuf::try_from(                            repo_root.path().join("plugins/missing-plugin"),                        )?,                    },                    installed: false,                    enabled: false,                    install_policy: PluginInstallPolicy::Available,                    auth_policy: PluginAuthPolicy::OnInstall,                    availability: codex_app_server_protocol::PluginAvailability::Available,                    interface: None,                    keywords: Vec::new(),                },            ],        }]    );    assert!(response.marketplace_load_errors.is_empty());    Ok(())}#[tokio::test]async fn plugin_list_accepts_omitted_cwds() -> Result<()> {    let codex_home = TempDir::new()?;    std::fs::create_dir_all(codex_home.path().join(".agents/plugins"))?;    write_plugins_enabled_config(codex_home.path())?;    std::fs::write(        codex_home.path().join(".agents/plugins/marketplace.json"),        r#"{  "name": "codex-curated",  "plugins": [    {      "name": "home-plugin",      "source": {        "source": "local",        "path": "./home-plugin"      }    }  ]}"#,    )?;    let home = codex_home.path().to_string_lossy().into_owned();    let mut mcp = TestAppServer::new_with_env(        codex_home.path(),        &[            ("HOME", Some(home.as_str())),            ("USERPROFILE", Some(home.as_str())),        ],    )    .await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let _: PluginListResponse = to_response(response)?;    Ok(())}#[tokio::test]async fn plugin_list_returns_share_context_for_shared_local_plugin() -> Result<()> {    let codex_home = TempDir::new()?;    let repo_root = TempDir::new()?;    let plugin_root = repo_root.path().join("plugins/demo-plugin");    std::fs::create_dir_all(repo_root.path().join(".git"))?;    std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;    std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;    write_plugins_enabled_config(codex_home.path())?;    std::fs::write(        repo_root.path().join(".agents/plugins/marketplace.json"),        r#"{  "name": "codex-curated",  "plugins": [    {      "name": "demo-plugin",      "source": {        "source": "local",        "path": "./plugins/demo-plugin"      }    }  ]}"#,    )?;    std::fs::write(        plugin_root.join(".codex-plugin/plugin.json"),        r#"{"name":"demo-plugin","version":"1.2.3"}"#,    )?;    write_plugin_share_local_path_mapping(        codex_home.path(),        "plugins_123",        &AbsolutePathBuf::try_from(plugin_root)?,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    let plugin = response        .marketplaces        .iter()        .flat_map(|marketplace| marketplace.plugins.iter())        .find(|plugin| plugin.name == "demo-plugin")        .expect("expected demo-plugin entry");    assert_eq!(plugin.remote_plugin_id, None);    assert_eq!(plugin.local_version.as_deref(), Some("1.2.3"));    let share_context = plugin        .share_context        .as_ref()        .expect("expected share context");    assert_eq!(share_context.remote_plugin_id, "plugins_123");    assert_eq!(share_context.remote_version, None);    assert_eq!(share_context.discoverability, None);    assert_eq!(share_context.share_url, None);    assert_eq!(share_context.creator_account_user_id, None);    assert_eq!(share_context.creator_name, None);    assert_eq!(share_context.share_principals, None);    Ok(())}#[tokio::test]async fn plugin_list_includes_install_and_enabled_state_from_config() -> Result<()> {    let codex_home = TempDir::new()?;    let repo_root = TempDir::new()?;    std::fs::create_dir_all(repo_root.path().join(".git"))?;    std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;    write_installed_plugin(&codex_home, "codex-curated", "enabled-plugin")?;    write_installed_plugin(&codex_home, "codex-curated", "disabled-plugin")?;    std::fs::write(        repo_root.path().join(".agents/plugins/marketplace.json"),        r#"{  "name": "codex-curated",  "interface": {    "displayName": "ChatGPT Official"  },  "plugins": [    {      "name": "enabled-plugin",      "source": {        "source": "local",        "path": "./enabled-plugin"      }    },    {      "name": "disabled-plugin",      "source": {        "source": "local",        "path": "./disabled-plugin"      }    },    {      "name": "uninstalled-plugin",      "source": {        "source": "local",        "path": "./uninstalled-plugin"      }    }  ]}"#,    )?;    std::fs::write(        codex_home.path().join("config.toml"),        r#"[features]plugins = true[plugins."enabled-plugin@codex-curated"]enabled = true[plugins."disabled-plugin@codex-curated"]enabled = false"#,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    let marketplace = response        .marketplaces        .into_iter()        .find(|marketplace| {            marketplace.path.as_ref()                == Some(                    &AbsolutePathBuf::try_from(                        repo_root.path().join(".agents/plugins/marketplace.json"),                    )                    .expect("absolute marketplace path"),                )        })        .expect("expected repo marketplace entry");    assert_eq!(marketplace.name, "codex-curated");    assert_eq!(        marketplace            .interface            .as_ref()            .and_then(|interface| interface.display_name.as_deref()),        Some("ChatGPT Official")    );    assert_eq!(marketplace.plugins.len(), 3);    assert_eq!(marketplace.plugins[0].id, "enabled-plugin@codex-curated");    assert_eq!(marketplace.plugins[0].name, "enabled-plugin");    assert_eq!(marketplace.plugins[0].installed, true);    assert_eq!(marketplace.plugins[0].enabled, true);    assert_eq!(        marketplace.plugins[0].install_policy,        PluginInstallPolicy::Available    );    assert_eq!(        marketplace.plugins[0].auth_policy,        PluginAuthPolicy::OnInstall    );    assert_eq!(marketplace.plugins[1].id, "disabled-plugin@codex-curated");    assert_eq!(marketplace.plugins[1].name, "disabled-plugin");    assert_eq!(marketplace.plugins[1].installed, true);    assert_eq!(marketplace.plugins[1].enabled, false);    assert_eq!(        marketplace.plugins[1].install_policy,        PluginInstallPolicy::Available    );    assert_eq!(        marketplace.plugins[1].auth_policy,        PluginAuthPolicy::OnInstall    );    assert_eq!(        marketplace.plugins[2].id,        "uninstalled-plugin@codex-curated"    );    assert_eq!(marketplace.plugins[2].name, "uninstalled-plugin");    assert_eq!(marketplace.plugins[2].installed, false);    assert_eq!(marketplace.plugins[2].enabled, false);    assert_eq!(        marketplace.plugins[2].install_policy,        PluginInstallPolicy::Available    );    assert_eq!(        marketplace.plugins[2].auth_policy,        PluginAuthPolicy::OnInstall    );    Ok(())}#[tokio::test]async fn plugin_list_uses_home_config_for_enabled_state() -> Result<()> {    let codex_home = TempDir::new()?;    std::fs::create_dir_all(codex_home.path().join(".agents/plugins"))?;    write_installed_plugin(&codex_home, "codex-curated", "shared-plugin")?;    std::fs::write(        codex_home.path().join(".agents/plugins/marketplace.json"),        r#"{  "name": "codex-curated",  "plugins": [    {      "name": "shared-plugin",      "source": {        "source": "local",        "path": "./shared-plugin"      }    }  ]}"#,    )?;    std::fs::write(        codex_home.path().join("config.toml"),        r#"[features]plugins = true[plugins."shared-plugin@codex-curated"]enabled = true"#,    )?;    let workspace_enabled = TempDir::new()?;    std::fs::create_dir_all(workspace_enabled.path().join(".git"))?;    std::fs::create_dir_all(workspace_enabled.path().join(".agents/plugins"))?;    std::fs::write(        workspace_enabled            .path()            .join(".agents/plugins/marketplace.json"),        r#"{  "name": "codex-curated",  "plugins": [    {      "name": "shared-plugin",      "source": {        "source": "local",        "path": "./shared-plugin"      }    }  ]}"#,    )?;    std::fs::create_dir_all(workspace_enabled.path().join(".codex"))?;    std::fs::write(        workspace_enabled.path().join(".codex/config.toml"),        r#"[plugins."shared-plugin@codex-curated"]enabled = false"#,    )?;    set_project_trust_level(        codex_home.path(),        workspace_enabled.path(),        TrustLevel::Trusted,    )?;    let workspace_default = TempDir::new()?;    let home = codex_home.path().to_string_lossy().into_owned();    let mut mcp = TestAppServer::new_with_env(        codex_home.path(),        &[            ("HOME", Some(home.as_str())),            ("USERPROFILE", Some(home.as_str())),        ],    )    .await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: Some(vec![                AbsolutePathBuf::try_from(workspace_enabled.path())?,                AbsolutePathBuf::try_from(workspace_default.path())?,            ]),            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    let shared_plugin = response        .marketplaces        .iter()        .flat_map(|marketplace| marketplace.plugins.iter())        .find(|plugin| plugin.name == "shared-plugin")        .expect("expected shared-plugin entry");    assert_eq!(shared_plugin.id, "shared-plugin@codex-curated");    assert_eq!(shared_plugin.installed, true);    assert_eq!(shared_plugin.enabled, true);    Ok(())}#[tokio::test]async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Result<()> {    let codex_home = TempDir::new()?;    let repo_root = TempDir::new()?;    let plugin_root = repo_root.path().join("plugins/demo-plugin");    std::fs::create_dir_all(repo_root.path().join(".git"))?;    std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;    std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;    write_plugins_enabled_config(codex_home.path())?;    std::fs::write(        repo_root.path().join(".agents/plugins/marketplace.json"),        r#"{  "name": "codex-curated",  "plugins": [    {      "name": "demo-plugin",      "source": {        "source": "local",        "path": "./plugins/demo-plugin"      },      "policy": {        "installation": "AVAILABLE",        "authentication": "ON_INSTALL"      },      "category": "Design"    }  ]}"#,    )?;    std::fs::write(        plugin_root.join(".codex-plugin/plugin.json"),        r##"{  "name": "demo-plugin",  "interface": {    "displayName": "Plugin Display Name",    "shortDescription": "Short description for subtitle",    "longDescription": "Long description for details page",    "developerName": "OpenAI",    "category": "Productivity",    "capabilities": ["Interactive", "Write"],    "websiteURL": "https://openai.com/",    "privacyPolicyURL": "https://openai.com/policies/row-privacy-policy/",    "termsOfServiceURL": "https://openai.com/policies/row-terms-of-use/",    "defaultPrompt": [      "Starter prompt for trying a plugin",      "Find my next action"    ],    "brandColor": "#3B82F6",    "composerIcon": "./assets/icon.png",    "logo": "./assets/logo.png",    "screenshots": ["./assets/screenshot1.png", "./assets/screenshot2.png"]  }}"##,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    let plugin = response        .marketplaces        .iter()        .flat_map(|marketplace| marketplace.plugins.iter())        .find(|plugin| plugin.name == "demo-plugin")        .expect("expected demo-plugin entry");    assert_eq!(plugin.id, "demo-plugin@codex-curated");    assert_eq!(plugin.installed, false);    assert_eq!(plugin.enabled, false);    assert_eq!(plugin.install_policy, PluginInstallPolicy::Available);    assert_eq!(plugin.auth_policy, PluginAuthPolicy::OnInstall);    let interface = plugin        .interface        .as_ref()        .expect("expected plugin interface");    assert_eq!(        interface.display_name.as_deref(),        Some("Plugin Display Name")    );    assert_eq!(interface.category.as_deref(), Some("Design"));    assert_eq!(        interface.website_url.as_deref(),        Some("https://openai.com/")    );    assert_eq!(        interface.privacy_policy_url.as_deref(),        Some("https://openai.com/policies/row-privacy-policy/")    );    assert_eq!(        interface.terms_of_service_url.as_deref(),        Some("https://openai.com/policies/row-terms-of-use/")    );    assert_eq!(        interface.default_prompt,        Some(vec![            "Starter prompt for trying a plugin".to_string(),            "Find my next action".to_string()        ])    );    assert_eq!(        interface.composer_icon,        Some(AbsolutePathBuf::try_from(            plugin_root.join("assets/icon.png")        )?)    );    assert_eq!(        interface.logo,        Some(AbsolutePathBuf::try_from(            plugin_root.join("assets/logo.png")        )?)    );    assert_eq!(        interface.screenshots,        vec![            AbsolutePathBuf::try_from(plugin_root.join("assets/screenshot1.png"))?,            AbsolutePathBuf::try_from(plugin_root.join("assets/screenshot2.png"))?,        ]    );    Ok(())}#[tokio::test]async fn plugin_list_accepts_legacy_string_default_prompt() -> Result<()> {    let codex_home = TempDir::new()?;    let repo_root = TempDir::new()?;    let plugin_root = repo_root.path().join("plugins/demo-plugin");    std::fs::create_dir_all(repo_root.path().join(".git"))?;    std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;    std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;    write_plugins_enabled_config(codex_home.path())?;    std::fs::write(        repo_root.path().join(".agents/plugins/marketplace.json"),        r#"{  "name": "codex-curated",  "plugins": [    {      "name": "demo-plugin",      "source": {        "source": "local",        "path": "./plugins/demo-plugin"      }    }  ]}"#,    )?;    std::fs::write(        plugin_root.join(".codex-plugin/plugin.json"),        r##"{  "name": "demo-plugin",  "interface": {    "defaultPrompt": "Starter prompt for trying a plugin"  }}"##,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    let plugin = response        .marketplaces        .iter()        .flat_map(|marketplace| marketplace.plugins.iter())        .find(|plugin| plugin.name == "demo-plugin")        .expect("expected demo-plugin entry");    assert_eq!(        plugin            .interface            .as_ref()            .and_then(|interface| interface.default_prompt.clone()),        Some(vec!["Starter prompt for trying a plugin".to_string()])    );    Ok(())}#[tokio::test]async fn plugin_list_returns_installed_git_source_interface_from_cache() -> Result<()> {    let codex_home = TempDir::new()?;    let repo_root = TempDir::new()?;    let missing_remote_repo = repo_root.path().join("missing-remote-plugin-repo");    let missing_remote_repo_url = url::Url::from_directory_path(&missing_remote_repo)        .unwrap()        .to_string();    std::fs::create_dir_all(repo_root.path().join(".git"))?;    std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;    std::fs::write(        repo_root.path().join(".agents/plugins/marketplace.json"),        format!(            r#"{{  "name": "debug",  "plugins": [    {{      "name": "toolkit",      "source": {{        "source": "git-subdir",        "url": "{missing_remote_repo_url}",        "path": "plugins/toolkit"      }},      "category": "Developer Tools"    }}  ]}}"#        ),    )?;    let cached_plugin_root = codex_home.path().join("plugins/cache/debug/toolkit/local");    std::fs::create_dir_all(cached_plugin_root.join(".codex-plugin"))?;    std::fs::write(        cached_plugin_root.join(".codex-plugin/plugin.json"),        r##"{  "name": "toolkit",  "interface": {    "displayName": "Toolkit",    "shortDescription": "Search cached data",    "category": "Cached Category",    "brandColor": "#3B82F6",    "composerIcon": "./assets/icon.png",    "logo": "./assets/logo.png"  }}"##,    )?;    std::fs::write(        codex_home.path().join("config.toml"),        r#"[features]plugins = true[plugins."toolkit@debug"]enabled = true"#,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    let plugin = response        .marketplaces        .iter()        .flat_map(|marketplace| marketplace.plugins.iter())        .find(|plugin| plugin.name == "toolkit")        .expect("expected toolkit entry");    assert_eq!(plugin.id, "toolkit@debug");    assert_eq!(plugin.installed, true);    assert_eq!(plugin.enabled, true);    assert_eq!(        plugin.source,        PluginSource::Git {            url: missing_remote_repo_url,            path: Some("plugins/toolkit".to_string()),            ref_name: None,            sha: None,        }    );    let interface = plugin        .interface        .as_ref()        .expect("expected cached plugin interface");    assert_eq!(interface.display_name.as_deref(), Some("Toolkit"));    assert_eq!(        interface.short_description.as_deref(),        Some("Search cached data")    );    assert_eq!(interface.category.as_deref(), Some("Developer Tools"));    assert_eq!(interface.brand_color.as_deref(), Some("#3B82F6"));    let canonical_cached_plugin_root = std::fs::canonicalize(&cached_plugin_root)?;    assert_eq!(        interface.composer_icon,        Some(AbsolutePathBuf::try_from(            canonical_cached_plugin_root.join("assets/icon.png")        )?)    );    assert_eq!(        interface.logo,        Some(AbsolutePathBuf::try_from(            canonical_cached_plugin_root.join("assets/logo.png")        )?)    );    Ok(())}#[tokio::test]async fn app_server_startup_sync_downloads_remote_installed_plugin_bundles() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_remote_plugin_catalog_config(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let bundle_url = mount_remote_plugin_bundle(        &server,        "linear",        remote_plugin_bundle_tar_gz_bytes("linear")?,    )    .await;    let remote_app_manifest = serde_json::json!({        "apps": {            "linear-remote": {                "id": "remote-linear-app"            }        }    });    let global_installed_body = remote_installed_plugin_body_with_app_manifest(        &bundle_url,        "1.2.3",        /*enabled*/ true,        remote_app_manifest.clone(),    );    mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await;    mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())        .await;    mount_empty_user_installed_plugins(&server).await;    let installed_path = codex_home        .path()        .join("plugins/cache/openai-curated-remote/linear/1.2.3");    let mut mcp = TestAppServer::new_with_env_and_plugin_startup_tasks(        codex_home.path(),        &[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],    )    .await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    wait_for_path_exists(&installed_path.join(".codex-plugin/plugin.json")).await?;    let installed_plugin_manifest: serde_json::Value = serde_json::from_str(        &std::fs::read_to_string(installed_path.join(".codex-plugin/plugin.json"))?,    )?;    assert_eq!(        installed_plugin_manifest["version"],        serde_json::json!("1.2.3")    );    let installed_app_manifest: serde_json::Value =        serde_json::from_str(&std::fs::read_to_string(installed_path.join(".app.json"))?)?;    assert_eq!(installed_app_manifest, remote_app_manifest);    assert!(installed_path.join("skills/plan-work/SKILL.md").is_file());    let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;    assert!(!config.contains("linear@openai-curated-remote"));    Ok(())}#[tokio::test]async fn plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_remote_plugin_catalog_config(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    write_installed_plugin_with_version(&codex_home, "openai-curated-remote", "linear", "1.0.0")?;    write_installed_plugin_with_version(&codex_home, "openai-curated-remote", "stale", "1.0.0")?;    let bundle_url = mount_remote_plugin_bundle(        &server,        "linear",        remote_plugin_bundle_tar_gz_bytes("linear")?,    )    .await;    let remote_app_manifest = serde_json::json!({        "apps": {            "linear-remote": {                "id": "remote-linear-app"            }        }    });    let global_installed_body = remote_installed_plugin_body_with_app_manifest(        &bundle_url,        "1.2.3",        /*enabled*/ true,        remote_app_manifest.clone(),    );    mount_remote_plugin_list(&server, "GLOBAL", &global_installed_body).await;    mount_remote_plugin_list(&server, "WORKSPACE", empty_remote_installed_plugins_body()).await;    mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await;    mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())        .await;    mount_empty_user_installed_plugins(&server).await;    let old_path = codex_home        .path()        .join("plugins/cache/openai-curated-remote/linear/1.0.0");    let new_path = codex_home        .path()        .join("plugins/cache/openai-curated-remote/linear/1.2.3");    let stale_path = codex_home        .path()        .join("plugins/cache/openai-curated-remote/stale");    let mut mcp = TestAppServer::new_with_env(        codex_home.path(),        &[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],    )    .await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    let remote_marketplace = response        .marketplaces        .into_iter()        .find(|marketplace| marketplace.name == "openai-curated-remote")        .expect("expected openai-curated-remote marketplace entry");    assert_eq!(        remote_marketplace            .plugins            .into_iter()            .map(|plugin| (plugin.id, plugin.installed, plugin.enabled))            .collect::<Vec<_>>(),        vec![("linear@openai-curated-remote".to_string(), true, true)]    );    wait_for_path_exists(&new_path.join(".codex-plugin/plugin.json")).await?;    let installed_plugin_manifest: serde_json::Value = serde_json::from_str(        &std::fs::read_to_string(new_path.join(".codex-plugin/plugin.json"))?,    )?;    assert_eq!(        installed_plugin_manifest["version"],        serde_json::json!("1.2.3")    );    let installed_app_manifest: serde_json::Value =        serde_json::from_str(&std::fs::read_to_string(new_path.join(".app.json"))?)?;    assert_eq!(installed_app_manifest, remote_app_manifest);    wait_for_path_missing(&old_path).await?;    wait_for_path_missing(&stale_path).await?;    let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;    assert!(!config.contains("linear@openai-curated-remote"));    Ok(())}#[tokio::test]async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_remote_plugin_catalog_config(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let global_directory_body = r#"{  "plugins": [    {      "id": "plugins~Plugin_00000000000000000000000000000000",      "name": "linear",      "scope": "GLOBAL",      "installation_policy": "AVAILABLE",      "authentication_policy": "ON_USE",      "status": "ENABLED",      "release": {        "display_name": "Linear",        "description": "Track work in Linear",        "app_ids": [],        "keywords": ["issue-tracking", "project management"],        "interface": {          "short_description": "Plan and track work",          "capabilities": ["Read", "Write"],          "default_prompt": "Use the legacy Linear prompt",          "default_prompts": ["Create a Linear issue", "Review my Linear projects"],          "logo_url": "https://example.com/linear.png",          "screenshot_urls": ["https://example.com/linear-shot.png"]        },        "skills": []      }    }  ],  "pagination": {    "limit": 50,    "next_page_token": null  }}"#;    let empty_page_body = r#"{  "plugins": [],  "pagination": {    "limit": 50,    "next_page_token": null  }}"#;    let global_installed_body = r#"{  "plugins": [    {      "id": "plugins~Plugin_00000000000000000000000000000000",      "name": "linear",      "scope": "GLOBAL",      "installation_policy": "AVAILABLE",      "authentication_policy": "ON_USE",      "status": "ENABLED",      "release": {        "display_name": "Linear",        "description": "Track work in Linear",        "app_ids": [],        "interface": {          "short_description": "Plan and track work",          "capabilities": ["Read", "Write"],          "logo_url": "https://example.com/linear.png",          "screenshot_urls": ["https://example.com/linear-shot.png"]        },        "skills": []      },      "enabled": true,      "disabled_skill_names": []    }  ],  "pagination": {    "limit": 50,    "next_page_token": null  }}"#;    Mock::given(method("GET"))        .and(path("/backend-api/ps/plugins/list"))        .and(query_param("scope", "GLOBAL"))        .and(query_param("limit", "200"))        .and(header("authorization", "Bearer chatgpt-token"))        .and(header("chatgpt-account-id", "account-123"))        .and(header("oai-product-sku", "codex"))        .respond_with(ResponseTemplate::new(200).set_body_string(global_directory_body))        .mount(&server)        .await;    Mock::given(method("GET"))        .and(path("/backend-api/ps/plugins/list"))        .and(query_param("scope", "WORKSPACE"))        .and(query_param("limit", "200"))        .and(header("authorization", "Bearer chatgpt-token"))        .and(header("chatgpt-account-id", "account-123"))        .and(header("oai-product-sku", "codex"))        .respond_with(ResponseTemplate::new(200).set_body_string(empty_page_body))        .mount(&server)        .await;    Mock::given(method("GET"))        .and(path("/backend-api/ps/plugins/installed"))        .and(query_param("scope", "GLOBAL"))        .and(header("authorization", "Bearer chatgpt-token"))        .and(header("chatgpt-account-id", "account-123"))        .and(header("oai-product-sku", "codex"))        .respond_with(ResponseTemplate::new(200).set_body_string(global_installed_body))        .mount(&server)        .await;    Mock::given(method("GET"))        .and(path("/backend-api/ps/plugins/installed"))        .and(query_param("scope", "WORKSPACE"))        .and(header("authorization", "Bearer chatgpt-token"))        .and(header("chatgpt-account-id", "account-123"))        .and(header("oai-product-sku", "codex"))        .respond_with(ResponseTemplate::new(200).set_body_string(empty_page_body))        .mount(&server)        .await;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    let remote_marketplace = response        .marketplaces        .into_iter()        .find(|marketplace| marketplace.name == "openai-curated-remote")        .expect("expected openai-curated remote marketplace");    assert_eq!(remote_marketplace.path, None);    assert_eq!(        remote_marketplace            .interface            .as_ref()            .and_then(|interface| interface.display_name.as_deref()),        Some("OpenAI Curated Remote")    );    assert_eq!(remote_marketplace.plugins.len(), 1);    assert_eq!(        remote_marketplace.plugins[0].id,        "linear@openai-curated-remote"    );    assert_eq!(        remote_marketplace.plugins[0].remote_plugin_id.as_deref(),        Some("plugins~Plugin_00000000000000000000000000000000")    );    assert_eq!(remote_marketplace.plugins[0].name, "linear");    assert_eq!(remote_marketplace.plugins[0].source, PluginSource::Remote);    assert_eq!(remote_marketplace.plugins[0].installed, true);    assert_eq!(remote_marketplace.plugins[0].enabled, true);    assert_eq!(        remote_marketplace.plugins[0].availability,        codex_app_server_protocol::PluginAvailability::Available    );    assert_eq!(        remote_marketplace.plugins[0]            .interface            .as_ref()            .and_then(|interface| interface.display_name.as_deref()),        Some("Linear")    );    assert_eq!(        remote_marketplace.plugins[0]            .interface            .as_ref()            .and_then(|interface| interface.default_prompt.clone()),        Some(vec![            "Create a Linear issue".to_string(),            "Review my Linear projects".to_string(),        ])    );    assert_eq!(        remote_marketplace.plugins[0].keywords,        vec![            "issue-tracking".to_string(),            "project management".to_string()        ]    );    let cache_files = std::fs::read_dir(codex_home.path().join("cache/remote_plugin_catalog"))?        .map(|entry| entry.map(|entry| entry.path()))        .collect::<Result<Vec<_>, _>>()?;    assert_eq!(cache_files.len(), 1);    let cached_catalog: serde_json::Value =        serde_json::from_slice(&std::fs::read(&cache_files[0])?)?;    assert_eq!(cached_catalog["schema_version"], serde_json::json!(1));    assert_eq!(        cached_catalog["plugins"][0]["release"]["interface"]["default_prompts"],        serde_json::json!(["Create a Linear issue", "Review my Linear projects"])    );    let cached_plugin_ids = cached_catalog["plugins"]        .as_array()        .expect("cached plugins should be an array")        .iter()        .map(|plugin| plugin["id"].as_str().expect("cached plugin id").to_string())        .collect::<Vec<_>>();    assert_eq!(        cached_plugin_ids,        vec!["plugins~Plugin_00000000000000000000000000000000".to_string()]    );    assert_eq!(response.featured_plugin_ids, Vec::<String>::new());    assert!(        !server            .received_requests()            .await            .expect("wiremock should record requests")            .iter()            .any(|request| request                .url                .query_pairs()                .any(|(name, value)| name == "collection" && value == "vertical"))    );    Ok(())}#[tokio::test]async fn plugin_list_uses_cached_global_remote_catalog_and_refreshes_it() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_remote_plugin_catalog_config(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let cached_remote_plugin_id = "plugins~Plugin_00000000000000000000000000000000";    let refreshed_remote_plugin_id = "plugins~Plugin_11111111111111111111111111111111";    let cached_body =        remote_plugin_list_body(cached_remote_plugin_id, "linear", "Linear", "Plan work");    let refreshed_body = remote_plugin_list_body(        refreshed_remote_plugin_id,        "notion",        "Notion",        "Capture notes",    );    mount_remote_plugin_list(&server, "GLOBAL", &cached_body).await;    mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;    mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())        .await;    mount_empty_user_installed_plugins(&server).await;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: None,        })        .await?;    let response: PluginListResponse = to_response(        timeout(            DEFAULT_TIMEOUT,            mcp.read_stream_until_response_message(RequestId::Integer(request_id)),        )        .await??,    )?;    let remote_marketplace = response        .marketplaces        .iter()        .find(|marketplace| marketplace.name == "openai-curated-remote")        .expect("expected warmed remote marketplace");    assert_eq!(        remote_marketplace.plugins[0].id,        "linear@openai-curated-remote"    );    wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 1).await?;    wait_for_cached_remote_catalog_plugin_ids(codex_home.path(), &[cached_remote_plugin_id])        .await?;    server.reset().await;    mount_remote_plugin_list(&server, "GLOBAL", &refreshed_body).await;    mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;    mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())        .await;    mount_empty_user_installed_plugins(&server).await;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: None,        })        .await?;    let response: PluginListResponse = to_response(        timeout(            DEFAULT_TIMEOUT,            mcp.read_stream_until_response_message(RequestId::Integer(request_id)),        )        .await??,    )?;    let remote_marketplace = response        .marketplaces        .iter()        .find(|marketplace| marketplace.name == "openai-curated-remote")        .expect("expected cached remote marketplace");    assert_eq!(        remote_marketplace.plugins[0].id,        "linear@openai-curated-remote"    );    wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 1).await?;    wait_for_cached_remote_catalog_plugin_ids(codex_home.path(), &[refreshed_remote_plugin_id])        .await?;    Ok(())}#[tokio::test]async fn plugin_list_includes_openai_curated_remote_collection_when_requested() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_plugins_enabled_config_with_base_url(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let collection_body = r#"{  "plugins": [    {      "id": "plugins~Plugin_00000000000000000000000000000000",      "name": "linear",      "scope": "GLOBAL",      "installation_policy": "AVAILABLE",      "authentication_policy": "ON_USE",      "status": "ENABLED",      "release": {        "display_name": "Linear",        "description": "Track work in Linear",        "app_ids": [],        "interface": {          "short_description": "Plan and track work",          "capabilities": ["Read", "Write"]        },        "skills": []      }    }  ],  "pagination": {    "limit": 50,    "next_page_token": null  }}"#;    mount_openai_curated_remote_collection_plugin_list(&server, collection_body).await;    mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;    mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())        .await;    mount_empty_user_installed_plugins(&server).await;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]),        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    let remote_marketplace = response        .marketplaces        .into_iter()        .find(|marketplace| marketplace.name == "openai-curated-remote")        .expect("expected openai-curated remote marketplace");    assert_eq!(remote_marketplace.path, None);    assert_eq!(        remote_marketplace            .interface            .as_ref()            .and_then(|interface| interface.display_name.as_deref()),        Some("OpenAI Curated Remote")    );    assert_eq!(remote_marketplace.plugins.len(), 1);    let plugin = &remote_marketplace.plugins[0];    assert_eq!(plugin.id, "linear@openai-curated-remote");    assert_eq!(        plugin.remote_plugin_id.as_deref(),        Some("plugins~Plugin_00000000000000000000000000000000")    );    assert_eq!(plugin.name, "linear");    assert_eq!(plugin.source, PluginSource::Remote);    assert_eq!(plugin.installed, false);    assert_eq!(plugin.enabled, false);    let requests = server        .received_requests()        .await        .expect("wiremock should record requests");    assert!(requests.iter().any(|request| {        request.method == "GET"            && request.url.path().ends_with("/ps/plugins/list")            && request                .url                .query_pairs()                .any(|(name, value)| name == "collection" && value == "vertical")    }));    Ok(())}#[tokio::test]async fn plugin_list_propagates_explicit_openai_curated_remote_collection_errors() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_plugins_enabled_config_with_base_url(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    Mock::given(method("GET"))        .and(path("/backend-api/ps/plugins/list"))        .and(query_param("scope", "GLOBAL"))        .and(query_param("limit", "200"))        .and(query_param("collection", "vertical"))        .and(header("authorization", "Bearer chatgpt-token"))        .and(header("chatgpt-account-id", "account-123"))        .respond_with(ResponseTemplate::new(500).set_body_string("temporary failure"))        .mount(&server)        .await;    mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;    mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())        .await;    mount_empty_user_installed_plugins(&server).await;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]),        })        .await?;    let err = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_error_message(RequestId::Integer(request_id)),    )    .await??;    assert_eq!(err.error.code, -32603);    assert!(        err.error            .message            .contains("list OpenAI Curated remote plugin catalog")    );    Ok(())}#[tokio::test]async fn plugin_list_skips_explicit_openai_curated_remote_collection_for_api_auth() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_plugins_enabled_config_with_base_url(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    login_with_api_key(        codex_home.path(),        "sk-test-key",        AuthCredentialsStoreMode::File,        AuthKeyringBackendKind::default(),    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]),        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert!(response.marketplaces.is_empty());    assert!(response.marketplace_load_errors.is_empty());    wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;    Ok(())}#[tokio::test]async fn plugin_list_includes_api_curated_marketplace_for_api_auth_when_remote_plugin_enabled()-> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_remote_plugin_catalog_config(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_openai_api_curated_marketplace(codex_home.path(), &["api-plugin"])?;    login_with_api_key(        codex_home.path(),        "sk-test-key",        AuthCredentialsStoreMode::File,        AuthKeyringBackendKind::default(),    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    let api_curated_marketplace = response        .marketplaces        .iter()        .find(|marketplace| marketplace.name == "openai-api-curated")        .expect("expected API curated marketplace");    assert_eq!(        api_curated_marketplace            .interface            .as_ref()            .and_then(|interface| interface.display_name.as_deref()),        Some("OpenAI Curated")    );    assert_eq!(api_curated_marketplace.plugins.len(), 1);    assert_eq!(        api_curated_marketplace.plugins[0].id,        "api-plugin@openai-api-curated"    );    assert!(response.marketplace_load_errors.is_empty());    wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;    Ok(())}#[tokio::test]async fn plugin_list_does_not_query_openai_curated_remote_collection_by_default() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_plugins_enabled_config_with_base_url(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert!(        response            .marketplaces            .iter()            .all(|marketplace| marketplace.name != "openai-curated-remote")    );    assert!(        server            .received_requests()            .await            .expect("wiremock should record requests")            .iter()            .all(|request| !request                .url                .query_pairs()                .any(|(name, value)| name == "collection" && value == "vertical"))    );    Ok(())}#[tokio::test]async fn plugin_list_vertical_kind_noops_when_remote_plugin_enabled() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_remote_plugin_catalog_config(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]),        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert!(        response            .marketplaces            .iter()            .all(|marketplace| marketplace.name != "openai-curated-remote")    );    assert!(        server            .received_requests()            .await            .expect("wiremock should record requests")            .iter()            .all(|request| !request                .url                .query_pairs()                .any(|(name, value)| name == "collection" && value == "vertical"))    );    Ok(())}#[tokio::test]async fn plugin_list_does_not_append_global_remote_when_marketplace_kinds_are_explicit()-> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_remote_plugin_catalog_config(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: Some(vec![PluginListMarketplaceKind::Local]),        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert!(        response            .marketplaces            .iter()            .all(|marketplace| marketplace.name != "openai-curated-remote")    );    wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;    Ok(())}#[tokio::test]async fn plugin_installed_includes_remote_shared_with_me_plugins() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    std::fs::write(        codex_home.path().join("config.toml"),        format!(            r#"chatgpt_base_url = "{}/backend-api/"[features]plugins = trueremote_plugin = falseplugin_sharing = true"#,            server.uri()        ),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let mut workspace_installed_body: serde_json::Value =        serde_json::from_str(&workspace_remote_plugin_page_body(            "plugins~Plugin_22222222222222222222222222222222",            "shared-linear",            "Shared Linear",            "PRIVATE",            /*enabled*/ Some(true),        ))?;    let unlisted_installed_body: serde_json::Value =        serde_json::from_str(&workspace_remote_plugin_page_body(            "plugins~Plugin_33333333333333333333333333333333",            "unlisted-linear",            "Unlisted Linear",            "UNLISTED",            /*enabled*/ Some(false),        ))?;    workspace_installed_body["plugins"]        .as_array_mut()        .expect("installed plugins should be an array")        .push(unlisted_installed_body["plugins"][0].clone());    let workspace_installed_body = serde_json::to_string(&workspace_installed_body)?;    let global_installed_body = remote_installed_plugin_body("", "1.2.3", /*enabled*/ true);    mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await;    mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await;    mount_empty_user_installed_plugins(&server).await;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_installed_request(PluginInstalledParams {            cwds: None,            install_suggestion_plugin_names: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginInstalledResponse = to_response(response)?;    assert_eq!(response.marketplaces.len(), 1);    let marketplace = &response.marketplaces[0];    assert_eq!(marketplace.name, "workspace-shared-with-me");    assert_eq!(        marketplace            .interface            .as_ref()            .and_then(|interface| interface.display_name.as_deref()),        Some("Shared with me")    );    assert_eq!(        marketplace            .plugins            .iter()            .map(|plugin| (plugin.id.clone(), plugin.installed, plugin.enabled))            .collect::<Vec<_>>(),        vec![            (                "shared-linear@workspace-shared-with-me".to_string(),                true,                true            ),            (                "unlisted-linear@workspace-shared-with-me".to_string(),                true,                false            )        ]    );    wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;    wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;    Ok(())}#[tokio::test]async fn plugin_installed_includes_workspace_directory_without_plugin_sharing() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    std::fs::write(        codex_home.path().join("config.toml"),        format!(            r#"chatgpt_base_url = "{}/backend-api/"[features]plugins = trueremote_plugin = falseplugin_sharing = false"#,            server.uri()        ),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let mut workspace_installed_body: serde_json::Value =        serde_json::from_str(&workspace_remote_plugin_page_body(            "plugins~Plugin_11111111111111111111111111111111",            "workspace-linear",            "Workspace Linear",            "LISTED",            /*enabled*/ Some(true),        ))?;    let shared_installed_body: serde_json::Value =        serde_json::from_str(&workspace_remote_plugin_page_body(            "plugins~Plugin_22222222222222222222222222222222",            "shared-linear",            "Shared Linear",            "PRIVATE",            /*enabled*/ Some(true),        ))?;    workspace_installed_body["plugins"]        .as_array_mut()        .expect("installed plugins should be an array")        .push(shared_installed_body["plugins"][0].clone());    let workspace_installed_body = serde_json::to_string(&workspace_installed_body)?;    mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;    mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await;    mount_empty_user_installed_plugins(&server).await;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_installed_request(PluginInstalledParams {            cwds: None,            install_suggestion_plugin_names: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginInstalledResponse = to_response(response)?;    assert_eq!(response.marketplaces.len(), 1);    let marketplace = &response.marketplaces[0];    assert_eq!(marketplace.name, "workspace-directory");    assert_eq!(        marketplace            .plugins            .iter()            .map(|plugin| (plugin.id.clone(), plugin.installed, plugin.enabled))            .collect::<Vec<_>>(),        vec![(            "workspace-linear@workspace-directory".to_string(),            true,            true        )]    );    wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;    wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;    Ok(())}#[tokio::test]async fn plugin_installed_includes_created_by_me_when_remote_plugins_enabled() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    std::fs::write(        codex_home.path().join("config.toml"),        format!(            r#"chatgpt_base_url = "{}/backend-api/"[features]plugins = trueremote_plugin = trueplugin_sharing = false"#,            server.uri()        ),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;    mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())        .await;    let bundle_url = mount_remote_plugin_bundle(        &server,        "private-linear",        remote_plugin_bundle_tar_gz_bytes("private-linear")?,    )    .await;    let mut user_installed_body: serde_json::Value =        serde_json::from_str(&user_remote_plugin_page_body(            "plugins~Plugin_55555555555555555555555555555555",            "private-linear",            "Private Linear",            "PRIVATE",            /*enabled*/ Some(true),        ))?;    user_installed_body["plugins"][0]["release"]["bundle_download_url"] =        serde_json::json!(bundle_url);    mount_remote_installed_plugins(        &server,        "USER",        &serde_json::to_string(&user_installed_body)?,    )    .await;    let mut mcp = TestAppServer::new_with_env(        codex_home.path(),        &[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],    )    .await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_installed_request(PluginInstalledParams {            cwds: None,            install_suggestion_plugin_names: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginInstalledResponse = to_response(response)?;    assert_eq!(response.marketplaces.len(), 1);    assert_eq!(response.marketplaces[0].name, "created-by-me-remote");    assert_eq!(        response.marketplaces[0]            .plugins            .iter()            .map(|plugin| (plugin.id.as_str(), plugin.installed, plugin.enabled))            .collect::<Vec<_>>(),        vec![("private-linear@created-by-me-remote", true, true)]    );    wait_for_path_exists(        &codex_home.path().join(            "plugins/cache/created-by-me-remote/private-linear/1.2.3/.codex-plugin/plugin.json",        ),    )    .await?;    wait_for_remote_installed_scope_request(&server, "USER").await?;    Ok(())}#[tokio::test]async fn plugin_installed_starts_remote_installed_bundle_sync() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    std::fs::write(        codex_home.path().join("config.toml"),        format!(            r#"chatgpt_base_url = "{}/backend-api/"[features]plugins = trueremote_plugin = trueplugin_sharing = false"#,            server.uri()        ),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let bundle_url = mount_remote_plugin_bundle(        &server,        "linear",        remote_plugin_bundle_tar_gz_bytes("linear")?,    )    .await;    let global_installed_body =        remote_installed_plugin_body(&bundle_url, "1.2.3", /*enabled*/ true);    mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await;    mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())        .await;    mount_empty_user_installed_plugins(&server).await;    let mut mcp = TestAppServer::new_with_env(        codex_home.path(),        &[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],    )    .await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let plugin_installed_request_id = mcp        .send_plugin_installed_request(PluginInstalledParams {            cwds: None,            install_suggestion_plugin_names: None,        })        .await?;    let response: PluginInstalledResponse = to_response(        timeout(            DEFAULT_TIMEOUT,            mcp.read_stream_until_response_message(RequestId::Integer(plugin_installed_request_id)),        )        .await??,    )?;    assert_eq!(response.marketplaces.len(), 1);    assert_eq!(response.marketplaces[0].name, "openai-curated-remote");    assert_eq!(        response.marketplaces[0]            .plugins            .iter()            .map(|plugin| (plugin.id.clone(), plugin.installed, plugin.enabled))            .collect::<Vec<_>>(),        vec![("linear@openai-curated-remote".to_string(), true, true)]    );    let installed_path = codex_home        .path()        .join("plugins/cache/openai-curated-remote/linear/1.2.3/.codex-plugin/plugin.json");    wait_for_path_exists(&installed_path).await?;    wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;    wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;    Ok(())}#[tokio::test]async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_plugins_enabled_config_with_base_url(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let workspace_plugin_body = workspace_remote_plugin_page_body(        "plugins~Plugin_11111111111111111111111111111111",        "workspace-linear",        "Workspace Linear",        "LISTED",        /*enabled*/ None,    );    let workspace_installed_body = workspace_remote_plugin_page_body(        "plugins~Plugin_11111111111111111111111111111111",        "workspace-linear",        "Workspace Linear",        "LISTED",        /*enabled*/ Some(false),    );    mount_remote_plugin_list(&server, "WORKSPACE", &workspace_plugin_body).await;    mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await;    mount_empty_user_installed_plugins(&server).await;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: Some(vec![PluginListMarketplaceKind::WorkspaceDirectory]),        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert_eq!(response.marketplaces.len(), 1);    let marketplace = &response.marketplaces[0];    assert_eq!(marketplace.name, "workspace-directory");    assert_eq!(        marketplace            .interface            .as_ref()            .and_then(|interface| interface.display_name.as_deref()),        Some("Workspace Directory")    );    assert_eq!(marketplace.plugins.len(), 1);    assert_eq!(        marketplace.plugins[0].id,        "workspace-linear@workspace-directory"    );    assert_eq!(        marketplace.plugins[0].remote_plugin_id.as_deref(),        Some("plugins~Plugin_11111111111111111111111111111111")    );    assert_eq!(marketplace.plugins[0].name, "workspace-linear");    assert_eq!(marketplace.plugins[0].installed, true);    assert_eq!(marketplace.plugins[0].enabled, false);    assert!(        !server            .received_requests()            .await            .expect("wiremock should record requests")            .iter()            .any(|request| request                .url                .query()                .is_some_and(|query| query.contains("scope=GLOBAL")))    );    Ok(())}#[tokio::test]async fn plugin_list_fetches_user_plugins_in_created_by_me_remote_marketplace() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    std::fs::write(        codex_home.path().join("config.toml"),        format!(            r#"chatgpt_base_url = "{}/backend-api/"[features]plugins = trueremote_plugin = trueplugin_sharing = false"#,            server.uri()        ),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let mut private_page: serde_json::Value = serde_json::from_str(&user_remote_plugin_page_body(        "plugins~Plugin_55555555555555555555555555555555",        "private-linear",        "Private Linear",        "PRIVATE",        /*enabled*/ None,    ))?;    private_page["pagination"]["next_page_token"] = serde_json::json!("page-2");    Mock::given(method("GET"))        .and(path("/backend-api/ps/plugins/list"))        .and(query_param("scope", "USER"))        .and(query_param("limit", "200"))        .and(query_param_is_missing("pageToken"))        .respond_with(ResponseTemplate::new(200).set_body_json(private_page))        .mount(&server)        .await;    Mock::given(method("GET"))        .and(path("/backend-api/ps/plugins/list"))        .and(query_param("scope", "USER"))        .and(query_param("limit", "200"))        .and(query_param("pageToken", "page-2"))        .respond_with(            ResponseTemplate::new(200).set_body_string(user_remote_plugin_page_body(                "plugins~Plugin_66666666666666666666666666666666",                "second-private-linear",                "Second Private Linear",                "PRIVATE",                /*enabled*/ None,            )),        )        .mount(&server)        .await;    mount_remote_installed_plugins(        &server,        "USER",        &user_remote_plugin_page_body(            "plugins~Plugin_55555555555555555555555555555555",            "private-linear",            "Private Linear",            "PRIVATE",            /*enabled*/ Some(true),        ),    )    .await;    mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;    mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())        .await;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: Some(vec![PluginListMarketplaceKind::CreatedByMeRemote]),        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert_eq!(response.marketplaces.len(), 1);    let marketplace = &response.marketplaces[0];    assert_eq!(marketplace.name, "created-by-me-remote");    assert_eq!(        marketplace            .interface            .as_ref()            .and_then(|interface| interface.display_name.as_deref()),        Some("Created by me")    );    assert_eq!(marketplace.plugins.len(), 2);    assert_eq!(        marketplace.plugins[0].id,        "private-linear@created-by-me-remote"    );    assert_eq!(        marketplace.plugins[0].remote_plugin_id.as_deref(),        Some("plugins~Plugin_55555555555555555555555555555555")    );    assert_eq!(marketplace.plugins[0].installed, true);    assert_eq!(marketplace.plugins[0].enabled, true);    assert_eq!(marketplace.plugins[0].share_context, None);    assert_eq!(        marketplace.plugins[1].id,        "second-private-linear@created-by-me-remote"    );    assert_eq!(marketplace.plugins[1].installed, false);    assert_eq!(marketplace.plugins[1].enabled, false);    assert!(        !server            .received_requests()            .await            .expect("wiremock should record requests")            .iter()            .any(|request| {                request.url.path().ends_with("/ps/plugins/list")                    && request                        .url                        .query_pairs()                        .any(|(key, value)| key == "scope" && value != "USER")            })    );    Ok(())}#[tokio::test]async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_plugins_enabled_config_with_base_url(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let mut shared_plugin_body: serde_json::Value =        serde_json::from_str(&workspace_remote_plugin_page_body(            "plugins~Plugin_22222222222222222222222222222222",            "shared-linear",            "Shared Linear",            "PRIVATE",            /*enabled*/ None,        ))?;    shared_plugin_body["plugins"][0]["share_principals"] = serde_json::Value::Null;    let shared_unlisted_body: serde_json::Value =        serde_json::from_str(&workspace_remote_plugin_page_body(            "plugins~Plugin_44444444444444444444444444444444",            "shared-unlisted-linear",            "Shared Unlisted Linear",            "UNLISTED",            /*enabled*/ None,        ))?;    shared_plugin_body["plugins"]        .as_array_mut()        .expect("shared plugins should be an array")        .push(shared_unlisted_body["plugins"][0].clone());    let shared_plugin_body = serde_json::to_string(&shared_plugin_body)?;    let mut workspace_installed_body: serde_json::Value =        serde_json::from_str(&workspace_remote_plugin_page_body(            "plugins~Plugin_22222222222222222222222222222222",            "shared-linear",            "Shared Linear",            "PRIVATE",            /*enabled*/ Some(true),        ))?;    let unlisted_installed_body: serde_json::Value =        serde_json::from_str(&workspace_remote_plugin_page_body(            "plugins~Plugin_33333333333333333333333333333333",            "unlisted-linear",            "Unlisted Linear",            "UNLISTED",            /*enabled*/ Some(false),        ))?;    workspace_installed_body["plugins"]        .as_array_mut()        .expect("installed plugins should be an array")        .push(unlisted_installed_body["plugins"][0].clone());    let workspace_installed_body = serde_json::to_string(&workspace_installed_body)?;    mount_shared_workspace_plugins(&server, &shared_plugin_body).await;    mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;    mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await;    mount_empty_user_installed_plugins(&server).await;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: Some(vec![PluginListMarketplaceKind::SharedWithMe]),        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert_eq!(response.marketplaces.len(), 2);    let marketplace = response        .marketplaces        .iter()        .find(|marketplace| marketplace.name == "workspace-shared-with-me-private")        .expect("expected private shared-with-me marketplace");    assert_eq!(        marketplace            .interface            .as_ref()            .and_then(|interface| interface.display_name.as_deref()),        Some("Shared with me")    );    assert_eq!(marketplace.plugins.len(), 2);    assert_eq!(        marketplace.plugins[0].id,        "shared-linear@workspace-shared-with-me"    );    assert_eq!(        marketplace.plugins[0].remote_plugin_id.as_deref(),        Some("plugins~Plugin_22222222222222222222222222222222")    );    assert_eq!(marketplace.plugins[0].name, "shared-linear");    assert_eq!(marketplace.plugins[0].installed, true);    assert_eq!(marketplace.plugins[0].enabled, true);    let share_context = marketplace.plugins[0]        .share_context        .as_ref()        .expect("expected share context");    assert_eq!(        share_context.remote_plugin_id,        "plugins~Plugin_22222222222222222222222222222222"    );    assert_eq!(share_context.remote_version.as_deref(), Some("1.2.3"));    assert_eq!(        share_context.discoverability,        Some(PluginShareDiscoverability::Private)    );    assert_eq!(        share_context.creator_account_user_id.as_deref(),        Some("user-gavin__account-123")    );    assert_eq!(share_context.creator_name.as_deref(), Some("Gavin"));    assert_eq!(        share_context.share_url.as_deref(),        Some("https://chatgpt.example/plugins/share/share-key-1")    );    assert_eq!(share_context.share_principals, None);    assert_eq!(        marketplace.plugins[1].id,        "shared-unlisted-linear@workspace-shared-with-me"    );    assert_eq!(        marketplace.plugins[1].remote_plugin_id.as_deref(),        Some("plugins~Plugin_44444444444444444444444444444444")    );    assert_eq!(marketplace.plugins[1].name, "shared-unlisted-linear");    assert_eq!(marketplace.plugins[1].installed, false);    assert_eq!(marketplace.plugins[1].enabled, false);    let share_context = marketplace.plugins[1]        .share_context        .as_ref()        .expect("expected share context");    assert_eq!(        share_context.remote_plugin_id,        "plugins~Plugin_44444444444444444444444444444444"    );    assert_eq!(        share_context.discoverability,        Some(PluginShareDiscoverability::Unlisted)    );    let marketplace = response        .marketplaces        .iter()        .find(|marketplace| marketplace.name == "workspace-shared-with-me-unlisted")        .expect("expected unlisted shared-with-me marketplace");    assert_eq!(        marketplace            .interface            .as_ref()            .and_then(|interface| interface.display_name.as_deref()),        Some("Shared with me (unlisted)")    );    assert_eq!(marketplace.plugins.len(), 1);    assert_eq!(        marketplace.plugins[0].id,        "unlisted-linear@workspace-shared-with-me"    );    assert_eq!(        marketplace.plugins[0].remote_plugin_id.as_deref(),        Some("plugins~Plugin_33333333333333333333333333333333")    );    assert_eq!(marketplace.plugins[0].name, "unlisted-linear");    assert_eq!(marketplace.plugins[0].installed, true);    assert_eq!(marketplace.plugins[0].enabled, false);    let share_context = marketplace.plugins[0]        .share_context        .as_ref()        .expect("expected share context");    assert_eq!(        share_context.remote_plugin_id,        "plugins~Plugin_33333333333333333333333333333333"    );    assert_eq!(share_context.remote_version.as_deref(), Some("1.2.3"));    assert_eq!(        share_context.discoverability,        Some(PluginShareDiscoverability::Unlisted)    );    wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;    wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;    wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;    Ok(())}#[tokio::test]async fn plugin_list_omits_shared_with_me_kind_when_plugin_sharing_disabled() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    std::fs::write(        codex_home.path().join("config.toml"),        format!(            r#"chatgpt_base_url = "{}/backend-api/"[features]plugins = trueplugin_sharing = false"#,            server.uri()        ),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: Some(vec![PluginListMarketplaceKind::SharedWithMe]),        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert_eq!(        response,        PluginListResponse {            marketplaces: Vec::new(),            marketplace_load_errors: Vec::new(),            featured_plugin_ids: Vec::new(),        }    );    wait_for_remote_plugin_request_count(        &server,        "/ps/plugins/workspace/shared",        /*expected_count*/ 0,    )    .await?;    Ok(())}#[tokio::test]async fn plugin_list_omits_created_by_me_when_remote_plugins_disabled() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    std::fs::write(        codex_home.path().join("config.toml"),        format!(            r#"chatgpt_base_url = "{}/backend-api/"[features]plugins = trueremote_plugin = falseplugin_sharing = true"#,            server.uri()        ),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: Some(vec![PluginListMarketplaceKind::CreatedByMeRemote]),        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert_eq!(        response,        PluginListResponse {            marketplaces: Vec::new(),            marketplace_load_errors: Vec::new(),            featured_plugin_ids: Vec::new(),        }    );    wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;    Ok(())}#[tokio::test]async fn plugin_list_marks_remote_plugin_disabled_by_admin() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_remote_plugin_catalog_config(        codex_home.path(),        &format!("{}/backend-api/", server.uri()),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let global_directory_body = r#"{  "plugins": [    {      "id": "plugins~Plugin_00000000000000000000000000000000",      "name": "linear",      "scope": "GLOBAL",      "installation_policy": "AVAILABLE",      "authentication_policy": "ON_USE",      "status": "DISABLED_BY_ADMIN",      "release": {        "display_name": "Linear",        "description": "Track work in Linear",        "app_ids": [],        "interface": {},        "skills": []      }    }  ],  "pagination": {    "limit": 50,    "next_page_token": null  }}"#;    let global_installed_body = r#"{  "plugins": [    {      "id": "plugins~Plugin_00000000000000000000000000000000",      "name": "linear",      "scope": "GLOBAL",      "installation_policy": "AVAILABLE",      "authentication_policy": "ON_USE",      "status": "DISABLED_BY_ADMIN",      "release": {        "display_name": "Linear",        "description": "Track work in Linear",        "app_ids": [],        "interface": {},        "skills": []      },      "enabled": true,      "disabled_skill_names": []    }  ],  "pagination": {    "limit": 50,    "next_page_token": null  }}"#;    let empty_page_body = r#"{  "plugins": [],  "pagination": {    "limit": 50,    "next_page_token": null  }}"#;    for (scope, body) in [        ("GLOBAL", global_directory_body),        ("WORKSPACE", empty_page_body),    ] {        Mock::given(method("GET"))            .and(path("/backend-api/ps/plugins/list"))            .and(query_param("scope", scope))            .and(query_param("limit", "200"))            .and(header("authorization", "Bearer chatgpt-token"))            .and(header("chatgpt-account-id", "account-123"))            .respond_with(ResponseTemplate::new(200).set_body_string(body))            .mount(&server)            .await;    }    for (scope, body) in [        ("GLOBAL", global_installed_body),        ("WORKSPACE", empty_page_body),    ] {        Mock::given(method("GET"))            .and(path("/backend-api/ps/plugins/installed"))            .and(query_param("scope", scope))            .and(header("authorization", "Bearer chatgpt-token"))            .and(header("chatgpt-account-id", "account-123"))            .respond_with(ResponseTemplate::new(200).set_body_string(body))            .mount(&server)            .await;    }    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    let remote_marketplace = response        .marketplaces        .into_iter()        .find(|marketplace| marketplace.name == "openai-curated-remote")        .expect("expected ChatGPT remote marketplace");    let plugin = remote_marketplace        .plugins        .first()        .expect("expected remote plugin");    assert_eq!(plugin.installed, true);    assert_eq!(plugin.enabled, true);    assert_eq!(        plugin.availability,        codex_app_server_protocol::PluginAvailability::DisabledByAdmin    );    Ok(())}#[tokio::test]async fn plugin_list_does_not_fetch_remote_marketplaces_when_plugins_disabled() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    std::fs::write(        codex_home.path().join("config.toml"),        format!(            r#"chatgpt_base_url = "{}/backend-api/"[features]plugins = falseremote_plugin = true"#,            server.uri()        ),    )?;    write_chatgpt_auth(        codex_home.path(),        ChatGptAuthFixture::new("chatgpt-token")            .account_id("account-123")            .chatgpt_user_id("user-123")            .chatgpt_account_id("account-123"),        AuthCredentialsStoreMode::File,    )?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert!(response.marketplaces.is_empty());    wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;    Ok(())}#[tokio::test]async fn plugin_list_fetches_featured_plugin_ids_without_chatgpt_auth() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_plugin_sync_config(codex_home.path(), &format!("{}/backend-api/", server.uri()))?;    write_openai_curated_marketplace(codex_home.path(), &["linear", "gmail"])?;    Mock::given(method("GET"))        .and(path("/backend-api/plugins/featured"))        .and(query_param("platform", "codex"))        .respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#))        .mount(&server)        .await;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert_eq!(        response.featured_plugin_ids,        vec!["linear@openai-curated".to_string()]    );    Ok(())}#[tokio::test]async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() -> Result<()> {    let codex_home = TempDir::new()?;    let server = MockServer::start().await;    write_plugin_sync_config(codex_home.path(), &format!("{}/backend-api/", server.uri()))?;    write_openai_curated_marketplace(codex_home.path(), &["linear", "gmail"])?;    Mock::given(method("GET"))        .and(path("/backend-api/plugins/featured"))        .and(query_param("platform", "codex"))        .respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#))        .expect(1)        .mount(&server)        .await;    let mut mcp = TestAppServer::new_with_plugin_startup_tasks(codex_home.path()).await?;    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;    wait_for_featured_plugin_request_count(&server, /*expected_count*/ 1).await?;    let request_id = mcp        .send_plugin_list_request(PluginListParams {            cwds: None,            marketplace_kinds: None,        })        .await?;    let response: JSONRPCResponse = timeout(        DEFAULT_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),    )    .await??;    let response: PluginListResponse = to_response(response)?;    assert_eq!(        response.featured_plugin_ids,        vec!["linear@openai-curated".to_string()]    );    Ok(())}async fn wait_for_featured_plugin_request_count(    server: &MockServer,    expected_count: usize,) -> Result<()> {    wait_for_remote_plugin_request_count(server, "/plugins/featured", expected_count).await}async fn wait_for_workspace_settings_request_count(    server: &MockServer,    expected_count: usize,) -> Result<()> {    wait_for_remote_plugin_request_count(server, "/accounts/account-123/settings", expected_count)        .await}async fn wait_for_remote_plugin_request_count(    server: &MockServer,    path_suffix: &str,    expected_count: usize,) -> Result<()> {    timeout(DEFAULT_TIMEOUT, async {        loop {            let Some(requests) = server.received_requests().await else {                bail!("wiremock did not record requests");            };            let request_count = requests                .iter()                .filter(|request| {                    request.method == "GET" && request.url.path().ends_with(path_suffix)                })                .count();            if request_count == expected_count {                return Ok::<(), anyhow::Error>(());            }            if request_count > expected_count {                bail!(                    "expected exactly {expected_count} {path_suffix} requests, got {request_count}"                );            }            tokio::time::sleep(Duration::from_millis(10)).await;        }    })    .await??;    Ok(())}async fn wait_for_remote_installed_scope_request(server: &MockServer, scope: &str) -> Result<()> {    timeout(DEFAULT_TIMEOUT, async {        loop {            let Some(requests) = server.received_requests().await else {                bail!("wiremock did not record requests");            };            if requests.iter().any(|request| {                request.method == "GET"                    && request.url.path().ends_with("/ps/plugins/installed")                    && request                        .url                        .query_pairs()                        .any(|(name, value)| name == "scope" && value == scope)            }) {                return Ok::<(), anyhow::Error>(());            }            tokio::time::sleep(Duration::from_millis(10)).await;        }    })    .await??;    Ok(())}async fn wait_for_cached_remote_catalog_plugin_ids(    codex_home: &std::path::Path,    expected_plugin_ids: &[&str],) -> Result<()> {    let mut expected_plugin_ids = expected_plugin_ids        .iter()        .copied()        .map(str::to_string)        .collect::<Vec<_>>();    expected_plugin_ids.sort();    timeout(DEFAULT_TIMEOUT, async {        loop {            let plugin_ids = cached_remote_catalog_plugin_ids(codex_home)?;            if plugin_ids == expected_plugin_ids {                return Ok::<(), anyhow::Error>(());            }            tokio::time::sleep(Duration::from_millis(10)).await;        }    })    .await??;    Ok(())}fn cached_remote_catalog_plugin_ids(codex_home: &std::path::Path) -> Result<Vec<String>> {    let cache_dir = codex_home.join("cache/remote_plugin_catalog");    if !cache_dir.exists() {        return Ok(Vec::new());    }    let mut plugin_ids = Vec::new();    for entry in std::fs::read_dir(cache_dir)? {        let path = entry?.path();        let cached_catalog: serde_json::Value = serde_json::from_slice(&std::fs::read(path)?)?;        let Some(plugins) = cached_catalog["plugins"].as_array() else {            continue;        };        plugin_ids.extend(            plugins                .iter()                .filter_map(|plugin| plugin["id"].as_str())                .map(str::to_string),        );    }    plugin_ids.sort();    Ok(plugin_ids)}async fn wait_for_path_exists(path: &std::path::Path) -> Result<()> {    timeout(DEFAULT_TIMEOUT, async {        loop {            if path.exists() {                return Ok::<(), anyhow::Error>(());            }            tokio::time::sleep(Duration::from_millis(10)).await;        }    })    .await??;    Ok(())}async fn wait_for_path_missing(path: &std::path::Path) -> Result<()> {    timeout(DEFAULT_TIMEOUT, async {        loop {            if !path.exists() {                return Ok::<(), anyhow::Error>(());            }            tokio::time::sleep(Duration::from_millis(10)).await;        }    })    .await??;    Ok(())}async fn mount_remote_plugin_list(server: &MockServer, scope: &str, body: &str) {    Mock::given(method("GET"))        .and(path("/backend-api/ps/plugins/list"))        .and(query_param("scope", scope))        .and(query_param("limit", "200"))        .and(header("authorization", "Bearer chatgpt-token"))        .and(header("chatgpt-account-id", "account-123"))        .respond_with(ResponseTemplate::new(200).set_body_string(body))        .mount(server)        .await;}fn remote_plugin_list_body(    remote_plugin_id: &str,    plugin_name: &str,    display_name: &str,    short_description: &str,) -> String {    format!(        r#"{{  "plugins": [    {{      "id": "{remote_plugin_id}",      "name": "{plugin_name}",      "scope": "GLOBAL",      "installation_policy": "AVAILABLE",      "authentication_policy": "ON_USE",      "status": "ENABLED",      "release": {{        "version": "1.2.3",        "display_name": "{display_name}",        "description": "{display_name}",        "app_ids": [],        "interface": {{          "short_description": "{short_description}",          "capabilities": ["Read"]        }},        "skills": []      }}    }}  ],  "pagination": {{    "limit": 50,    "next_page_token": null  }}}}"#    )}async fn mount_openai_curated_remote_collection_plugin_list(server: &MockServer, body: &str) {    Mock::given(method("GET"))        .and(path("/backend-api/ps/plugins/list"))        .and(query_param("scope", "GLOBAL"))        .and(query_param("limit", "200"))        .and(query_param("collection", "vertical"))        .and(header("authorization", "Bearer chatgpt-token"))        .and(header("chatgpt-account-id", "account-123"))        .respond_with(ResponseTemplate::new(200).set_body_string(body))        .mount(server)        .await;}async fn mount_shared_workspace_plugins(server: &MockServer, body: &str) {    Mock::given(method("GET"))        .and(path("/backend-api/ps/plugins/workspace/shared"))        .and(query_param("limit", "200"))        .and(header("authorization", "Bearer chatgpt-token"))        .and(header("chatgpt-account-id", "account-123"))        .respond_with(ResponseTemplate::new(200).set_body_string(body))        .mount(server)        .await;}async fn mount_remote_installed_plugins(server: &MockServer, scope: &str, body: &str) {    Mock::given(method("GET"))        .and(path("/backend-api/ps/plugins/installed"))        .and(query_param("scope", scope))        .and(header("authorization", "Bearer chatgpt-token"))        .and(header("chatgpt-account-id", "account-123"))        .respond_with(ResponseTemplate::new(200).set_body_string(body))        .mount(server)        .await;}async fn mount_empty_user_installed_plugins(server: &MockServer) {    mount_remote_installed_plugins(server, "USER", empty_remote_installed_plugins_body()).await;}fn empty_remote_installed_plugins_body() -> &'static str {    r#"{  "plugins": [],  "pagination": {    "limit": 50,    "next_page_token": null  }}"#}fn workspace_remote_plugin_page_body(    remote_plugin_id: &str,    plugin_name: &str,    display_name: &str,    discoverability: &str,    enabled: Option<bool>,) -> String {    let enabled_field = enabled        .map(|enabled| format!(r#", "enabled": {enabled}, "disabled_skill_names": []"#))        .unwrap_or_default();    format!(        r#"{{  "plugins": [    {{      "id": "{remote_plugin_id}",      "name": "{plugin_name}",      "scope": "WORKSPACE",      "discoverability": "{discoverability}",      "creator_account_user_id": "user-gavin__account-123",      "share_url": "https://chatgpt.example/plugins/share/share-key-1",      "installation_policy": "AVAILABLE",      "authentication_policy": "ON_USE",      "status": "ENABLED",      "creator_name": "Gavin",      "share_principals": [        {{          "principal_type": "user",          "principal_id": "user-gavin__account-123",          "role": "owner",          "name": "Gavin"        }},        {{          "principal_type": "user",          "principal_id": "user-ada__account-123",          "role": "reader",          "name": "Ada"        }}      ],      "release": {{        "version": "1.2.3",        "display_name": "{display_name}",        "description": "Track work",        "app_ids": [],        "interface": {{}},        "skills": []      }}{enabled_field}    }}  ],  "pagination": {{    "limit": 50,    "next_page_token": null  }}}}"#    )}fn user_remote_plugin_page_body(    remote_plugin_id: &str,    plugin_name: &str,    display_name: &str,    discoverability: &str,    enabled: Option<bool>,) -> String {    workspace_remote_plugin_page_body(        remote_plugin_id,        plugin_name,        display_name,        discoverability,        enabled,    )    .replacen(r#""scope": "WORKSPACE""#, r#""scope": "USER""#, 1)}fn remote_installed_plugin_body(    bundle_download_url: &str,    release_version: &str,    enabled: bool,) -> String {    remote_installed_plugin_body_with_optional_app_manifest(        bundle_download_url,        release_version,        enabled,        /*app_manifest*/ None,    )}fn remote_installed_plugin_body_with_app_manifest(    bundle_download_url: &str,    release_version: &str,    enabled: bool,    app_manifest: serde_json::Value,) -> String {    remote_installed_plugin_body_with_optional_app_manifest(        bundle_download_url,        release_version,        enabled,        Some(app_manifest),    )}fn remote_installed_plugin_body_with_optional_app_manifest(    bundle_download_url: &str,    release_version: &str,    enabled: bool,    app_manifest: Option<serde_json::Value>,) -> String {    let app_manifest_field = app_manifest        .map(|manifest| format!(r#"        "app_manifest": {manifest},"#))        .unwrap_or_default();    format!(        r#"{{  "plugins": [    {{      "id": "plugins~Plugin_00000000000000000000000000000000",      "name": "linear",      "scope": "GLOBAL",      "installation_policy": "AVAILABLE",      "authentication_policy": "ON_USE",      "release": {{        "version": "{release_version}",        "display_name": "Linear",        "description": "Track work in Linear",        "bundle_download_url": "{bundle_download_url}",        "app_ids": [],{app_manifest_field}        "interface": {{}},        "skills": []      }},      "enabled": {enabled},      "disabled_skill_names": []    }}  ],  "pagination": {{    "limit": 50,    "next_page_token": null  }}}}"#    )}async fn mount_remote_plugin_bundle(    server: &MockServer,    plugin_name: &str,    body: Vec<u8>,) -> String {    let bundle_path = format!("/bundles/{plugin_name}.tar.gz");    Mock::given(method("GET"))        .and(path(bundle_path.as_str()))        .respond_with(            ResponseTemplate::new(200)                .insert_header("content-type", "application/gzip")                .set_body_bytes(body),        )        .mount(server)        .await;    format!("{}{bundle_path}", server.uri())}fn remote_plugin_bundle_tar_gz_bytes(plugin_name: &str) -> Result<Vec<u8>> {    let manifest = format!(r#"{{"name":"{plugin_name}"}}"#);    let skill = "---\nname: plan-work\ndescription: Track work in Linear.\n---\n\n# Plan Work\n";    let encoder = GzEncoder::new(Vec::new(), Compression::default());    let mut tar = tar::Builder::new(encoder);    for (path, contents, mode) in [        (            ".codex-plugin/plugin.json",            manifest.as_bytes(),            /*mode*/ 0o644,        ),        (            "skills/plan-work/SKILL.md",            skill.as_bytes(),            /*mode*/ 0o644,        ),    ] {        let mut header = tar::Header::new_gnu();        header.set_size(contents.len() as u64);        header.set_mode(mode);        header.set_cksum();        tar.append_data(&mut header, path, contents)?;    }    Ok(tar.into_inner()?.finish()?)}fn write_installed_plugin(    codex_home: &TempDir,    marketplace_name: &str,    plugin_name: &str,) -> Result<()> {    write_installed_plugin_with_version(codex_home, marketplace_name, plugin_name, "local")}fn write_installed_plugin_with_version(    codex_home: &TempDir,    marketplace_name: &str,    plugin_name: &str,    plugin_version: &str,) -> Result<()> {    let plugin_root = codex_home        .path()        .join("plugins/cache")        .join(marketplace_name)        .join(plugin_name)        .join(plugin_version)        .join(".codex-plugin");    std::fs::create_dir_all(&plugin_root)?;    std::fs::write(        plugin_root.join("plugin.json"),        format!(r#"{{"name":"{plugin_name}"}}"#),    )?;    Ok(())}fn write_plugin_sync_config(codex_home: &std::path::Path, base_url: &str) -> std::io::Result<()> {    std::fs::write(        codex_home.join("config.toml"),        format!(            r#"chatgpt_base_url = "{base_url}"[features]plugins = true[plugins."linear@openai-curated"]enabled = false[plugins."gmail@openai-curated"]enabled = false[plugins."calendar@openai-curated"]enabled = true"#        ),    )}fn write_remote_plugin_catalog_config(    codex_home: &std::path::Path,    base_url: &str,) -> std::io::Result<()> {    std::fs::write(        codex_home.join("config.toml"),        format!(            r#"chatgpt_base_url = "{base_url}"[features]plugins = trueremote_plugin = true"#        ),    )}fn write_openai_curated_marketplace(    codex_home: &std::path::Path,    plugin_names: &[&str],) -> std::io::Result<()> {    write_curated_marketplace(        codex_home,        "marketplace.json",        "openai-curated",        /*display_name*/ None,        plugin_names,    )}fn write_openai_api_curated_marketplace(    codex_home: &std::path::Path,    plugin_names: &[&str],) -> std::io::Result<()> {    write_curated_marketplace(        codex_home,        "api_marketplace.json",        "openai-api-curated",        Some("OpenAI Curated"),        plugin_names,    )}fn write_curated_marketplace(    codex_home: &std::path::Path,    manifest_name: &str,    marketplace_name: &str,    display_name: Option<&str>,    plugin_names: &[&str],) -> std::io::Result<()> {    let curated_root = codex_home.join(".tmp/plugins");    std::fs::create_dir_all(curated_root.join(".git"))?;    std::fs::create_dir_all(curated_root.join(".agents/plugins"))?;    let plugins = plugin_names        .iter()        .map(|plugin_name| {            format!(                r#"{{      "name": "{plugin_name}",      "source": {{        "source": "local",        "path": "./plugins/{plugin_name}"      }}    }}"#            )        })        .collect::<Vec<_>>()        .join(",\n");    let interface = display_name        .map(|display_name| {            format!(                r#"  "interface": {{    "displayName": "{display_name}"  }},"#            )        })        .unwrap_or_default();    std::fs::write(        curated_root.join(".agents/plugins").join(manifest_name),        format!(            r#"{{  "name": "{marketplace_name}",{interface}  "plugins": [{plugins}  ]}}"#        ),    )?;    for plugin_name in plugin_names {        let plugin_root = curated_root.join(format!("plugins/{plugin_name}/.codex-plugin"));        std::fs::create_dir_all(&plugin_root)?;        std::fs::write(            plugin_root.join("plugin.json"),            format!(r#"{{"name":"{plugin_name}"}}"#),        )?;    }    std::fs::create_dir_all(codex_home.join(".tmp"))?;    std::fs::write(        codex_home.join(".tmp/plugins.sha"),        format!("{TEST_CURATED_PLUGIN_SHA}\n"),    )?;    Ok(())}fn write_plugin_share_local_path_mapping(    codex_home: &std::path::Path,    remote_plugin_id: &str,    plugin_path: &AbsolutePathBuf,) -> std::io::Result<()> {    let mut local_plugin_paths_by_remote_plugin_id = serde_json::Map::new();    local_plugin_paths_by_remote_plugin_id.insert(        remote_plugin_id.to_string(),        serde_json::to_value(plugin_path).map_err(std::io::Error::other)?,    );    let contents = serde_json::to_string_pretty(&serde_json::json!({        "localPluginPathsByRemotePluginId": local_plugin_paths_by_remote_plugin_id,    }))    .map_err(std::io::Error::other)?;    std::fs::create_dir_all(codex_home.join(".tmp"))?;    std::fs::write(        codex_home.join(".tmp/plugin-share-local-paths-v1.json"),        format!("{contents}\n"),    )}