Codex Handbook
ext/mcp/src/executor_plugin/provider_tests.rs 275 lines
use super::DEFAULT_MCP_CONFIG_FILE;use super::ExecutorPluginMcpProviderError;use super::load_from_file_system;use codex_config::McpServerConfig;use codex_config::McpServerTransportConfig;use codex_exec_server::CopyOptions;use codex_exec_server::CreateDirectoryOptions;use codex_exec_server::ExecutorFileSystem;use codex_exec_server::ExecutorFileSystemFuture;use codex_exec_server::FileMetadata;use codex_exec_server::FileSystemReadStream;use codex_exec_server::FileSystemResult;use codex_exec_server::FileSystemSandboxContext;use codex_exec_server::ReadDirectoryEntry;use codex_exec_server::RemoveOptions;use codex_plugin::ResolvedPlugin;use codex_plugin::manifest::PluginManifest;use codex_plugin::manifest::PluginManifestPaths;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_path_uri::PathUri;use pretty_assertions::assert_eq;use std::collections::HashMap;use std::io;use std::sync::Mutex;const MCP_CONFIG_CONTENTS: &str = r#"{  "mcpServers": {    "demo": {"command": "demo-mcp", "environment_id": "local"},    "hosted": {"url": "https://example.com/mcp"}  }}"#;struct SyntheticExecutorFileSystem {    config_path: AbsolutePathBuf,    config_contents: Option<&'static str>,    reads: Mutex<Vec<AbsolutePathBuf>>,}impl SyntheticExecutorFileSystem {    fn unsupported<T>() -> FileSystemResult<T> {        Err(io::Error::new(            io::ErrorKind::Unsupported,            "operation is not used by executor MCP provider tests",        ))    }}impl ExecutorFileSystem for SyntheticExecutorFileSystem {    fn canonicalize<'a>(        &'a self,        _path: &'a PathUri,        _sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, PathUri> {        Box::pin(async { Self::unsupported() })    }    fn read_file<'a>(        &'a self,        path: &'a PathUri,        _sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, Vec<u8>> {        Box::pin(async move {            let path = path.to_abs_path()?;            self.reads                .lock()                .unwrap_or_else(std::sync::PoisonError::into_inner)                .push(path.clone());            if path != self.config_path {                return Err(io::Error::new(io::ErrorKind::NotFound, "not found"));            }            self.config_contents                .map(|contents| contents.as_bytes().to_vec())                .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "not found"))        })    }    fn read_file_stream<'a>(        &'a self,        _path: &'a PathUri,        _sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, FileSystemReadStream> {        Box::pin(async { Self::unsupported() })    }    fn write_file<'a>(        &'a self,        _path: &'a PathUri,        _contents: Vec<u8>,        _sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, ()> {        Box::pin(async { Self::unsupported() })    }    fn create_directory<'a>(        &'a self,        _path: &'a PathUri,        _options: CreateDirectoryOptions,        _sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, ()> {        Box::pin(async { Self::unsupported() })    }    fn get_metadata<'a>(        &'a self,        _path: &'a PathUri,        _sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, FileMetadata> {        Box::pin(async { Self::unsupported() })    }    fn read_directory<'a>(        &'a self,        _path: &'a PathUri,        _sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>> {        Box::pin(async { Self::unsupported() })    }    fn remove<'a>(        &'a self,        _path: &'a PathUri,        _options: RemoveOptions,        _sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, ()> {        Box::pin(async { Self::unsupported() })    }    fn copy<'a>(        &'a self,        _source_path: &'a PathUri,        _destination_path: &'a PathUri,        _options: CopyOptions,        _sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, ()> {        Box::pin(async { Self::unsupported() })    }}#[tokio::test]async fn reads_declared_config_only_through_executor_file_system() {    let temp_dir = tempfile::tempdir().expect("tempdir");    let plugin_root =        AbsolutePathBuf::from_absolute_path_checked(temp_dir.path().join("executor-only-plugin"))            .expect("absolute plugin root");    assert!(!plugin_root.as_path().exists());    let config_path = plugin_root.join("config/mcp.json");    let plugin = resolved_plugin(&plugin_root, Some(config_path.clone()));    let file_system = SyntheticExecutorFileSystem {        config_path: config_path.clone(),        config_contents: Some(MCP_CONFIG_CONTENTS),        reads: Mutex::new(Vec::new()),    };    let servers = load_from_file_system(&plugin, &plugin_root, &file_system)        .await        .expect("load executor MCP config");    assert_eq!(        servers,        vec![(            "demo".to_string(),            McpServerConfig {                transport: McpServerTransportConfig::Stdio {                    command: "demo-mcp".to_string(),                    args: Vec::new(),                    env: None,                    env_vars: Vec::new(),                    cwd: Some(plugin_root.to_path_buf()),                },                environment_id: "executor-test".to_string(),                enabled: true,                required: false,                supports_parallel_tool_calls: false,                disabled_reason: None,                startup_timeout_sec: None,                tool_timeout_sec: None,                default_tools_approval_mode: None,                enabled_tools: None,                disabled_tools: None,                scopes: None,                oauth: None,                oauth_resource: None,                tools: HashMap::new(),            },        )]    );    assert_eq!(reads(&file_system), vec![config_path]);}#[tokio::test]async fn missing_default_config_is_empty() {    let temp_dir = tempfile::tempdir().expect("tempdir");    let plugin_root = AbsolutePathBuf::from_absolute_path_checked(temp_dir.path().join("plugin"))        .expect("absolute plugin root");    let config_path = plugin_root.join(DEFAULT_MCP_CONFIG_FILE);    let plugin = resolved_plugin(&plugin_root, /*mcp_servers*/ None);    let file_system = SyntheticExecutorFileSystem {        config_path: config_path.clone(),        config_contents: None,        reads: Mutex::new(Vec::new()),    };    let servers = load_from_file_system(&plugin, &plugin_root, &file_system)        .await        .expect("missing default config should be ignored");    assert_eq!(servers, Vec::new());    assert_eq!(reads(&file_system), vec![config_path]);}#[tokio::test]async fn malformed_declared_config_is_an_error() {    let temp_dir = tempfile::tempdir().expect("tempdir");    let plugin_root = AbsolutePathBuf::from_absolute_path_checked(temp_dir.path().join("plugin"))        .expect("absolute plugin root");    let config_path = plugin_root.join("mcp.json");    let plugin = resolved_plugin(&plugin_root, Some(config_path.clone()));    let file_system = SyntheticExecutorFileSystem {        config_path: config_path.clone(),        config_contents: Some("{not-json"),        reads: Mutex::new(Vec::new()),    };    let err = load_from_file_system(&plugin, &plugin_root, &file_system)        .await        .expect_err("malformed declared config should fail");    let ExecutorPluginMcpProviderError::ParseConfig {        plugin_id,        path,        source: _,    } = err    else {        panic!("expected parse error");    };    assert_eq!(        (plugin_id, path),        ("selected-root".to_string(), config_path.clone())    );    assert_eq!(reads(&file_system), vec![config_path]);}fn resolved_plugin(    plugin_root: &AbsolutePathBuf,    mcp_servers: Option<AbsolutePathBuf>,) -> ResolvedPlugin {    ResolvedPlugin::from_environment(        "selected-root".to_string(),        "executor-test".to_string(),        plugin_root.clone(),        plugin_root.join(".codex-plugin/plugin.json"),        PluginManifest {            name: "demo-plugin".to_string(),            version: None,            description: None,            keywords: Vec::new(),            paths: PluginManifestPaths {                skills: None,                mcp_servers,                apps: None,                hooks: None,            },            interface: None,        },    )    .expect("valid plugin descriptor")}fn reads(file_system: &SyntheticExecutorFileSystem) -> Vec<AbsolutePathBuf> {    file_system        .reads        .lock()        .unwrap_or_else(std::sync::PoisonError::into_inner)        .clone()}