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
This commit is contained in:
parent
ee1c63c53f
commit
6a3b32d3ab
1 changed files with 430 additions and 211 deletions
|
|
@ -13,3 +13,122 @@ fn test_tool_list_result() {
|
|||
})
|
||||
));
|
||||
}
|
||||
|
||||
/// 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(_)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue