Files
alknet/crates/alknet-http/src/adapters/from_mcp/tests.rs
glm-5.2 58e16d088b review(http): mark http/review-mcp completed + fix formatting across crate
Review-mcp verification complete: all 12 checklist items pass (from_mcp/to_mcp
conformance, ADR-037/041/014/023/034, feature gate isolation, GatewayDispatch
concrete struct, test coverage 223+5). Applied cargo fmt across crate.
2026-07-01 19:32:42 +00:00

258 lines
8.7 KiB
Rust

use super::*;
use alknet_call::registry::spec::Visibility;
use rmcp::model::{CallToolResult, Content, Tool};
fn make_tool(name: &str, input: Value, output: Option<Value>) -> Tool {
let input_map = match input {
Value::Object(m) => m,
_ => serde_json::Map::new(),
};
let mut tool = Tool::new_with_raw(
name.to_string(),
Some("test tool".into()),
std::sync::Arc::new(input_map),
);
if let Some(out) = output {
let out_map = match out {
Value::Object(m) => m,
_ => serde_json::Map::new(),
};
tool = tool.with_raw_output_schema(std::sync::Arc::new(out_map));
}
tool
}
fn call_tool_result(
content: Vec<Content>,
structured: Option<Value>,
is_error: Option<bool>,
) -> CallToolResult {
let json = serde_json::json!({
"content": content,
"structuredContent": structured,
"isError": is_error,
});
serde_json::from_value(json).expect("CallToolResult deserializes")
}
#[test]
fn struct_holds_endpoint_auth_token_namespace() {
let adapter = FromMCP::new("http://localhost:8000/mcp", "weather");
assert_eq!(adapter.endpoint(), "http://localhost:8000/mcp");
assert_eq!(adapter.namespace(), "weather");
assert_eq!(adapter.auth_token(), None);
let with_token = adapter.with_auth_token("sekrit");
assert_eq!(with_token.auth_token(), Some("sekrit"));
}
#[test]
fn output_schema_present_uses_declared_schema() {
let declared = serde_json::json!({
"type": "object",
"properties": { "temperature": { "type": "number" } }
});
let tool = make_tool("get_weather", serde_json::json!({}), Some(declared.clone()));
let schema = output_schema_for(&tool);
assert_eq!(schema, declared);
}
#[test]
fn output_schema_absent_uses_content_block_union() {
let tool = make_tool("legacy_tool", serde_json::json!({}), None);
let schema = output_schema_for(&tool);
assert_eq!(schema, content_block_union_schema());
assert_eq!(schema["type"], "array");
}
#[test]
fn content_block_union_schema_has_all_five_variants() {
let schema = content_block_union_schema();
let one_of = schema["items"]["oneOf"].as_array().expect("oneOf array");
let variants: Vec<&str> = one_of
.iter()
.filter_map(|v| v["properties"]["type"]["enum"][0].as_str())
.collect();
assert!(variants.contains(&"text"));
assert!(variants.contains(&"image"));
assert!(variants.contains(&"audio"));
assert!(variants.contains(&"resource"));
assert!(variants.contains(&"resource_link"));
}
#[test]
fn map_structured_content_present_used_as_result() {
let result = CallToolResult::structured(serde_json::json!({ "temperature": 22.5 }));
let response = map_call_tool_result(result, "req-1".to_string());
assert_eq!(response.request_id, "req-1");
match response.result {
Ok(v) => assert_eq!(v, serde_json::json!({ "temperature": 22.5 })),
Err(e) => panic!("expected Ok, got Err: {e:?}"),
}
}
#[test]
fn map_structured_content_absent_maps_content_blocks() {
let result = CallToolResult::success(vec![
Content::text("hello world"),
Content::image("base64data", "image/png"),
]);
let response = map_call_tool_result(result, "req-2".to_string());
match response.result {
Ok(Value::Array(blocks)) => {
assert_eq!(blocks.len(), 2);
assert_eq!(blocks[0]["type"], "text");
assert_eq!(blocks[0]["text"], "hello world");
assert_eq!(blocks[1]["type"], "image");
assert_eq!(blocks[1]["data"], "base64data");
}
other => panic!("expected array, got {other:?}"),
}
}
#[test]
fn map_single_text_block_carried_as_content_block_not_json_parsed() {
let result = CallToolResult::success(vec![Content::text(r#"{"key":"value"}"#)]);
let response = map_call_tool_result(result, "req-3".to_string());
match response.result {
Ok(Value::Array(blocks)) => {
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0]["type"], "text");
assert_eq!(blocks[0]["text"], r#"{"key":"value"}"#);
}
other => panic!("expected array (not JSON-parsed), got {other:?}"),
}
}
#[test]
fn map_is_error_true_returns_call_error() {
let result = CallToolResult::error(vec![Content::text("something went wrong")]);
let response = map_call_tool_result(result, "req-4".to_string());
match response.result {
Err(e) => {
assert_eq!(e.code, "MCP_TOOL_ERROR");
assert!(!e.retryable);
assert!(e.message.contains("isError"));
let details = e.details.expect("details present");
let blocks = details.as_array().expect("details is array");
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0]["text"], "something went wrong");
}
other => panic!("expected Err, got {other:?}"),
}
}
#[test]
fn map_is_error_true_with_no_content_still_errors() {
let result = call_tool_result(vec![], None, Some(true));
let response = map_call_tool_result(result, "req-5".to_string());
match response.result {
Err(e) => {
assert_eq!(e.code, "MCP_TOOL_ERROR");
assert!(e.message.contains("no content"));
}
other => panic!("expected Err, got {other:?}"),
}
}
#[test]
fn map_empty_success_returns_empty_array() {
let result = call_tool_result(vec![], None, Some(false));
let response = map_call_tool_result(result, "req-6".to_string());
match response.result {
Ok(Value::Array(blocks)) => assert!(blocks.is_empty()),
other => panic!("expected empty array, got {other:?}"),
}
}
#[test]
fn map_structured_content_preferred_over_content_blocks() {
let result = call_tool_result(
vec![Content::text("ignored text")],
Some(serde_json::json!({ "structured": true })),
Some(false),
);
let response = map_call_tool_result(result, "req-7".to_string());
match response.result {
Ok(v) => assert_eq!(v, serde_json::json!({ "structured": true })),
other => panic!("expected structured content, got {other:?}"),
}
}
#[test]
fn error_schemas_for_tool_yields_mcp_tool_error() {
let tool = make_tool("weather", serde_json::json!({}), None);
let errors = error_schemas_for(&tool);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].code, "MCP_TOOL_ERROR");
assert!(errors[0].description.contains("weather"));
assert!(errors[0].description.contains("isError"));
assert!(errors[0].schema["type"] == "array");
}
#[test]
fn capabilities_for_token_injects_http_token() {
let caps = capabilities_for(Some("tok-123".to_string()));
let secret = caps.get(MCP_CAPABILITY_KEY).expect("token present");
assert_eq!(secret.expose_secret(), "tok-123");
}
#[test]
fn capabilities_for_none_yields_empty() {
let caps = capabilities_for(None);
assert!(caps.get(MCP_CAPABILITY_KEY).is_none());
}
#[test]
fn build_spec_output_schema_present_shape() {
let tool = make_tool(
"get_weather",
serde_json::json!({ "type": "object", "properties": { "city": { "type": "string" } } }),
Some(
serde_json::json!({ "type": "object", "properties": { "temperature": { "type": "number" } } }),
),
);
let spec = build_spec(&tool, "weather");
assert_eq!(spec.name, "weather/get_weather");
assert_eq!(spec.namespace, "weather");
assert_eq!(spec.op_type, OperationType::Mutation);
assert_eq!(spec.visibility, Visibility::Internal);
assert_eq!(spec.input_schema["type"], "object");
assert_eq!(spec.input_schema["properties"]["city"]["type"], "string");
assert_eq!(spec.output_schema["type"], "object");
assert_eq!(
spec.output_schema["properties"]["temperature"]["type"],
"number"
);
assert_eq!(spec.error_schemas.len(), 1);
assert_eq!(spec.error_schemas[0].code, "MCP_TOOL_ERROR");
assert!(spec.access_control == AccessControl::default());
}
#[test]
fn build_spec_output_schema_absent_uses_union() {
let tool = make_tool("legacy", serde_json::json!({}), None);
let spec = build_spec(&tool, "legacy");
assert_eq!(spec.output_schema, content_block_union_schema());
}
#[test]
fn build_spec_name_with_prefix_when_namespace_set() {
let tool = make_tool("search", serde_json::json!({}), None);
let spec = build_spec(&tool, "tools");
assert_eq!(spec.name, "tools/search");
assert_eq!(spec.namespace, "tools");
}
#[test]
fn no_env_vars_in_capability_key_constant() {
assert_eq!(MCP_CAPABILITY_KEY, "mcp");
}
#[tokio::test]
async fn forwarding_handler_reads_capabilities_not_env_vars() {
let adapter = FromMCP::new("http://127.0.0.1:1/mcp", "ns");
let _ = adapter.auth_token();
assert!(adapter.auth_token().is_none());
}