Codex Handbook
core/src/plugins/discoverable_tests.rs 407 lines
use crate::plugins::test_support::load_plugins_config;use crate::plugins::test_support::write_file;use crate::plugins::test_support::write_openai_curated_marketplace;use codex_core_plugins::PluginsManager;use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;use codex_core_plugins::remote::RemotePluginServiceConfig;use codex_core_plugins::remote::fetch_and_cache_global_remote_plugin_catalog;use codex_core_plugins::startup_sync::curated_plugins_repo_path;use codex_protocol::protocol::Product;use codex_tools::DiscoverablePluginInfo;use pretty_assertions::assert_eq;use tempfile::tempdir;async fn list_discoverable_plugins(    config: &crate::config::Config,    loaded_plugin_app_connector_ids: &[String],) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {    list_discoverable_plugins_with_auth(config, /*auth*/ None, loaded_plugin_app_connector_ids)        .await}async fn list_discoverable_plugins_with_auth(    config: &crate::config::Config,    auth: Option<&codex_login::CodexAuth>,    loaded_plugin_app_connector_ids: &[String],) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {    let plugins_manager = PluginsManager::new_with_options(        config.codex_home.to_path_buf(),        Some(Product::Codex),        auth.map(codex_login::CodexAuth::api_auth_mode),    );    list_discoverable_plugins_with_manager_and_auth(        config,        &plugins_manager,        auth,        loaded_plugin_app_connector_ids,    )    .await}async fn list_discoverable_plugins_with_manager_and_auth(    config: &crate::config::Config,    plugins_manager: &PluginsManager,    auth: Option<&codex_login::CodexAuth>,    loaded_plugin_app_connector_ids: &[String],) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {    super::list_tool_suggest_discoverable_plugins(        config,        plugins_manager,        auth,        loaded_plugin_app_connector_ids,    )    .await}#[tokio::test]async fn list_tool_suggest_discoverable_plugins_includes_cached_remote_global_plugins() {    use codex_login::CodexAuth;    use serde_json::json;    use wiremock::Mock;    use wiremock::MockServer;    use wiremock::ResponseTemplate;    use wiremock::matchers::method;    use wiremock::matchers::path;    use wiremock::matchers::query_param;    let codex_home = tempdir().expect("tempdir should succeed");    write_file(        &codex_home.path().join(crate::config::CONFIG_TOML_FILE),        r#"[features]plugins = trueremote_plugin = true"#,    );    let server = MockServer::start().await;    Mock::given(method("GET"))        .and(path("/backend-api/ps/plugins/list"))        .and(query_param("scope", "GLOBAL"))        .respond_with(ResponseTemplate::new(200).set_body_json(json!({            "plugins": [                {                    "id": "plugins~Plugin_remote_github",                    "name": "github",                    "scope": "GLOBAL",                    "installation_policy": "AVAILABLE",                    "authentication_policy": "ON_USE",                    "status": "AVAILABLE",                    "release": {                        "display_name": "Remote GitHub",                        "description": "Remote GitHub long",                        "app_ids": ["github"],                        "interface": {                            "short_description": "Remote GitHub short",                            "long_description": null,                            "developer_name": null,                            "category": null,                            "capabilities": [],                            "website_url": null,                            "privacy_policy_url": null,                            "terms_of_service_url": null,                            "brand_color": null,                            "default_prompt": null,                            "composer_icon_url": null,                            "logo_url": null,                            "screenshot_urls": []                        },                        "skills": [                            {                                "name": "github",                                "description": "Use GitHub",                                "interface": null                            }                        ]                    }                },                {                    "id": "plugins~Plugin_remote_unlisted",                    "name": "remote-unlisted",                    "scope": "GLOBAL",                    "installation_policy": "AVAILABLE",                    "authentication_policy": "ON_USE",                    "status": "AVAILABLE",                    "release": {                        "display_name": "Remote Unlisted",                        "description": "Remote Unlisted long",                        "app_ids": [],                        "interface": {                            "short_description": "Remote Unlisted short",                            "long_description": null,                            "developer_name": null,                            "category": null,                            "capabilities": [],                            "website_url": null,                            "privacy_policy_url": null,                            "terms_of_service_url": null,                            "brand_color": null,                            "default_prompt": null,                            "composer_icon_url": null,                            "logo_url": null,                            "screenshot_urls": []                        },                        "skills": [                            {                                "name": "remote-unlisted",                                "description": "Use unlisted remote plugin",                                "interface": null                            }                        ]                    }                },                {                    "id": "plugins~Plugin_remote_slack_not_available",                    "name": "slack",                    "scope": "GLOBAL",                    "installation_policy": "NOT_AVAILABLE",                    "authentication_policy": "ON_USE",                    "status": "AVAILABLE",                    "release": {                        "display_name": "Remote Slack",                        "description": "Remote Slack long",                        "app_ids": [],                        "interface": {                            "short_description": "Remote Slack short",                            "long_description": null,                            "developer_name": null,                            "category": null,                            "capabilities": [],                            "website_url": null,                            "privacy_policy_url": null,                            "terms_of_service_url": null,                            "brand_color": null,                            "default_prompt": null,                            "composer_icon_url": null,                            "logo_url": null,                            "screenshot_urls": []                        },                        "skills": []                    }                },                {                    "id": "plugins~Plugin_remote_figma_admin_disabled",                    "name": "figma",                    "scope": "GLOBAL",                    "installation_policy": "AVAILABLE",                    "authentication_policy": "ON_USE",                    "status": "DISABLED_BY_ADMIN",                    "release": {                        "display_name": "Remote Figma",                        "description": "Remote Figma long",                        "app_ids": [],                        "interface": {                            "short_description": "Remote Figma short",                            "long_description": null,                            "developer_name": null,                            "category": null,                            "capabilities": [],                            "website_url": null,                            "privacy_policy_url": null,                            "terms_of_service_url": null,                            "brand_color": null,                            "default_prompt": null,                            "composer_icon_url": null,                            "logo_url": null,                            "screenshot_urls": []                        },                        "skills": []                    }                }            ],            "pagination": {                "next_page_token": null            }        })))        .expect(1)        .mount(&server)        .await;    let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();    let mut config = load_plugins_config(codex_home.path()).await;    config.chatgpt_base_url = format!("{}/backend-api", server.uri());    let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());    fetch_and_cache_global_remote_plugin_catalog(        codex_home.path(),        &RemotePluginServiceConfig {            chatgpt_base_url: config.chatgpt_base_url.clone(),        },        Some(&auth),    )    .await    .expect("remote plugin catalog cache should write");    let discoverable_plugins = list_discoverable_plugins_with_manager_and_auth(        &config,        &plugins_manager,        Some(&auth),        &[],    )    .await    .unwrap();    assert!(        discoverable_plugins            .iter()            .all(|plugin| plugin.id != "github@openai-curated-remote")    );    for scope in ["GLOBAL", "USER", "WORKSPACE"] {        Mock::given(method("GET"))            .and(path("/backend-api/ps/plugins/installed"))            .and(query_param("scope", scope))            .respond_with(ResponseTemplate::new(200).set_body_json(json!({                "plugins": [],                "pagination": {                    "next_page_token": null                }            })))            .expect(1)            .mount(&server)            .await;    }    plugins_manager        .build_and_cache_remote_installed_plugin_marketplaces(            &config.plugins_config_input(),            Some(&auth),            &[REMOTE_GLOBAL_MARKETPLACE_NAME],            /*on_effective_plugins_changed*/ None,        )        .await        .expect("remote installed plugin cache should write");    let discoverable_plugins = list_discoverable_plugins_with_manager_and_auth(        &config,        &plugins_manager,        Some(&auth),        &[],    )    .await    .unwrap();    assert_eq!(        discoverable_plugins            .iter()            .filter(|plugin| plugin.id.ends_with("@openai-curated-remote"))            .map(|plugin| plugin.id.as_str())            .collect::<Vec<_>>(),        vec!["github@openai-curated-remote"]    );    let remote_plugins = discoverable_plugins        .into_iter()        .filter(|plugin| plugin.id == "github@openai-curated-remote")        .collect::<Vec<_>>();    assert_eq!(        remote_plugins,        vec![DiscoverablePluginInfo {            id: "github@openai-curated-remote".to_string(),            remote_plugin_id: Some("plugins~Plugin_remote_github".to_string()),            name: "Remote GitHub".to_string(),            description: Some("Remote GitHub short".to_string()),            has_skills: true,            mcp_server_names: Vec::new(),            app_connector_ids: vec!["github".to_string()],        }]    );    write_file(        &codex_home.path().join(crate::config::CONFIG_TOML_FILE),        r#"[features]plugins = trueremote_plugin = true[tool_suggest]disabled_tools = [  { type = "plugin", id = "github@openai-curated-remote" }]"#,    );    let mut config_with_disabled_remote_plugin = load_plugins_config(codex_home.path()).await;    config_with_disabled_remote_plugin.chatgpt_base_url = config.chatgpt_base_url.clone();    let discoverable_plugins = list_discoverable_plugins_with_manager_and_auth(        &config_with_disabled_remote_plugin,        &plugins_manager,        Some(&auth),        &[],    )    .await    .unwrap();    assert!(        discoverable_plugins            .iter()            .all(|plugin| plugin.id != "github@openai-curated-remote")    );}#[tokio::test]async fn list_tool_suggest_discoverable_plugins_returns_empty_when_plugins_feature_disabled() {    let codex_home = tempdir().expect("tempdir should succeed");    let curated_root = curated_plugins_repo_path(codex_home.path());    write_openai_curated_marketplace(&curated_root, &["slack"]);    write_file(        &codex_home.path().join(crate::config::CONFIG_TOML_FILE),        r#"[features]plugins = false"#,    );    let config = load_plugins_config(codex_home.path()).await;    let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();    assert_eq!(discoverable_plugins, Vec::<DiscoverablePluginInfo>::new());}#[tokio::test]async fn list_tool_suggest_discoverable_plugins_omits_disabled_tool_suggestions() {    let codex_home = tempdir().expect("tempdir should succeed");    let curated_root = curated_plugins_repo_path(codex_home.path());    write_openai_curated_marketplace(&curated_root, &["slack"]);    write_file(        &codex_home.path().join(crate::config::CONFIG_TOML_FILE),        r#"[features]plugins = true[tool_suggest]disabled_tools = [  { type = "plugin", id = "slack@openai-curated" }]"#,    );    let config = load_plugins_config(codex_home.path()).await;    let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();    assert_eq!(discoverable_plugins, Vec::<DiscoverablePluginInfo>::new());}#[tokio::test]async fn list_tool_suggest_discoverable_plugins_includes_configured_plugin_ids() {    let codex_home = tempdir().expect("tempdir should succeed");    let curated_root = curated_plugins_repo_path(codex_home.path());    write_openai_curated_marketplace(&curated_root, &["sample"]);    write_file(        &codex_home.path().join(crate::config::CONFIG_TOML_FILE),        r#"[features]plugins = true[tool_suggest]discoverables = [{ type = "plugin", id = "sample@openai-curated" }]"#,    );    let config = load_plugins_config(codex_home.path()).await;    let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();    assert_eq!(        discoverable_plugins,        vec![DiscoverablePluginInfo {            id: "sample@openai-curated".to_string(),            remote_plugin_id: None,            name: "sample".to_string(),            description: Some(                "Plugin that includes skills, MCP servers, and app connectors".to_string(),            ),            has_skills: true,            mcp_server_names: vec!["sample-docs".to_string()],            app_connector_ids: vec!["connector_calendar".to_string()],        }]    );}