mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
- [x] Add resource_uri and other things to _meta to shortcut resource lookup and speed things up.
264 lines
8.5 KiB
Rust
264 lines
8.5 KiB
Rust
use super::*;
|
|
use crate::config::CONFIG_TOML_FILE;
|
|
use crate::config::ConfigBuilder;
|
|
use crate::features::Feature;
|
|
use crate::plugins::AppConnectorId;
|
|
use crate::plugins::PluginCapabilitySummary;
|
|
use pretty_assertions::assert_eq;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use toml::Value;
|
|
|
|
fn write_file(path: &Path, contents: &str) {
|
|
fs::create_dir_all(path.parent().expect("file should have a parent")).unwrap();
|
|
fs::write(path, contents).unwrap();
|
|
}
|
|
|
|
fn plugin_config_toml() -> String {
|
|
let mut root = toml::map::Map::new();
|
|
|
|
let mut features = toml::map::Map::new();
|
|
features.insert("plugins".to_string(), Value::Boolean(true));
|
|
root.insert("features".to_string(), Value::Table(features));
|
|
|
|
let mut plugin = toml::map::Map::new();
|
|
plugin.insert("enabled".to_string(), Value::Boolean(true));
|
|
|
|
let mut plugins = toml::map::Map::new();
|
|
plugins.insert("sample@test".to_string(), Value::Table(plugin));
|
|
root.insert("plugins".to_string(), Value::Table(plugins));
|
|
|
|
toml::to_string(&Value::Table(root)).expect("plugin test config should serialize")
|
|
}
|
|
|
|
fn make_tool(name: &str) -> Tool {
|
|
Tool {
|
|
name: name.to_string(),
|
|
title: None,
|
|
description: None,
|
|
input_schema: serde_json::json!({"type": "object", "properties": {}}),
|
|
output_schema: None,
|
|
annotations: None,
|
|
icons: None,
|
|
meta: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn split_qualified_tool_name_returns_server_and_tool() {
|
|
assert_eq!(
|
|
split_qualified_tool_name("mcp__alpha__do_thing"),
|
|
Some(("alpha".to_string(), "do_thing".to_string()))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn split_qualified_tool_name_rejects_invalid_names() {
|
|
assert_eq!(split_qualified_tool_name("other__alpha__do_thing"), None);
|
|
assert_eq!(split_qualified_tool_name("mcp__alpha__"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn group_tools_by_server_strips_prefix_and_groups() {
|
|
let mut tools = HashMap::new();
|
|
tools.insert("mcp__alpha__do_thing".to_string(), make_tool("do_thing"));
|
|
tools.insert(
|
|
"mcp__alpha__nested__op".to_string(),
|
|
make_tool("nested__op"),
|
|
);
|
|
tools.insert("mcp__beta__do_other".to_string(), make_tool("do_other"));
|
|
|
|
let mut expected_alpha = HashMap::new();
|
|
expected_alpha.insert("do_thing".to_string(), make_tool("do_thing"));
|
|
expected_alpha.insert("nested__op".to_string(), make_tool("nested__op"));
|
|
|
|
let mut expected_beta = HashMap::new();
|
|
expected_beta.insert("do_other".to_string(), make_tool("do_other"));
|
|
|
|
let mut expected = HashMap::new();
|
|
expected.insert("alpha".to_string(), expected_alpha);
|
|
expected.insert("beta".to_string(), expected_beta);
|
|
|
|
assert_eq!(group_tools_by_server(&tools), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn tool_plugin_provenance_collects_app_and_mcp_sources() {
|
|
let provenance = ToolPluginProvenance::from_capability_summaries(&[
|
|
PluginCapabilitySummary {
|
|
display_name: "alpha-plugin".to_string(),
|
|
app_connector_ids: vec![AppConnectorId("connector_example".to_string())],
|
|
mcp_server_names: vec!["alpha".to_string()],
|
|
..PluginCapabilitySummary::default()
|
|
},
|
|
PluginCapabilitySummary {
|
|
display_name: "beta-plugin".to_string(),
|
|
app_connector_ids: vec![
|
|
AppConnectorId("connector_example".to_string()),
|
|
AppConnectorId("connector_gmail".to_string()),
|
|
],
|
|
mcp_server_names: vec!["beta".to_string()],
|
|
..PluginCapabilitySummary::default()
|
|
},
|
|
]);
|
|
|
|
assert_eq!(
|
|
provenance,
|
|
ToolPluginProvenance {
|
|
plugin_display_names_by_connector_id: HashMap::from([
|
|
(
|
|
"connector_example".to_string(),
|
|
vec!["alpha-plugin".to_string(), "beta-plugin".to_string()],
|
|
),
|
|
(
|
|
"connector_gmail".to_string(),
|
|
vec!["beta-plugin".to_string()],
|
|
),
|
|
]),
|
|
plugin_display_names_by_mcp_server_name: HashMap::from([
|
|
("alpha".to_string(), vec!["alpha-plugin".to_string()]),
|
|
("beta".to_string(), vec!["beta-plugin".to_string()]),
|
|
]),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn codex_apps_mcp_url_for_base_url_keeps_existing_paths() {
|
|
assert_eq!(
|
|
codex_apps_mcp_url_for_base_url("https://chatgpt.com/backend-api"),
|
|
"https://chatgpt.com/backend-api/wham/apps"
|
|
);
|
|
assert_eq!(
|
|
codex_apps_mcp_url_for_base_url("https://chat.openai.com"),
|
|
"https://chat.openai.com/backend-api/wham/apps"
|
|
);
|
|
assert_eq!(
|
|
codex_apps_mcp_url_for_base_url("http://localhost:8080/api/codex"),
|
|
"http://localhost:8080/api/codex/apps"
|
|
);
|
|
assert_eq!(
|
|
codex_apps_mcp_url_for_base_url("http://localhost:8080"),
|
|
"http://localhost:8080/api/codex/apps"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn codex_apps_mcp_url_uses_legacy_codex_apps_path() {
|
|
let mut config = crate::config::test_config();
|
|
config.chatgpt_base_url = "https://chatgpt.com".to_string();
|
|
|
|
assert_eq!(
|
|
codex_apps_mcp_url(&config),
|
|
"https://chatgpt.com/backend-api/wham/apps"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn codex_apps_server_config_uses_legacy_codex_apps_path() {
|
|
let mut config = crate::config::test_config();
|
|
config.chatgpt_base_url = "https://chatgpt.com".to_string();
|
|
|
|
let mut servers = with_codex_apps_mcp(HashMap::new(), false, None, &config);
|
|
assert!(!servers.contains_key(CODEX_APPS_MCP_SERVER_NAME));
|
|
|
|
config
|
|
.features
|
|
.enable(Feature::Apps)
|
|
.expect("test config should allow apps");
|
|
|
|
servers = with_codex_apps_mcp(servers, true, None, &config);
|
|
let server = servers
|
|
.get(CODEX_APPS_MCP_SERVER_NAME)
|
|
.expect("codex apps should be present when apps is enabled");
|
|
let url = match &server.transport {
|
|
McpServerTransportConfig::StreamableHttp { url, .. } => url,
|
|
_ => panic!("expected streamable http transport for codex apps"),
|
|
};
|
|
|
|
assert_eq!(url, "https://chatgpt.com/backend-api/wham/apps");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn effective_mcp_servers_include_plugins_without_overriding_user_config() {
|
|
let codex_home = tempfile::tempdir().expect("tempdir");
|
|
let plugin_root = codex_home
|
|
.path()
|
|
.join("plugins/cache")
|
|
.join("test/sample/local");
|
|
write_file(
|
|
&plugin_root.join(".codex-plugin/plugin.json"),
|
|
r#"{"name":"sample"}"#,
|
|
);
|
|
write_file(
|
|
&plugin_root.join(".mcp.json"),
|
|
r#"{
|
|
"mcpServers": {
|
|
"sample": {
|
|
"type": "http",
|
|
"url": "https://plugin.example/mcp"
|
|
},
|
|
"docs": {
|
|
"type": "http",
|
|
"url": "https://docs.example/mcp"
|
|
}
|
|
}
|
|
}"#,
|
|
);
|
|
write_file(
|
|
&codex_home.path().join(CONFIG_TOML_FILE),
|
|
&plugin_config_toml(),
|
|
);
|
|
|
|
let mut config = ConfigBuilder::default()
|
|
.codex_home(codex_home.path().to_path_buf())
|
|
.build()
|
|
.await
|
|
.expect("config should load");
|
|
|
|
let mut configured_servers = config.mcp_servers.get().clone();
|
|
configured_servers.insert(
|
|
"sample".to_string(),
|
|
McpServerConfig {
|
|
transport: McpServerTransportConfig::StreamableHttp {
|
|
url: "https://user.example/mcp".to_string(),
|
|
bearer_token_env_var: None,
|
|
http_headers: None,
|
|
env_http_headers: None,
|
|
},
|
|
enabled: true,
|
|
required: false,
|
|
disabled_reason: None,
|
|
startup_timeout_sec: None,
|
|
tool_timeout_sec: None,
|
|
enabled_tools: None,
|
|
disabled_tools: None,
|
|
scopes: None,
|
|
oauth_resource: None,
|
|
},
|
|
);
|
|
config
|
|
.mcp_servers
|
|
.set(configured_servers)
|
|
.expect("test config should accept MCP servers");
|
|
|
|
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone())));
|
|
let effective = mcp_manager.effective_servers(&config, None);
|
|
|
|
let sample = effective.get("sample").expect("user server should exist");
|
|
let docs = effective.get("docs").expect("plugin server should exist");
|
|
|
|
match &sample.transport {
|
|
McpServerTransportConfig::StreamableHttp { url, .. } => {
|
|
assert_eq!(url, "https://user.example/mcp");
|
|
}
|
|
other => panic!("expected streamable http transport, got {other:?}"),
|
|
}
|
|
match &docs.transport {
|
|
McpServerTransportConfig::StreamableHttp { url, .. } => {
|
|
assert_eq!(url, "https://docs.example/mcp");
|
|
}
|
|
other => panic!("expected streamable http transport, got {other:?}"),
|
|
}
|
|
}
|