Download codex-rs/core-plugins/src/discoverable_tests.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 38.1 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core-plugins/src/discoverable_tests.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/core-plugins/src/discoverable_tests.rs
-
curl -L -o discoverable_tests.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core-plugins/src/discoverable_tests.rs
38.1 kB
| use super::ToolSuggestDiscoverablePlugin; | |
| use super::ToolSuggestPluginDiscoveryInput; | |
| use crate::OPENAI_BUNDLED_MARKETPLACE_NAME; | |
| use crate::PluginInstallRequest; | |
| use crate::PluginsConfigInput; | |
| use crate::PluginsManager; | |
| use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; | |
| use crate::remote::RemotePluginServiceConfig; | |
| use crate::remote::fetch_and_cache_global_remote_plugin_catalog; | |
| use crate::startup_sync::curated_plugins_repo_path; | |
| use crate::test_support::TEST_CURATED_PLUGIN_SHA; | |
| use crate::test_support::load_plugins_config; | |
| use crate::test_support::set_test_auth_mode; | |
| use crate::test_support::test_auth_manager; | |
| use crate::test_support::test_plugins_manager; | |
| use crate::test_support::test_plugins_manager_with_auth_manager; | |
| use crate::test_support::test_plugins_manager_with_options; | |
| use crate::test_support::write_curated_plugin; | |
| use crate::test_support::write_curated_plugin_sha_with; | |
| use crate::test_support::write_file; | |
| use crate::test_support::write_openai_api_curated_marketplace; | |
| use crate::test_support::write_openai_curated_marketplace; | |
| use codex_config::CONFIG_TOML_FILE; | |
| use codex_login::CodexAuth; | |
| use codex_protocol::auth::AuthMode; | |
| use codex_protocol::protocol::Product; | |
| use codex_utils_absolute_path::AbsolutePathBuf; | |
| use pretty_assertions::assert_eq; | |
| use serde_json::json; | |
| use std::collections::HashSet; | |
| use std::path::Path; | |
| use std::sync::Arc; | |
| use tempfile::tempdir; | |
| use tracing::Level; | |
| use tracing_subscriber::fmt::format::FmtSpan; | |
| use tracing_test::internal::MockWriter; | |
| use wiremock::Mock; | |
| use wiremock::MockServer; | |
| use wiremock::ResponseTemplate; | |
| use wiremock::matchers::method; | |
| use wiremock::matchers::path; | |
| use wiremock::matchers::query_param; | |
| use wiremock::matchers::query_param_is_missing; | |
| async fn returns_fallback_plugins_when_remote_disabled_for_codex_auth() { | |
| let codex_home = tempdir().expect("tempdir should succeed"); | |
| write_file( | |
| &codex_home.path().join(CONFIG_TOML_FILE), | |
| r#"[features] | |
| plugins = true | |
| remote_plugin = false | |
| "#, | |
| ); | |
| let curated_root = curated_plugins_repo_path(codex_home.path()); | |
| write_openai_curated_marketplace(&curated_root, &["sample", "slack", "openai-developers"]); | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager_with_options( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Some(AuthMode::Chatgpt), | |
| ); | |
| let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| Some(&auth), | |
| ) | |
| .await; | |
| assert_eq!( | |
| discoverable_plugins | |
| .into_iter() | |
| .map(|plugin| plugin.id) | |
| .collect::<Vec<_>>(), | |
| vec![ | |
| "openai-developers@openai-curated".to_string(), | |
| "slack@openai-curated".to_string(), | |
| ] | |
| ); | |
| } | |
| async fn returns_api_curated_fallback_plugins_for_direct_provider_auth() { | |
| let codex_home = tempdir().expect("tempdir should succeed"); | |
| let curated_root = curated_plugins_repo_path(codex_home.path()); | |
| write_openai_api_curated_marketplace(&curated_root, &["sample", "slack", "openai-developers"]); | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager_with_options( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Some(AuthMode::ApiKey), | |
| ); | |
| let auth = CodexAuth::from_api_key("test-api-key"); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| Some(&auth), | |
| ) | |
| .await; | |
| assert_eq!( | |
| discoverable_plugins | |
| .into_iter() | |
| .map(|plugin| plugin.id) | |
| .collect::<Vec<_>>(), | |
| vec![ | |
| "openai-developers@openai-api-curated".to_string(), | |
| "slack@openai-api-curated".to_string(), | |
| ] | |
| ); | |
| } | |
| async fn returns_microsoft_fallback_plugins() { | |
| let codex_home = tempdir().expect("tempdir should succeed"); | |
| let curated_root = curated_plugins_repo_path(codex_home.path()); | |
| write_openai_curated_marketplace( | |
| &curated_root, | |
| &["teams", "sharepoint", "outlook-email", "outlook-calendar"], | |
| ); | |
| install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "teams").await; | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager_with_options( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Some(AuthMode::Chatgpt), | |
| ); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!( | |
| discoverable_plugins | |
| .into_iter() | |
| .map(|plugin| plugin.id) | |
| .collect::<Vec<_>>(), | |
| vec![ | |
| "outlook-calendar@openai-curated".to_string(), | |
| "outlook-email@openai-curated".to_string(), | |
| "sharepoint@openai-curated".to_string(), | |
| ] | |
| ); | |
| } | |
| async fn omits_openai_curated_but_keeps_configured_marketplaces_for_remote_codex_auth() { | |
| 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"]); | |
| let bundled_marketplace_name = OPENAI_BUNDLED_MARKETPLACE_NAME; | |
| let bundled_marketplace_root = codex_home.path().join(format!( | |
| ".tmp/bundled-marketplaces/{bundled_marketplace_name}" | |
| )); | |
| write_file( | |
| &bundled_marketplace_root.join(".agents/plugins/marketplace.json"), | |
| &format!( | |
| r#"{{ | |
| "name": "{bundled_marketplace_name}", | |
| "plugins": [ | |
| {{"name": "chrome", "source": {{"source": "local", "path": "./plugins/chrome"}}}} | |
| ] | |
| }} | |
| "# | |
| ), | |
| ); | |
| write_curated_plugin(&bundled_marketplace_root, "chrome"); | |
| write_file( | |
| &codex_home.path().join(CONFIG_TOML_FILE), | |
| &format!( | |
| r#"[features] | |
| plugins = true | |
| [marketplaces.{bundled_marketplace_name}] | |
| source_type = "local" | |
| source = {bundled_marketplace_root:?} | |
| "# | |
| ), | |
| ); | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager_with_options( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Some(AuthMode::Chatgpt), | |
| ); | |
| let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| Some(&auth), | |
| ) | |
| .await; | |
| assert_eq!( | |
| discoverable_plugins | |
| .into_iter() | |
| .map(|plugin| plugin.id) | |
| .collect::<Vec<_>>(), | |
| vec!["chrome@openai-bundled".to_string()] | |
| ); | |
| } | |
| async fn includes_openai_api_curated_when_remote_enabled_without_auth() { | |
| let codex_home = tempdir().expect("tempdir should succeed"); | |
| let curated_root = curated_plugins_repo_path(codex_home.path()); | |
| write_openai_api_curated_marketplace(&curated_root, &["slack"]); | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!( | |
| discoverable_plugins | |
| .into_iter() | |
| .map(|plugin| plugin.id) | |
| .collect::<Vec<_>>(), | |
| vec!["slack@openai-api-curated".to_string()] | |
| ); | |
| } | |
| async fn deduplicates_and_reprojects_cached_configured_marketplace_plugin() { | |
| let codex_home = tempdir().expect("tempdir should succeed"); | |
| let plugin_name = "sample"; | |
| let marketplace_name = OPENAI_BUNDLED_MARKETPLACE_NAME; | |
| let plugin_id = format!("{plugin_name}@{marketplace_name}"); | |
| let marketplace_root = codex_home | |
| .path() | |
| .join(format!(".tmp/bundled-marketplaces/{marketplace_name}")); | |
| write_file( | |
| &marketplace_root.join(".agents/plugins/marketplace.json"), | |
| &format!( | |
| r#"{{ | |
| "name": "{marketplace_name}", | |
| "plugins": [ | |
| {{"name": "{plugin_name}", "source": {{"source": "local", "path": "./plugins/{plugin_name}"}}}} | |
| ] | |
| }} | |
| "# | |
| ), | |
| ); | |
| write_curated_plugin(&marketplace_root, plugin_name); | |
| write_plugin_app( | |
| &marketplace_root, | |
| plugin_name, | |
| "sample-docs", | |
| "connector_sample", | |
| ); | |
| write_file( | |
| &codex_home.path().join(CONFIG_TOML_FILE), | |
| &format!( | |
| r#"[features] | |
| plugins = true | |
| [marketplaces.{marketplace_name}] | |
| source_type = "local" | |
| source = {marketplace_root:?} | |
| "# | |
| ), | |
| ); | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let auth_manager = test_auth_manager(Some(AuthMode::Chatgpt)); | |
| let plugins_manager = test_plugins_manager_with_auth_manager( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Arc::clone(&auth_manager), | |
| ); | |
| let chatgpt_projection = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins.clone(), &[plugin_id.as_str()], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| let expected = ToolSuggestDiscoverablePlugin { | |
| id: plugin_id.clone(), | |
| 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::new(), | |
| app_connector_ids: vec!["connector_sample".to_string()], | |
| }; | |
| assert_eq!(chatgpt_projection, vec![expected.clone()]); | |
| set_test_auth_mode(&auth_manager, Some(AuthMode::ApiKey)).await; | |
| let api_key_projection = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[plugin_id.as_str()], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!( | |
| api_key_projection, | |
| vec![ToolSuggestDiscoverablePlugin { | |
| mcp_server_names: vec!["sample-docs".to_string()], | |
| app_connector_ids: Vec::new(), | |
| ..expected | |
| }] | |
| ); | |
| } | |
| async fn reprojects_cached_skill_availability_for_current_config() { | |
| 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"]); | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager_with_options( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Some(AuthMode::Chatgpt), | |
| ); | |
| let expected = ToolSuggestDiscoverablePlugin { | |
| id: "slack@openai-curated".to_string(), | |
| remote_plugin_id: None, | |
| name: "slack".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()], | |
| }; | |
| let initial = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!(initial, vec![expected.clone()]); | |
| write_file( | |
| &codex_home.path().join(CONFIG_TOML_FILE), | |
| r#"[[skills.config]] | |
| name = "slack:sample" | |
| enabled = false | |
| "#, | |
| ); | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let after_skill_disabled = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!( | |
| after_skill_disabled, | |
| vec![ToolSuggestDiscoverablePlugin { | |
| has_skills: false, | |
| ..expected | |
| }] | |
| ); | |
| } | |
| async fn does_not_advertise_skills_when_skill_loading_fails() { | |
| 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( | |
| &curated_root.join("plugins/slack/skills/SKILL.md"), | |
| "---\nname: bad", | |
| ); | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager_with_options( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Some(AuthMode::Chatgpt), | |
| ); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!( | |
| discoverable_plugins, | |
| vec![ToolSuggestDiscoverablePlugin { | |
| id: "slack@openai-curated".to_string(), | |
| remote_plugin_id: None, | |
| name: "slack".to_string(), | |
| description: Some( | |
| "Plugin that includes skills, MCP servers, and app connectors".to_string(), | |
| ), | |
| has_skills: false, | |
| mcp_server_names: vec!["sample-docs".to_string()], | |
| app_connector_ids: vec!["connector_calendar".to_string()], | |
| }] | |
| ); | |
| } | |
| async fn clear_cache_invalidates_cached_tool_suggest_metadata() { | |
| 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"]); | |
| let plugin_manifest = curated_root.join("plugins/slack/.codex-plugin/plugin.json"); | |
| write_file( | |
| &plugin_manifest, | |
| r#"{ | |
| "name": "slack", | |
| "description": "Before reload" | |
| }"#, | |
| ); | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager_with_options( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Some(AuthMode::Chatgpt), | |
| ); | |
| let input = discovery_input(plugins, &[], &[], &[]); | |
| let expected_cached = vec![ToolSuggestDiscoverablePlugin { | |
| id: "slack@openai-curated".to_string(), | |
| remote_plugin_id: None, | |
| name: "slack".to_string(), | |
| description: Some("Before reload".to_string()), | |
| has_skills: true, | |
| mcp_server_names: vec!["sample-docs".to_string()], | |
| app_connector_ids: vec!["connector_calendar".to_string()], | |
| }]; | |
| let initial = list_discoverable_plugins(&plugins_manager, input.clone(), /*auth*/ None).await; | |
| assert_eq!(initial, expected_cached); | |
| write_file( | |
| &plugin_manifest, | |
| r#"{ | |
| "name": "slack", | |
| "description": "After reload" | |
| }"#, | |
| ); | |
| let before_reload = | |
| list_discoverable_plugins(&plugins_manager, input.clone(), /*auth*/ None).await; | |
| assert_eq!(before_reload, expected_cached); | |
| plugins_manager.clear_cache(); | |
| let after_reload = list_discoverable_plugins(&plugins_manager, input, /*auth*/ None).await; | |
| assert_eq!( | |
| after_reload, | |
| vec![ToolSuggestDiscoverablePlugin { | |
| description: Some("After reload".to_string()), | |
| ..expected_cached[0].clone() | |
| }] | |
| ); | |
| } | |
| async fn ignores_missing_marketplace_plugin() { | |
| let codex_home = tempdir().expect("tempdir should succeed"); | |
| let curated_root = curated_plugins_repo_path(codex_home.path()); | |
| write_openai_curated_marketplace(&curated_root, &["installed", "slack"]); | |
| let marketplace_name = OPENAI_BUNDLED_MARKETPLACE_NAME; | |
| let marketplace_root = codex_home | |
| .path() | |
| .join(format!(".tmp/marketplaces/{marketplace_name}")); | |
| write_file( | |
| &marketplace_root.join(".agents/plugins/marketplace.json"), | |
| &format!( | |
| r#"{{ | |
| "name": "{marketplace_name}", | |
| "plugins": [ | |
| {{"name": "sample", "source": {{"source": "local", "path": "./plugins/sample"}}}} | |
| ] | |
| }} | |
| "# | |
| ), | |
| ); | |
| write_file( | |
| &codex_home.path().join(CONFIG_TOML_FILE), | |
| &format!( | |
| r#"[features] | |
| plugins = true | |
| [marketplaces.{marketplace_name}] | |
| source_type = "git" | |
| source = "/tmp/{marketplace_name}" | |
| "# | |
| ), | |
| ); | |
| install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "installed").await; | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager_with_options( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Some(AuthMode::Chatgpt), | |
| ); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!(discoverable_plugins.len(), 1); | |
| assert_eq!(discoverable_plugins[0].id, "slack@openai-curated"); | |
| } | |
| async fn normalizes_description() { | |
| let codex_home = tempdir().expect("tempdir should succeed"); | |
| let curated_root = curated_plugins_repo_path(codex_home.path()); | |
| write_openai_curated_marketplace(&curated_root, &["installed", "slack"]); | |
| write_file( | |
| &curated_root.join("plugins/slack/.codex-plugin/plugin.json"), | |
| r#"{ | |
| "name": "slack", | |
| "description": " Plugin\n with extra spacing " | |
| }"#, | |
| ); | |
| install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "installed").await; | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager_with_options( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Some(AuthMode::Chatgpt), | |
| ); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!( | |
| discoverable_plugins, | |
| vec![ToolSuggestDiscoverablePlugin { | |
| id: "slack@openai-curated".to_string(), | |
| remote_plugin_id: None, | |
| name: "slack".to_string(), | |
| description: Some("Plugin with extra spacing".to_string()), | |
| has_skills: true, | |
| mcp_server_names: vec!["sample-docs".to_string()], | |
| app_connector_ids: vec!["connector_calendar".to_string()], | |
| }] | |
| ); | |
| } | |
| async fn omits_installed_curated_plugins() { | |
| 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"]); | |
| install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "slack").await; | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!(discoverable_plugins, Vec::new()); | |
| } | |
| async fn omits_not_available_curated_plugins() { | |
| let codex_home = tempdir().expect("tempdir should succeed"); | |
| let curated_root = curated_plugins_repo_path(codex_home.path()); | |
| write_file( | |
| &curated_root.join(".agents/plugins/marketplace.json"), | |
| r#"{ | |
| "name": "openai-curated", | |
| "plugins": [ | |
| { | |
| "name": "installed", | |
| "source": { | |
| "source": "local", | |
| "path": "./plugins/installed" | |
| } | |
| }, | |
| { | |
| "name": "slack", | |
| "source": { | |
| "source": "local", | |
| "path": "./plugins/slack" | |
| } | |
| }, | |
| { | |
| "name": "gmail", | |
| "source": { | |
| "source": "local", | |
| "path": "./plugins/gmail" | |
| }, | |
| "policy": { | |
| "installation": "NOT_AVAILABLE" | |
| } | |
| } | |
| ] | |
| } | |
| "#, | |
| ); | |
| write_curated_plugin(&curated_root, "installed"); | |
| write_curated_plugin(&curated_root, "slack"); | |
| write_curated_plugin(&curated_root, "gmail"); | |
| install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "installed").await; | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager_with_options( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Some(AuthMode::Chatgpt), | |
| ); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!( | |
| discoverable_plugins | |
| .into_iter() | |
| .map(|plugin| plugin.id) | |
| .collect::<Vec<_>>(), | |
| vec!["slack@openai-curated".to_string()] | |
| ); | |
| } | |
| async fn does_not_reload_marketplace_per_plugin() { | |
| 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", "gmail", "openai-developers"]); | |
| install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "slack").await; | |
| let too_long_prompt = "x".repeat(129); | |
| for plugin_name in ["gmail", "openai-developers"] { | |
| write_file( | |
| &curated_root.join(format!("plugins/{plugin_name}/.codex-plugin/plugin.json")), | |
| &format!( | |
| r#"{{ | |
| "name": "{plugin_name}", | |
| "description": "Plugin that includes skills, MCP servers, and app connectors", | |
| "interface": {{ | |
| "defaultPrompt": "{too_long_prompt}" | |
| }} | |
| }}"# | |
| ), | |
| ); | |
| } | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager_with_options( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Some(AuthMode::Chatgpt), | |
| ); | |
| let buffer: &'static std::sync::Mutex<Vec<u8>> = | |
| Box::leak(Box::new(std::sync::Mutex::new(Vec::new()))); | |
| let subscriber = tracing_subscriber::fmt() | |
| .with_level(true) | |
| .with_ansi(false) | |
| .with_max_level(Level::WARN) | |
| .with_span_events(FmtSpan::NONE) | |
| .with_writer(MockWriter::new(buffer)) | |
| .finish(); | |
| let _guard = tracing::subscriber::set_default(subscriber); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!( | |
| discoverable_plugins | |
| .iter() | |
| .map(|plugin| plugin.id.as_str()) | |
| .collect::<Vec<_>>(), | |
| vec!["gmail@openai-curated", "openai-developers@openai-curated"] | |
| ); | |
| let logs = String::from_utf8(buffer.lock().expect("buffer lock").clone()) | |
| .expect("utf8 logs") | |
| .replace('\\', "/"); | |
| assert_eq!(logs.matches("ignoring interface.defaultPrompt").count(), 8); | |
| assert_eq!(logs.matches("gmail/.codex-plugin/plugin.json").count(), 4); | |
| assert_eq!( | |
| logs.matches("openai-developers/.codex-plugin/plugin.json") | |
| .count(), | |
| 4 | |
| ); | |
| } | |
| async fn does_not_expand_local_plugins_by_installed_apps() { | |
| 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", "slack", "hubspot"]); | |
| write_plugin_app(&curated_root, "sample", "sample", "connector_sample"); | |
| install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "slack").await; | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!(discoverable_plugins, Vec::new()); | |
| } | |
| async fn does_not_read_local_plugins_for_loaded_apps() { | |
| let hubspot_app_id = "asdk_app_697acb8e53d88191bf7a79e62012ae14"; | |
| let granola_app_id = "asdk_app_697761cab6f48191b5ed345919a3ce8b"; | |
| let codex_home = tempdir().expect("tempdir should succeed"); | |
| let curated_root = curated_plugins_repo_path(codex_home.path()); | |
| write_openai_curated_marketplace(&curated_root, &["hubspot", "granola", "sample"]); | |
| write_plugin_app(&curated_root, "hubspot", "hubspot", hubspot_app_id); | |
| write_plugin_app(&curated_root, "granola", "granola", granola_app_id); | |
| write_file( | |
| &curated_root.join("plugins/sample/.app.json"), | |
| "invalid json", | |
| ); | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); | |
| let buffer: &'static std::sync::Mutex<Vec<u8>> = | |
| Box::leak(Box::new(std::sync::Mutex::new(Vec::new()))); | |
| let subscriber = tracing_subscriber::fmt() | |
| .with_level(true) | |
| .with_ansi(false) | |
| .with_max_level(Level::WARN) | |
| .with_span_events(FmtSpan::NONE) | |
| .with_writer(MockWriter::new(buffer)) | |
| .finish(); | |
| let _guard = tracing::subscriber::set_default(subscriber); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[hubspot_app_id]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!(discoverable_plugins, Vec::new()); | |
| let logs = String::from_utf8(buffer.lock().expect("buffer lock").clone()) | |
| .expect("utf8 logs") | |
| .replace('\\', "/"); | |
| assert_eq!(logs.matches("plugins/sample/.app.json").count(), 0); | |
| } | |
| async fn does_not_expand_local_sales_apps() { | |
| let hubspot_app_id = "asdk_app_697acb8e53d88191bf7a79e62012ae14"; | |
| let granola_app_id = "asdk_app_697761cab6f48191b5ed345919a3ce8b"; | |
| let test_app_id = "asdk_app_test_source"; | |
| let codex_home = tempdir().expect("tempdir should succeed"); | |
| let curated_root = curated_plugins_repo_path(codex_home.path()); | |
| write_openai_curated_marketplace(&curated_root, &["hubspot", "granola", "test-source"]); | |
| write_plugin_app(&curated_root, "hubspot", "hubspot", hubspot_app_id); | |
| write_plugin_app(&curated_root, "granola", "granola", granola_app_id); | |
| write_plugin_app(&curated_root, "test-source", "test_source", test_app_id); | |
| let sales_marketplace_name = "oai-maintained-plugins"; | |
| let sales_marketplace_root = codex_home | |
| .path() | |
| .join(format!(".tmp/marketplaces/{sales_marketplace_name}")); | |
| write_file( | |
| &sales_marketplace_root.join(".agents/plugins/marketplace.json"), | |
| &format!( | |
| r#"{{ | |
| "name": "{sales_marketplace_name}", | |
| "plugins": [ | |
| {{"name": "sales", "source": {{"source": "local", "path": "./plugins/sales"}}}} | |
| ] | |
| }} | |
| "# | |
| ), | |
| ); | |
| write_curated_plugin(&sales_marketplace_root, "sales"); | |
| write_file( | |
| &sales_marketplace_root.join("plugins/sales/.app.json"), | |
| &format!( | |
| r#"{{ | |
| "apps": {{ | |
| "hubspot": {{ | |
| "id": "{hubspot_app_id}" | |
| }}, | |
| "granola": {{ | |
| "id": "{granola_app_id}" | |
| }} | |
| }} | |
| }} | |
| "# | |
| ), | |
| ); | |
| write_file( | |
| &codex_home.path().join(CONFIG_TOML_FILE), | |
| &format!( | |
| r#"[features] | |
| plugins = true | |
| [marketplaces.{sales_marketplace_name}] | |
| source_type = "git" | |
| source = "/tmp/{sales_marketplace_name}" | |
| "# | |
| ), | |
| ); | |
| install_marketplace_plugin(codex_home.path(), sales_marketplace_root.as_path(), "sales").await; | |
| let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &[], &[]), | |
| /*auth*/ None, | |
| ) | |
| .await; | |
| assert_eq!(discoverable_plugins, Vec::new()); | |
| } | |
| async fn cached_remote_discovery_requires_installed_cache_and_filters_candidates() { | |
| let codex_home = tempdir().expect("tempdir should succeed"); | |
| write_file( | |
| &codex_home.path().join(CONFIG_TOML_FILE), | |
| r#"[features] | |
| plugins = 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"}, | |
| "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": ["remote-unlisted-app"], | |
| "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", | |
| "interface": {"short_description": "Remote Slack short"} | |
| } | |
| }, | |
| { | |
| "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", | |
| "interface": {"short_description": "Remote Figma short"} | |
| } | |
| } | |
| ], | |
| "pagination": { | |
| "next_page_token": null | |
| } | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); | |
| let mut plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; | |
| plugins.chatgpt_base_url = format!("{}/backend-api", server.uri()); | |
| let plugins_manager = test_plugins_manager_with_options( | |
| codex_home.path().to_path_buf(), | |
| Some(Product::Codex), | |
| Some(AuthMode::Chatgpt), | |
| ); | |
| fetch_and_cache_global_remote_plugin_catalog( | |
| codex_home.path(), | |
| &RemotePluginServiceConfig::new( | |
| plugins.chatgpt_base_url.clone(), | |
| crate::test_support::test_http_client_factory(), | |
| ), | |
| Some(&auth), | |
| ) | |
| .await | |
| .expect("remote plugin catalog cache should write"); | |
| assert_eq!( | |
| list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins.clone(), &[], &[], &["remote-unlisted-app"]), | |
| Some(&auth), | |
| ) | |
| .await, | |
| Vec::new() | |
| ); | |
| Mock::given(method("GET")) | |
| .and(path("/backend-api/ps/plugins/installed")) | |
| .and(query_param_is_missing("scope")) | |
| .and(query_param_is_missing("includeDownloadUrls")) | |
| .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( | |
| &plugins, | |
| Some(&auth), | |
| &[REMOTE_GLOBAL_MARKETPLACE_NAME], | |
| /*on_effective_plugins_changed*/ None, | |
| ) | |
| .await | |
| .expect("remote installed plugin cache should write"); | |
| let expected_github = ToolSuggestDiscoverablePlugin { | |
| 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()], | |
| }; | |
| assert_eq!( | |
| list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins.clone(), &[], &[], &[]), | |
| Some(&auth), | |
| ) | |
| .await, | |
| vec![expected_github.clone()] | |
| ); | |
| let discoverable_plugins = list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins.clone(), &[], &[], &["remote-unlisted-app"]), | |
| Some(&auth), | |
| ) | |
| .await; | |
| assert_eq!( | |
| discoverable_plugins, | |
| vec![ | |
| expected_github, | |
| ToolSuggestDiscoverablePlugin { | |
| id: "remote-unlisted@openai-curated-remote".to_string(), | |
| remote_plugin_id: Some("plugins~Plugin_remote_unlisted".to_string()), | |
| name: "Remote Unlisted".to_string(), | |
| description: Some("Remote Unlisted short".to_string()), | |
| has_skills: true, | |
| mcp_server_names: Vec::new(), | |
| app_connector_ids: vec!["remote-unlisted-app".to_string()], | |
| }, | |
| ] | |
| ); | |
| assert_eq!( | |
| list_discoverable_plugins( | |
| &plugins_manager, | |
| discovery_input(plugins, &[], &["github@openai-curated-remote"], &[]), | |
| Some(&auth), | |
| ) | |
| .await, | |
| Vec::new() | |
| ); | |
| } | |
| fn discovery_input( | |
| plugins: PluginsConfigInput, | |
| configured_plugin_ids: &[&str], | |
| disabled_plugin_ids: &[&str], | |
| loaded_plugin_app_connector_ids: &[&str], | |
| ) -> ToolSuggestPluginDiscoveryInput { | |
| ToolSuggestPluginDiscoveryInput { | |
| plugins, | |
| configured_plugin_ids: string_set(configured_plugin_ids), | |
| disabled_plugin_ids: string_set(disabled_plugin_ids), | |
| loaded_plugin_app_connector_ids: string_set(loaded_plugin_app_connector_ids), | |
| } | |
| } | |
| async fn list_discoverable_plugins( | |
| plugins_manager: &PluginsManager, | |
| input: ToolSuggestPluginDiscoveryInput, | |
| auth: Option<&CodexAuth>, | |
| ) -> Vec<ToolSuggestDiscoverablePlugin> { | |
| plugins_manager | |
| .list_tool_suggest_discoverable_plugins(&input, auth) | |
| .await | |
| .expect("discoverable plugins should load") | |
| } | |
| fn string_set(values: &[&str]) -> HashSet<String> { | |
| values.iter().map(ToString::to_string).collect() | |
| } | |
| async fn install_marketplace_plugin(codex_home: &Path, marketplace_root: &Path, plugin_name: &str) { | |
| write_curated_plugin_sha_with(codex_home, TEST_CURATED_PLUGIN_SHA); | |
| let config = load_plugins_config(codex_home, marketplace_root).await; | |
| test_plugins_manager(codex_home.to_path_buf()) | |
| .install_plugin( | |
| &config, | |
| PluginInstallRequest { | |
| plugin_name: plugin_name.to_string(), | |
| marketplace_path: AbsolutePathBuf::try_from( | |
| marketplace_root.join(".agents/plugins/marketplace.json"), | |
| ) | |
| .expect("marketplace path"), | |
| }, | |
| ) | |
| .await | |
| .expect("plugin should install"); | |
| } | |
| fn write_plugin_app(root: &Path, plugin_name: &str, app_name: &str, app_id: &str) { | |
| write_file( | |
| &root.join(format!("plugins/{plugin_name}/.app.json")), | |
| &format!( | |
| r#"{{ | |
| "apps": {{ | |
| "{app_name}": {{ | |
| "id": "{app_id}" | |
| }} | |
| }} | |
| }} | |
| "# | |
| ), | |
| ); | |
| } | |