Codex Handbook
core/src/connectors_tests.rs 546 lines
use super::*;use crate::config::CONFIG_TOML_FILE;use crate::config::ConfigBuilder;use codex_config::AppRequirementToml;use codex_config::AppsRequirementsToml;use codex_config::ConfigLayerStack;use codex_config::ConfigRequirements;use codex_config::ConfigRequirementsToml;use codex_config::test_support::CloudConfigBundleFixture;use codex_config::types::ApprovalsReviewer;use codex_connectors::merge::plugin_connector_to_app_info;use codex_connectors::metadata::connector_install_url;use codex_connectors::metadata::sanitize_name;use codex_features::Feature;use codex_login::CodexAuth;use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;use codex_mcp::ToolInfo;use pretty_assertions::assert_eq;use rmcp::model::JsonObject;use rmcp::model::Tool;use std::collections::BTreeMap;use std::collections::HashSet;use std::sync::Arc;use tempfile::tempdir;fn app(id: &str) -> AppInfo {    AppInfo {        id: id.to_string(),        name: id.to_string(),        description: None,        logo_url: None,        logo_url_dark: None,        distribution_channel: None,        install_url: None,        branding: None,        app_metadata: None,        labels: None,        is_accessible: false,        is_enabled: true,        plugin_display_names: Vec::new(),    }}fn plugin_names(names: &[&str]) -> Vec<String> {    names.iter().map(ToString::to_string).collect()}fn test_tool_definition(tool_name: &str) -> Tool {    Tool::new_with_raw(tool_name.to_string(), None, Arc::new(JsonObject::default()))}fn codex_app_tool(    tool_name: &str,    connector_id: &str,    connector_name: Option<&str>,    plugin_display_names: &[&str],) -> ToolInfo {    let tool_namespace = connector_name        .map(sanitize_name)        .map(|connector_name| format!("mcp__{CODEX_APPS_MCP_SERVER_NAME}__{connector_name}"))        .unwrap_or_else(|| CODEX_APPS_MCP_SERVER_NAME.to_string());    ToolInfo {        server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),        supports_parallel_tool_calls: false,        server_origin: None,        callable_name: tool_name.to_string(),        callable_namespace: tool_namespace,        namespace_description: None,        tool: test_tool_definition(tool_name),        connector_id: Some(connector_id.to_string()),        connector_name: connector_name.map(ToOwned::to_owned),        plugin_display_names: plugin_names(plugin_display_names),    }}fn with_accessible_connectors_cache_cleared<R>(f: impl FnOnce() -> R) -> R {    let previous = {        let mut cache_guard = ACCESSIBLE_CONNECTORS_CACHE            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner);        cache_guard.take()    };    let result = f();    let mut cache_guard = ACCESSIBLE_CONNECTORS_CACHE        .lock()        .unwrap_or_else(std::sync::PoisonError::into_inner);    *cache_guard = previous;    result}#[test]fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {    let tools = vec![        codex_app_tool(            "calendar_list_events",            "calendar",            /*connector_name*/ None,            &["sample", "sample"],        ),        codex_app_tool(            "calendar_create_event",            "calendar",            Some("Google Calendar"),            &["beta", "sample"],        ),        ToolInfo {            server_name: "sample".to_string(),            supports_parallel_tool_calls: false,            server_origin: None,            callable_name: "echo".to_string(),            callable_namespace: "sample".to_string(),            namespace_description: None,            tool: test_tool_definition("echo"),            connector_id: None,            connector_name: None,            plugin_display_names: plugin_names(&["ignored"]),        },    ];    let connectors = accessible_connectors_from_mcp_tools(&tools);    assert_eq!(        connectors,        vec![AppInfo {            id: "calendar".to_string(),            name: "Google Calendar".to_string(),            description: None,            logo_url: None,            logo_url_dark: None,            distribution_channel: None,            install_url: Some(connector_install_url("Google Calendar", "calendar")),            branding: None,            app_metadata: None,            labels: None,            is_accessible: true,            is_enabled: true,            plugin_display_names: plugin_names(&["beta", "sample"]),        }]    );}#[tokio::test]async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_installed_apps() {    let codex_home = tempdir().expect("tempdir should succeed");    let mut config = ConfigBuilder::default()        .codex_home(codex_home.path().to_path_buf())        .build()        .await        .expect("config should load");    let _ = config.features.set_enabled(Feature::Apps, /*enabled*/ true);    let cache_key = accessible_connectors_cache_key(&config, /*auth*/ None);    let tools = vec![        codex_app_tool(            "calendar_list_events",            "calendar",            Some("Google Calendar"),            &["calendar-plugin"],        ),        codex_app_tool(            "openai_hidden",            "connector_openai_hidden",            Some("Hidden"),            &[],        ),    ];    let cached = with_accessible_connectors_cache_cleared(|| {        refresh_accessible_connectors_cache_from_mcp_tools(&config, /*auth*/ None, &tools);        read_cached_accessible_connectors(&cache_key).expect("cache should be populated")    });    assert_eq!(        cached,        vec![            AppInfo {                id: "calendar".to_string(),                name: "Google Calendar".to_string(),                description: None,                logo_url: None,                logo_url_dark: None,                distribution_channel: None,                install_url: Some(connector_install_url("Google Calendar", "calendar")),                branding: None,                app_metadata: None,                labels: None,                is_accessible: true,                is_enabled: true,                plugin_display_names: plugin_names(&["calendar-plugin"]),            },            AppInfo {                id: "connector_openai_hidden".to_string(),                name: "Hidden".to_string(),                description: None,                logo_url: None,                logo_url_dark: None,                distribution_channel: None,                install_url: Some(connector_install_url("Hidden", "connector_openai_hidden")),                branding: None,                app_metadata: None,                labels: None,                is_accessible: true,                is_enabled: true,                plugin_display_names: Vec::new(),            }        ]    );}#[test]fn accessible_connectors_from_mcp_tools_preserves_description() {    let mcp_tools = vec![ToolInfo {        server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),        supports_parallel_tool_calls: false,        server_origin: None,        callable_name: "calendar_create_event".to_string(),        callable_namespace: "mcp__codex_apps__calendar".to_string(),        namespace_description: Some("Plan events".to_string()),        tool: Tool::new(            "calendar_create_event",            "Create a calendar event",            Arc::new(JsonObject::default()),        ),        connector_id: Some("calendar".to_string()),        connector_name: Some("Calendar".to_string()),        plugin_display_names: Vec::new(),    }];    assert_eq!(        accessible_connectors_from_mcp_tools(&mcp_tools),        vec![AppInfo {            id: "calendar".to_string(),            name: "Calendar".to_string(),            description: Some("Plan events".to_string()),            logo_url: None,            logo_url_dark: None,            distribution_channel: None,            branding: None,            app_metadata: None,            labels: None,            install_url: Some(connector_install_url("Calendar", "calendar")),            is_accessible: true,            is_enabled: true,            plugin_display_names: Vec::new(),        }]    );}#[tokio::test]async fn app_approvals_reviewer_uses_app_then_default_then_global() {    for (global, app_default, app, expected_global, expected_default, expected_app) in [        (            "user",            "auto_review",            "user",            ApprovalsReviewer::User,            ApprovalsReviewer::AutoReview,            ApprovalsReviewer::User,        ),        (            "auto_review",            "user",            "auto_review",            ApprovalsReviewer::AutoReview,            ApprovalsReviewer::User,            ApprovalsReviewer::AutoReview,        ),    ] {        let codex_home = tempdir().expect("tempdir should succeed");        std::fs::write(            codex_home.path().join(CONFIG_TOML_FILE),            format!(                r#"approvals_reviewer = "{global}"[apps._default]approvals_reviewer = "{app_default}"[apps.calendar]approvals_reviewer = "{app}""#            ),        )        .expect("write config");        let config = ConfigBuilder::default()            .codex_home(codex_home.path().to_path_buf())            .build()            .await            .expect("config should build");        assert_eq!(            mcp_approvals_reviewer(&config, CODEX_APPS_MCP_SERVER_NAME, Some("calendar")),            expected_app        );        assert_eq!(            mcp_approvals_reviewer(&config, CODEX_APPS_MCP_SERVER_NAME, Some("drive")),            expected_default        );        assert_eq!(            mcp_approvals_reviewer(                &config,                CODEX_APPS_MCP_SERVER_NAME,                /*connector_id*/ None            ),            expected_default        );        assert_eq!(            mcp_approvals_reviewer(&config, "custom_server", Some("calendar")),            expected_global        );    }}#[tokio::test]async fn default_app_approvals_reviewer_respects_global_reviewer_requirements() {    let codex_home = tempdir().expect("tempdir should succeed");    std::fs::write(        codex_home.path().join(CONFIG_TOML_FILE),        r#"approvals_reviewer = "auto_review"[apps._default]approvals_reviewer = "user""#,    )    .expect("write config");    let config = ConfigBuilder::default()        .codex_home(codex_home.path().to_path_buf())        .cloud_config_bundle(            CloudConfigBundleFixture::loader_with_enterprise_requirement(                r#"allowed_approvals_reviewers = ["auto_review"]"#,            ),        )        .build()        .await        .expect("config should build");    assert_eq!(        mcp_approvals_reviewer(&config, CODEX_APPS_MCP_SERVER_NAME, Some("calendar")),        ApprovalsReviewer::AutoReview    );}#[tokio::test]async fn app_approvals_reviewer_respects_global_reviewer_requirements() {    let codex_home = tempdir().expect("tempdir should succeed");    std::fs::write(        codex_home.path().join(CONFIG_TOML_FILE),        r#"approvals_reviewer = "auto_review"[apps.calendar]approvals_reviewer = "user""#,    )    .expect("write config");    let config = ConfigBuilder::default()        .codex_home(codex_home.path().to_path_buf())        .cloud_config_bundle(            CloudConfigBundleFixture::loader_with_enterprise_requirement(                r#"allowed_approvals_reviewers = ["auto_review"]"#,            ),        )        .build()        .await        .expect("config should build");    assert_eq!(        mcp_approvals_reviewer(&config, CODEX_APPS_MCP_SERVER_NAME, Some("calendar")),        ApprovalsReviewer::AutoReview    );}#[tokio::test]async fn with_app_enabled_state_preserves_unrelated_disabled_connector() {    let codex_home = tempdir().expect("tempdir should succeed");    let mut config = ConfigBuilder::default()        .codex_home(codex_home.path().to_path_buf())        .fallback_cwd(Some(codex_home.path().to_path_buf()))        .build()        .await        .expect("config should build");    let requirements = ConfigRequirementsToml {        apps: Some(AppsRequirementsToml {            apps: BTreeMap::from([(                "connector_drive".to_string(),                AppRequirementToml {                    enabled: Some(false),                    tools: None,                },            )]),        }),        ..Default::default()    };    config.config_layer_stack =        ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements)            .expect("requirements stack");    let mut slack = app("connector_slack");    slack.is_enabled = false;    let mut drive = app("connector_drive");    drive.is_enabled = false;    assert_eq!(        with_app_enabled_state(vec![slack.clone(), app("connector_drive")], &config),        vec![slack, drive]    );}#[tokio::test]async fn tool_suggest_connector_ids_include_configured_tool_suggest_discoverables() {    let codex_home = tempdir().expect("tempdir should succeed");    std::fs::write(        codex_home.path().join(CONFIG_TOML_FILE),        r#"[tool_suggest]discoverables = [  { type = "connector", id = "connector_2128aebfecb84f64a069897515042a44" },  { type = "plugin", id = "slack@openai-curated" },  { type = "connector", id = "   " }]"#,    )    .expect("write config");    let config = ConfigBuilder::default()        .codex_home(codex_home.path().to_path_buf())        .build()        .await        .expect("config should load");    assert_eq!(        tool_suggest_connector_ids(&config, &[]),        HashSet::from(["connector_2128aebfecb84f64a069897515042a44".to_string()])    );}#[tokio::test]async fn tool_suggest_connector_ids_exclude_disabled_tool_suggestions() {    let codex_home = tempdir().expect("tempdir should succeed");    std::fs::write(        codex_home.path().join(CONFIG_TOML_FILE),        r#"[tool_suggest]discoverables = [  { type = "connector", id = "connector_calendar" },  { type = "connector", id = "connector_gmail" }]disabled_tools = [  { type = "connector", id = "connector_calendar" }]"#,    )    .expect("write config");    let config = ConfigBuilder::default()        .codex_home(codex_home.path().to_path_buf())        .build()        .await        .expect("config should load");    assert_eq!(        tool_suggest_connector_ids(&config, &[]),        HashSet::from(["connector_gmail".to_string()])    );}#[tokio::test]async fn tool_suggest_uses_connector_id_fallback_when_directory_cache_is_empty() {    let codex_home = tempdir().expect("tempdir should succeed");    std::fs::write(        codex_home.path().join(CONFIG_TOML_FILE),        r#"[features]apps = true[tool_suggest]discoverables = [  { type = "connector", id = "connector_gmail" }]"#,    )    .expect("write config");    let config = ConfigBuilder::default()        .codex_home(codex_home.path().to_path_buf())        .build()        .await        .expect("config should load");    let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();    let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());    let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth(        &config,        &plugins_manager,        Some(&auth),        &[],        &[],    )    .await    .expect("discoverable tools should load");    assert_eq!(        discoverable_tools,        vec![DiscoverableTool::from(plugin_connector_to_app_info(            "connector_gmail".to_string(),        ))]    );}#[tokio::test]async fn tool_suggest_includes_connectors_from_loaded_plugin_apps() {    let codex_home = tempdir().expect("tempdir should succeed");    std::fs::write(        codex_home.path().join(CONFIG_TOML_FILE),        r#"[features]apps = true"#,    )    .expect("write config");    let config = ConfigBuilder::default()        .codex_home(codex_home.path().to_path_buf())        .build()        .await        .expect("config should load");    let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();    let loaded_plugin_app_connector_ids = vec!["asdk_app_databricks_workspace".to_string()];    let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());    let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth(        &config,        &plugins_manager,        Some(&auth),        &[],        &loaded_plugin_app_connector_ids,    )    .await    .expect("discoverable tools should load");    assert_eq!(        discoverable_tools,        vec![DiscoverableTool::from(plugin_connector_to_app_info(            "asdk_app_databricks_workspace".to_string(),        ))]    );}