use std::sync::Arc;
use forge_client::{McpClient, RouterDispatcher};
use forge_manifest::{server_entry_from_tools, ManifestBuilder, McpTool};
use forge_sandbox::{SandboxConfig, ToolDispatcher};
use forge_server::{ExecuteInput, ForgeServer, SearchInput};
use rmcp::handler::server::wrapper::Parameters;
fn test_server_bin() -> String {
if let Ok(path) = std::env::var("CARGO_BIN_EXE_forge-test-server") {
return path;
}
let mut path = std::env::current_exe()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
path.push("forge-test-server");
if path.exists() {
return path.to_str().unwrap().to_string();
}
let status =
std::process::Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()))
.args([
"build",
"-p",
"forge-test-server",
"--bin",
"forge-test-server",
])
.status()
.expect("failed to run cargo build for forge-test-server");
assert!(status.success(), "failed to build forge-test-server");
path.to_str().unwrap().to_string()
}
async fn setup_forge_with_test_server() -> (ForgeServer, Arc<McpClient>) {
let bin = test_server_bin();
let client = McpClient::connect_stdio("test-server", &bin, &[])
.await
.expect("failed to connect to test server");
let tools = client.list_tools().await.expect("failed to list tools");
assert!(!tools.is_empty(), "test server should expose tools");
let mcp_tools: Vec<McpTool> = tools
.into_iter()
.map(|t| McpTool {
name: t.name,
description: t.description,
input_schema: Some(t.input_schema),
})
.collect();
let server_entry = server_entry_from_tools("test-server", "Mock test server", mcp_tools);
let manifest = ManifestBuilder::new().add_server(server_entry).build();
let client = Arc::new(client);
let mut router = RouterDispatcher::new();
router.add_client("test-server", client.clone() as Arc<dyn ToolDispatcher>);
let dispatcher: Arc<dyn ToolDispatcher> = Arc::new(router);
let server = ForgeServer::new(SandboxConfig::default(), manifest, dispatcher, None);
(server, client)
}
#[tokio::test]
async fn mcpclient_connects_to_stdio_server() {
let bin = test_server_bin();
let client = McpClient::connect_stdio("test-server", &bin, &[])
.await
.expect("failed to connect to test server");
assert_eq!(client.name(), "test-server");
let tools = client.list_tools().await.expect("failed to list tools");
assert!(
tools.len() >= 3,
"expected at least 3 tools, got {}",
tools.len()
);
let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(tool_names.contains(&"echo"), "should have echo tool");
assert!(
tool_names.contains(&"math.add"),
"should have math.add tool"
);
assert!(
tool_names.contains(&"symbols.find"),
"should have symbols.find tool"
);
client.disconnect().await.expect("disconnect failed");
}
#[tokio::test]
async fn mcpclient_calls_tool_and_gets_result() {
let bin = test_server_bin();
let client = McpClient::connect_stdio("test-server", &bin, &[])
.await
.expect("failed to connect");
let result = client
.call_tool(
"test-server",
"echo",
serde_json::json!({"message": "hello forge"}),
)
.await
.expect("echo tool call failed");
assert_eq!(result["echoed"], "hello forge");
client.disconnect().await.ok();
}
#[tokio::test]
async fn mcpclient_handles_connection_failure() {
let result = McpClient::connect_stdio("nonexistent", "/nonexistent/path/to/binary", &[]).await;
assert!(
result.is_err(),
"connecting to nonexistent binary should fail"
);
let err = result.err().unwrap().to_string();
assert!(
err.contains("nonexistent") || err.contains("spawn") || err.contains("No such file"),
"error should be meaningful: {err}"
);
}
#[tokio::test]
async fn end_to_end_search_discovers_tools() {
let (server, _client) = setup_forge_with_test_server().await;
let result = server
.search(Parameters(SearchInput {
code: r#"async () => manifest.servers.map(s => s.name)"#.into(),
}))
.await;
let json = result.expect("search should succeed");
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let names = parsed.as_array().unwrap();
assert_eq!(names.len(), 1);
assert_eq!(names[0], "test-server");
}
#[tokio::test]
async fn end_to_end_search_discovers_categories() {
let (server, _client) = setup_forge_with_test_server().await;
let result = server
.search(Parameters(SearchInput {
code: r#"async () => manifest.servers.map(s => ({
name: s.name,
categories: Object.keys(s.categories),
totalTools: Object.values(s.categories).reduce((sum, c) => sum + c.tools.length, 0)
}))"#
.into(),
}))
.await;
let json = result.expect("search should succeed");
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let servers = parsed.as_array().unwrap();
let server_info = &servers[0];
assert_eq!(server_info["name"], "test-server");
assert!(server_info["totalTools"].as_i64().unwrap() >= 3);
}
#[tokio::test]
async fn end_to_end_execute_calls_echo() {
let (server, _client) = setup_forge_with_test_server().await;
let result = server
.execute(Parameters(ExecuteInput {
code: r#"async () => {
return await forge.callTool("test-server", "echo", { message: "from sandbox" });
}"#
.into(),
}))
.await;
let json = result.expect("execute should succeed");
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["echoed"], "from sandbox");
}
#[tokio::test]
async fn end_to_end_execute_calls_math() {
let (server, _client) = setup_forge_with_test_server().await;
let result = server
.execute(Parameters(ExecuteInput {
code: r#"async () => {
return await forge.callTool("test-server", "math.add", { a: 17, b: 25 });
}"#
.into(),
}))
.await;
let json = result.expect("execute should succeed");
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["result"], 42.0);
}
#[tokio::test]
async fn end_to_end_execute_chains_multiple_calls() {
let (server, _client) = setup_forge_with_test_server().await;
let result = server
.execute(Parameters(ExecuteInput {
code: r#"async () => {
const symbols = await forge.callTool("test-server", "symbols.find", {
pattern: "handle",
limit: 2
});
const sum = await forge.callTool("test-server", "math.add", {
a: symbols.length,
b: 10
});
return { symbols, sum };
}"#
.into(),
}))
.await;
let json = result.expect("execute should succeed");
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let symbols = parsed["symbols"].as_array().unwrap();
assert_eq!(symbols.len(), 2);
assert_eq!(symbols[0]["name"], "handle_0");
assert_eq!(parsed["sum"]["result"], 12.0);
}
#[tokio::test]
async fn end_to_end_execute_via_proxy_api() {
let (server, _client) = setup_forge_with_test_server().await;
let result = server
.execute(Parameters(ExecuteInput {
code: r#"async () => {
return await forge.server("test-server").symbols.find({
pattern: "main",
limit: 3
});
}"#
.into(),
}))
.await;
let json = result.expect("execute should succeed");
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let symbols = parsed.as_array().unwrap();
assert_eq!(symbols.len(), 3);
assert_eq!(symbols[0]["name"], "main_0");
}
#[tokio::test]
async fn mcpclient_list_resources_returns_items() {
let bin = test_server_bin();
let client = McpClient::connect_stdio("test-server", &bin, &[])
.await
.expect("failed to connect to test server");
let resources = client
.list_resources()
.await
.expect("failed to list resources");
assert_eq!(resources.len(), 2, "test server should expose 2 resources");
let uris: Vec<&str> = resources.iter().map(|r| r.uri.as_str()).collect();
assert!(
uris.contains(&"file:///logs/app.log"),
"should have app.log resource"
);
assert!(
uris.contains(&"db://schema/users"),
"should have users schema resource"
);
let log_resource = resources
.iter()
.find(|r| r.uri == "file:///logs/app.log")
.unwrap();
assert_eq!(log_resource.name, "Application Log");
client.disconnect().await.ok();
}
#[tokio::test]
async fn mcpclient_read_resource_returns_content() {
let bin = test_server_bin();
let client = McpClient::connect_stdio("test-server", &bin, &[])
.await
.expect("failed to connect to test server");
let content = client
.read_resource("file:///logs/app.log")
.await
.expect("failed to read resource");
let text = content.as_str().unwrap_or("");
assert!(
text.contains("startup complete"),
"resource should contain log text, got: {content}"
);
client.disconnect().await.ok();
}
#[tokio::test]
async fn mcpclient_read_resource_propagates_error() {
let bin = test_server_bin();
let client = McpClient::connect_stdio("test-server", &bin, &[])
.await
.expect("failed to connect to test server");
let result = client.read_resource("nonexistent://missing").await;
assert!(result.is_err(), "reading nonexistent resource should fail");
let err = result.unwrap_err().to_string();
assert!(
err.contains("resource") || err.contains("not found") || err.contains("error"),
"error should be meaningful: {err}"
);
client.disconnect().await.ok();
}