Skip to content

Commit 0545c99

Browse files
committed
test: add untagged ServerResult deserialization regression tests
1 parent 8f0be99 commit 0545c99

1 file changed

Lines changed: 119 additions & 0 deletions

File tree

crates/rmcp/tests/test_deserialization.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,122 @@ fn test_tool_list_result() {
1313
})
1414
));
1515
}
16+
17+
/// Regression tests for `#[serde(untagged)]` deserialization of `ServerResult`.
18+
///
19+
/// `ServerResult` is an untagged enum, so serde tries each variant in declaration
20+
/// order. `GetTaskPayloadResult` has a custom `Deserialize` impl that always fails
21+
/// so it is skipped, and `CustomResult(Value)` acts as the catch-all. If variant
22+
/// ordering changes or the custom impl is removed, these tests will catch the
23+
/// regression.
24+
mod untagged_server_result {
25+
use rmcp::model::{CallToolResult, JsonRpcResponse, ServerJsonRpcMessage, ServerResult};
26+
use serde_json::json;
27+
28+
/// Helper: wrap a result value in a JSON-RPC response envelope.
29+
fn wrap_response(result: serde_json::Value) -> serde_json::Value {
30+
json!({
31+
"jsonrpc": "2.0",
32+
"id": 1,
33+
"result": result
34+
})
35+
}
36+
37+
/// Parse a JSON-RPC response and return the inner `ServerResult`.
38+
fn parse_result(json: serde_json::Value) -> ServerResult {
39+
let msg: ServerJsonRpcMessage = serde_json::from_value(json).unwrap();
40+
match msg {
41+
ServerJsonRpcMessage::Response(JsonRpcResponse { result, .. }) => result,
42+
other => panic!("expected Response, got {other:?}"),
43+
}
44+
}
45+
46+
#[test]
47+
fn initialize_result_deserializes_to_correct_variant() {
48+
let result = parse_result(wrap_response(json!({
49+
"protocolVersion": "2025-03-26",
50+
"capabilities": {},
51+
"serverInfo": {
52+
"name": "test-server",
53+
"version": "1.0.0"
54+
}
55+
})));
56+
assert!(
57+
matches!(result, ServerResult::InitializeResult(_)),
58+
"expected InitializeResult, got {result:?}"
59+
);
60+
}
61+
62+
#[test]
63+
fn call_tool_result_deserializes_to_correct_variant() {
64+
let result = parse_result(wrap_response(json!({
65+
"content": [
66+
{ "type": "text", "text": "hello" }
67+
]
68+
})));
69+
assert!(
70+
matches!(result, ServerResult::CallToolResult(_)),
71+
"expected CallToolResult, got {result:?}"
72+
);
73+
}
74+
75+
#[test]
76+
fn empty_object_deserializes_to_empty_result() {
77+
let result = parse_result(wrap_response(json!({})));
78+
assert!(
79+
matches!(result, ServerResult::EmptyResult(_)),
80+
"expected EmptyResult, got {result:?}"
81+
);
82+
}
83+
84+
#[test]
85+
fn unknown_shape_falls_through_to_custom_result() {
86+
// A value that doesn't match any known result type should land in
87+
// CustomResult, NOT GetTaskPayloadResult.
88+
let result = parse_result(wrap_response(json!({
89+
"some_unknown_field": "some_value",
90+
"number": 42
91+
})));
92+
assert!(
93+
matches!(result, ServerResult::CustomResult(_)),
94+
"expected CustomResult, got {result:?}"
95+
);
96+
}
97+
98+
#[test]
99+
fn arbitrary_json_value_does_not_deserialize_as_get_task_payload_result() {
100+
// GetTaskPayloadResult wraps a bare Value, but its custom Deserialize
101+
// always fails so serde skips it during untagged resolution.
102+
// Any JSON value must fall through to CustomResult instead.
103+
for value in [json!(42), json!("hello"), json!(null), json!([1, 2, 3])] {
104+
let result = parse_result(wrap_response(value.clone()));
105+
assert!(
106+
matches!(result, ServerResult::CustomResult(_)),
107+
"value {value} should deserialize as CustomResult, got {result:?}"
108+
);
109+
}
110+
}
111+
112+
#[test]
113+
fn round_trip_initialize_result_preserves_variant() {
114+
let json = json!({
115+
"protocolVersion": "2025-03-26",
116+
"capabilities": {},
117+
"serverInfo": { "name": "test", "version": "1.0" }
118+
});
119+
// Parse as ServerResult, serialize back, parse again — must stay InitializeResult.
120+
let result = parse_result(wrap_response(json.clone()));
121+
assert!(matches!(&result, ServerResult::InitializeResult(_)));
122+
let reserialized = serde_json::to_value(&result).unwrap();
123+
let result2 = parse_result(wrap_response(reserialized));
124+
assert!(matches!(result2, ServerResult::InitializeResult(_)));
125+
}
126+
127+
#[test]
128+
fn round_trip_call_tool_result_preserves_variant() {
129+
let original = CallToolResult::success(vec![rmcp::model::Content::text("hello world")]);
130+
let json = serde_json::to_value(&original).unwrap();
131+
let result = parse_result(wrap_response(json));
132+
assert!(matches!(result, ServerResult::CallToolResult(_)));
133+
}
134+
}

0 commit comments

Comments
 (0)