rust-sdk/crates/rmcp/tests/test_deserialization.rs
Dale Seo 6a3b32d3ab
chore: add #[non_exhaustive] to remaining public structs (#768)
* chore: add #[non_exhaustive] to remaining public structs

* chore: add #[non_exhaustive] to remaining public types

* chore: enable exhaustive_structs/enums clippy lints

* test: add untagged ServerResult deserialization regression tests
2026-03-26 10:23:58 -04:00

134 lines
5 KiB
Rust

use rmcp::model::{JsonRpcResponse, ServerJsonRpcMessage, ServerResult};
#[test]
fn test_tool_list_result() {
let json = std::fs::read("tests/test_deserialization/tool_list_result.json").unwrap();
let result: ServerJsonRpcMessage = serde_json::from_slice(&json).unwrap();
println!("{result:#?}");
assert!(matches!(
result,
ServerJsonRpcMessage::Response(JsonRpcResponse {
result: ServerResult::ListToolsResult(_),
..
})
));
}
/// Regression tests for `#[serde(untagged)]` deserialization of `ServerResult`.
///
/// `ServerResult` is an untagged enum, so serde tries each variant in declaration
/// order. `GetTaskPayloadResult` has a custom `Deserialize` impl that always fails
/// so it is skipped, and `CustomResult(Value)` acts as the catch-all. If variant
/// ordering changes or the custom impl is removed, these tests will catch the
/// regression.
mod untagged_server_result {
use rmcp::model::{CallToolResult, JsonRpcResponse, ServerJsonRpcMessage, ServerResult};
use serde_json::json;
/// Helper: wrap a result value in a JSON-RPC response envelope.
fn wrap_response(result: serde_json::Value) -> serde_json::Value {
json!({
"jsonrpc": "2.0",
"id": 1,
"result": result
})
}
/// Parse a JSON-RPC response and return the inner `ServerResult`.
fn parse_result(json: serde_json::Value) -> ServerResult {
let msg: ServerJsonRpcMessage = serde_json::from_value(json).unwrap();
match msg {
ServerJsonRpcMessage::Response(JsonRpcResponse { result, .. }) => result,
other => panic!("expected Response, got {other:?}"),
}
}
#[test]
fn initialize_result_deserializes_to_correct_variant() {
let result = parse_result(wrap_response(json!({
"protocolVersion": "2025-03-26",
"capabilities": {},
"serverInfo": {
"name": "test-server",
"version": "1.0.0"
}
})));
assert!(
matches!(result, ServerResult::InitializeResult(_)),
"expected InitializeResult, got {result:?}"
);
}
#[test]
fn call_tool_result_deserializes_to_correct_variant() {
let result = parse_result(wrap_response(json!({
"content": [
{ "type": "text", "text": "hello" }
]
})));
assert!(
matches!(result, ServerResult::CallToolResult(_)),
"expected CallToolResult, got {result:?}"
);
}
#[test]
fn empty_object_deserializes_to_empty_result() {
let result = parse_result(wrap_response(json!({})));
assert!(
matches!(result, ServerResult::EmptyResult(_)),
"expected EmptyResult, got {result:?}"
);
}
#[test]
fn unknown_shape_falls_through_to_custom_result() {
// A value that doesn't match any known result type should land in
// CustomResult, NOT GetTaskPayloadResult.
let result = parse_result(wrap_response(json!({
"some_unknown_field": "some_value",
"number": 42
})));
assert!(
matches!(result, ServerResult::CustomResult(_)),
"expected CustomResult, got {result:?}"
);
}
#[test]
fn arbitrary_json_value_does_not_deserialize_as_get_task_payload_result() {
// GetTaskPayloadResult wraps a bare Value, but its custom Deserialize
// always fails so serde skips it during untagged resolution.
// Any JSON value must fall through to CustomResult instead.
for value in [json!(42), json!("hello"), json!(null), json!([1, 2, 3])] {
let result = parse_result(wrap_response(value.clone()));
assert!(
matches!(result, ServerResult::CustomResult(_)),
"value {value} should deserialize as CustomResult, got {result:?}"
);
}
}
#[test]
fn round_trip_initialize_result_preserves_variant() {
let json = json!({
"protocolVersion": "2025-03-26",
"capabilities": {},
"serverInfo": { "name": "test", "version": "1.0" }
});
// Parse as ServerResult, serialize back, parse again — must stay InitializeResult.
let result = parse_result(wrap_response(json.clone()));
assert!(matches!(&result, ServerResult::InitializeResult(_)));
let reserialized = serde_json::to_value(&result).unwrap();
let result2 = parse_result(wrap_response(reserialized));
assert!(matches!(result2, ServerResult::InitializeResult(_)));
}
#[test]
fn round_trip_call_tool_result_preserves_variant() {
let original = CallToolResult::success(vec![rmcp::model::Content::text("hello world")]);
let json = serde_json::to_value(&original).unwrap();
let result = parse_result(wrap_response(json));
assert!(matches!(result, ServerResult::CallToolResult(_)));
}
}