Files
1jehuang--jcode/crates/jcode-base/src/mcp/protocol_tests.rs
T
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

324 lines
11 KiB
Rust

use super::*;
#[test]
fn load_for_dir_without_project_does_not_load_process_cwd_config() {
let _guard = crate::storage::lock_test_env();
let original_cwd = std::env::current_dir().expect("current cwd");
let previous_home = std::env::var_os("JCODE_HOME");
let home = tempfile::tempdir().expect("home tempdir");
let project = tempfile::tempdir().expect("project tempdir");
crate::env::set_var("JCODE_HOME", home.path());
std::env::set_current_dir(project.path()).expect("set project cwd");
std::fs::write(
project.path().join(".mcp.json"),
r#"{"mcpServers":{"cwd-only":{"command":"cwd-server"}}}"#,
)
.expect("write project MCP config");
let result = std::panic::catch_unwind(|| {
let unbound = McpConfig::load_for_dir(None);
assert!(!unbound.servers.contains_key("cwd-only"));
let bound = McpConfig::load_for_dir(Some(project.path()));
assert!(bound.servers.contains_key("cwd-only"));
});
std::env::set_current_dir(original_cwd).expect("restore cwd");
if let Some(previous_home) = previous_home {
crate::env::set_var("JCODE_HOME", previous_home);
} else {
crate::env::remove_var("JCODE_HOME");
}
result.expect("MCP cwd isolation assertions");
}
#[test]
fn test_json_rpc_request_serialization() {
let request = JsonRpcRequest::new(1, "tools/list", None);
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"jsonrpc\":\"2.0\""));
assert!(json.contains("\"id\":1"));
assert!(json.contains("\"method\":\"tools/list\""));
}
#[test]
fn test_json_rpc_response_deserialization() {
let json = r#"{"jsonrpc":"2.0","id":1,"result":{"tools":[]}}"#;
let response: JsonRpcResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.id, Some(1));
assert!(response.result.is_some());
assert!(response.error.is_none());
}
#[test]
fn test_json_rpc_error_response() {
let json = r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid Request"}}"#;
let response: JsonRpcResponse = serde_json::from_str(json).unwrap();
assert!(response.error.is_some());
let err = response.error.unwrap();
assert_eq!(err.code, -32600);
assert_eq!(err.message, "Invalid Request");
}
#[test]
fn test_mcp_config_deserialization() {
let json = r#"{
"servers": {
"test-server": {
"command": "/usr/bin/test-mcp",
"args": ["--port", "8080"],
"env": {"API_KEY": "secret"}
}
}
}"#;
let config: McpConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.servers.len(), 1);
let server = config.servers.get("test-server").unwrap();
assert_eq!(server.command, "/usr/bin/test-mcp");
assert_eq!(server.args, vec!["--port", "8080"]);
assert_eq!(server.env.get("API_KEY"), Some(&"secret".to_string()));
}
#[test]
fn test_mcp_config_empty() {
let json = r#"{}"#;
let config: McpConfig = serde_json::from_str(json).unwrap();
assert!(config.servers.is_empty());
}
#[test]
fn test_mcp_config_accepts_claude_mcp_servers_key() {
// Claude Code uses `mcpServers`, not `servers`.
let json = r#"{
"mcpServers": {
"claude-server": {
"command": "npx",
"args": ["-y", "some-mcp"]
}
}
}"#;
let config: McpConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.servers.len(), 1);
let server = config.servers.get("claude-server").unwrap();
assert_eq!(server.command, "npx");
assert!(server.is_stdio());
}
#[test]
fn test_mcp_http_server_is_not_stdio() {
let json = r#"{
"mcpServers": {
"remote": {
"type": "http",
"url": "https://example.com/mcp"
}
}
}"#;
let config: McpConfig = serde_json::from_str(json).unwrap();
let server = config.servers.get("remote").unwrap();
assert!(!server.is_stdio());
assert_eq!(server.url.as_deref(), Some("https://example.com/mcp"));
}
#[test]
fn test_load_claude_json_global_and_project_servers() {
let temp = tempfile::tempdir().expect("tempdir");
let cwd = temp.path().join("myproject");
std::fs::create_dir_all(&cwd).unwrap();
let claude_json = temp.path().join(".claude.json");
let body = serde_json::json!({
"mcpServers": {
"global-srv": { "command": "global-bin" }
},
"projects": {
cwd.to_string_lossy(): {
"mcpServers": {
"project-srv": { "command": "project-bin", "args": ["--flag"] }
}
}
}
});
std::fs::write(&claude_json, serde_json::to_string_pretty(&body).unwrap()).unwrap();
let config = McpConfig::load_claude_json(&claude_json, Some(&cwd));
assert_eq!(config.servers.len(), 2);
assert_eq!(
config.servers.get("global-srv").unwrap().command,
"global-bin"
);
assert_eq!(
config.servers.get("project-srv").unwrap().command,
"project-bin"
);
}
#[test]
fn test_load_project_locals_resolves_against_given_dir_not_cwd() {
// Issue #420: remote/client sessions must load project-local MCP config
// from the session working directory, not the server process cwd.
let temp = tempfile::tempdir().expect("tempdir");
let project = temp.path().join("client-project");
std::fs::create_dir_all(&project).unwrap();
std::fs::write(
project.join(".mcp.json"),
r#"{"mcpServers":{"example-server":{"command":"npx","args":["-y","some-mcp-server"]}}}"#,
)
.unwrap();
// The process cwd (this repo) is unrelated to `project`; resolution must
// come from the explicit path.
let config = McpConfig::load_project_locals(&project);
assert_eq!(config.servers.len(), 1);
let server = config.servers.get("example-server").unwrap();
assert_eq!(server.command, "npx");
assert!(server.is_stdio());
// A directory with no project-local config yields nothing.
let empty = temp.path().join("empty-project");
std::fs::create_dir_all(&empty).unwrap();
assert!(McpConfig::load_project_locals(&empty).servers.is_empty());
}
#[test]
fn test_load_project_locals_merge_order() {
// `.jcode/mcp.json` loads first, then `.mcp.json` overrides same-named
// servers, then `.claude/mcp.json`.
let temp = tempfile::tempdir().expect("tempdir");
let project = temp.path();
std::fs::create_dir_all(project.join(".jcode")).unwrap();
std::fs::create_dir_all(project.join(".claude")).unwrap();
std::fs::write(
project.join(".jcode/mcp.json"),
r#"{"servers":{"shared-name":{"command":"jcode-bin"},"jcode-only":{"command":"a"}}}"#,
)
.unwrap();
std::fs::write(
project.join(".mcp.json"),
r#"{"mcpServers":{"shared-name":{"command":"claude-bin"}}}"#,
)
.unwrap();
std::fs::write(
project.join(".claude/mcp.json"),
r#"{"mcpServers":{"legacy-only":{"command":"c"}}}"#,
)
.unwrap();
let config = McpConfig::load_project_locals(project);
assert_eq!(config.servers.len(), 3);
assert_eq!(
config.servers.get("shared-name").unwrap().command,
"claude-bin",
".mcp.json must override .jcode/mcp.json for same-named servers"
);
assert!(config.servers.contains_key("jcode-only"));
assert!(config.servers.contains_key("legacy-only"));
}
#[test]
fn test_server_enabled_defaults_true() {
// Existing configs without the flag keep current behavior (issue #436).
let json = r#"{"servers":{"srv":{"command":"bin"}}}"#;
let config: McpConfig = serde_json::from_str(json).unwrap();
assert!(config.servers.get("srv").unwrap().is_enabled());
}
#[test]
fn test_server_enabled_false_opencode_style() {
let json = r#"{"servers":{"srv":{"command":"bin","enabled":false}}}"#;
let config: McpConfig = serde_json::from_str(json).unwrap();
assert!(!config.servers.get("srv").unwrap().is_enabled());
}
#[test]
fn test_server_disabled_true_claude_style() {
let json = r#"{"mcpServers":{"srv":{"command":"bin","disabled":true}}}"#;
let config: McpConfig = serde_json::from_str(json).unwrap();
assert!(!config.servers.get("srv").unwrap().is_enabled());
}
#[test]
fn test_server_disabled_wins_over_enabled() {
// `disabled` (Claude Code style) wins when both spellings are present.
let json = r#"{"servers":{"srv":{"command":"bin","enabled":true,"disabled":true}}}"#;
let config: McpConfig = serde_json::from_str(json).unwrap();
assert!(!config.servers.get("srv").unwrap().is_enabled());
let json = r#"{"servers":{"srv":{"command":"bin","enabled":false,"disabled":false}}}"#;
let config: McpConfig = serde_json::from_str(json).unwrap();
assert!(config.servers.get("srv").unwrap().is_enabled());
}
#[test]
fn test_disabled_server_survives_save_roundtrip() {
// Disabled servers must stay in config (kept, not spawned), including
// through a save/load cycle.
let json = r#"{"servers":{"off":{"command":"bin","enabled":false}}}"#;
let config: McpConfig = serde_json::from_str(json).unwrap();
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("mcp.json");
config.save_to_file(&path).unwrap();
let reloaded = McpConfig::load_from_file(&path).unwrap();
assert!(!reloaded.servers.get("off").unwrap().is_enabled());
}
#[test]
fn test_tool_def_deserialization() {
let json = r#"{
"name": "read_file",
"description": "Read a file from disk",
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string"}
},
"required": ["path"]
}
}"#;
let tool: McpToolDef = serde_json::from_str(json).unwrap();
assert_eq!(tool.name, "read_file");
assert_eq!(tool.description, Some("Read a file from disk".to_string()));
}
#[test]
fn test_tool_call_result_text() {
let json = r#"{
"content": [{"type": "text", "text": "File contents here"}],
"isError": false
}"#;
let result: ToolCallResult = serde_json::from_str(json).unwrap();
assert!(!result.is_error);
assert_eq!(result.content.len(), 1);
match &result.content[0] {
ContentBlock::Text { text, .. } => assert_eq!(text, "File contents here"),
_ => panic!("Expected text block"),
}
}
#[test]
fn test_tool_call_result_error() {
let json = r#"{
"content": [{"type": "text", "text": "File not found"}],
"isError": true
}"#;
let result: ToolCallResult = serde_json::from_str(json).unwrap();
assert!(result.is_error);
}
#[test]
fn test_initialize_result() {
let json = r#"{
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {"listChanged": true}
},
"serverInfo": {
"name": "test-server",
"version": "1.0.0"
}
}"#;
let result: InitializeResult = serde_json::from_str(json).unwrap();
assert_eq!(result.protocol_version, "2024-11-05");
assert!(result.server_info.is_some());
}