rust-sdk/crates/rmcp/tests/test_elicitation.rs
2026-04-08 15:07:04 -04:00

2234 lines
74 KiB
Rust

//cargo test --test test_elicitation --features "client server"
use rmcp::{model::*, object, service::*};
// For typed elicitation tests
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
#[cfg(feature = "schemars")]
use serde::{Deserialize, Serialize};
use serde_json::json;
/// Test that elicitation data structures can be serialized and deserialized correctly
/// This ensures JSON-RPC compatibility with MCP 2025-06-18 specification
#[tokio::test]
async fn test_elicitation_serialization() {
// Test ElicitationAction enum serialization
let accept = ElicitationAction::Accept;
let decline = ElicitationAction::Decline;
let cancel = ElicitationAction::Cancel;
assert_eq!(serde_json::to_string(&accept).unwrap(), "\"accept\"");
assert_eq!(serde_json::to_string(&decline).unwrap(), "\"decline\"");
assert_eq!(serde_json::to_string(&cancel).unwrap(), "\"cancel\"");
// Test deserialization
assert_eq!(
serde_json::from_str::<ElicitationAction>("\"accept\"").unwrap(),
ElicitationAction::Accept
);
assert_eq!(
serde_json::from_str::<ElicitationAction>("\"decline\"").unwrap(),
ElicitationAction::Decline
);
assert_eq!(
serde_json::from_str::<ElicitationAction>("\"cancel\"").unwrap(),
ElicitationAction::Cancel
);
}
/// Test CreateElicitationRequestParams structure serialization/deserialization
#[tokio::test]
async fn test_elicitation_request_param_serialization() {
let schema = ElicitationSchema::builder()
.required_property("email", PrimitiveSchema::String(StringSchema::email()))
.build()
.unwrap();
let request_param = CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Please provide your email address".to_string(),
requested_schema: schema,
};
// Test serialization
let json = serde_json::to_value(&request_param).unwrap();
let expected = json!({
"mode": "form",
"message": "Please provide your email address",
"requestedSchema": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email"
}
},
"required": ["email"]
}
});
assert_eq!(json, expected);
// Test deserialization
let deserialized: CreateElicitationRequestParams = serde_json::from_value(expected).unwrap();
match (&deserialized, &request_param) {
(
CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: msg1,
requested_schema: schema1,
},
CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: msg2,
requested_schema: schema2,
},
) => {
assert_eq!(msg1, msg2);
assert_eq!(schema1, schema2);
}
_ => panic!("Expected FormElicitationParam variant"),
}
}
/// Test CreateElicitationResult structure with different action types
#[tokio::test]
async fn test_elicitation_result_serialization() {
// Test Accept with content
let accept_result = CreateElicitationResult {
action: ElicitationAction::Accept,
content: Some(json!({"email": "user@example.com"})),
meta: None,
};
let json = serde_json::to_value(&accept_result).unwrap();
let expected = json!({
"action": "accept",
"content": {"email": "user@example.com"}
});
assert_eq!(json, expected);
// Test Decline without content
let decline_result = CreateElicitationResult {
action: ElicitationAction::Decline,
content: None,
meta: None,
};
let json = serde_json::to_value(&decline_result).unwrap();
let expected = json!({
"action": "decline"
// content should be omitted when None due to skip_serializing_if
});
assert_eq!(json, expected);
// Test deserialization
let deserialized: CreateElicitationResult = serde_json::from_value(expected).unwrap();
assert_eq!(deserialized.action, ElicitationAction::Decline);
assert_eq!(deserialized.content, None);
assert_eq!(deserialized.meta, None);
// Test protocol-level metadata round-trips as _meta.
let meta_result =
CreateElicitationResult::new(ElicitationAction::Accept).with_meta(Meta(object!({
"traceId": "elicitation-123"
})));
let json = serde_json::to_value(&meta_result).unwrap();
let expected = json!({
"action": "accept",
"_meta": {"traceId": "elicitation-123"}
});
assert_eq!(json, expected);
let deserialized: CreateElicitationResult = serde_json::from_value(expected).unwrap();
assert_eq!(
deserialized.meta,
Some(Meta(object!({ "traceId": "elicitation-123" })))
);
}
/// Test that elicitation requests can be created and handled through the JSON-RPC protocol
#[tokio::test]
async fn test_elicitation_json_rpc_protocol() {
let schema = ElicitationSchema::builder()
.required_property(
"confirmation",
PrimitiveSchema::Boolean(BooleanSchema::new()),
)
.build()
.unwrap();
// Create a complete JSON-RPC request for elicitation
let request = JsonRpcRequest {
jsonrpc: JsonRpcVersion2_0,
id: RequestId::Number(1),
request: CreateElicitationRequest::new(
CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Do you want to continue?".to_string(),
requested_schema: schema,
},
),
};
// Test serialization of complete request
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["jsonrpc"], "2.0");
assert_eq!(json["id"], 1);
assert_eq!(json["method"], "elicitation/create");
assert_eq!(json["params"]["message"], "Do you want to continue?");
// Test deserialization
let deserialized: JsonRpcRequest<CreateElicitationRequest> =
serde_json::from_value(json).unwrap();
assert_eq!(deserialized.id, RequestId::Number(1));
match &deserialized.request.params {
CreateElicitationRequestParams::FormElicitationParams { message, .. } => {
assert_eq!(message, "Do you want to continue?");
}
_ => panic!("Expected FormElicitationParam variant"),
}
}
/// Test elicitation action types and their expected behavior
#[tokio::test]
async fn test_elicitation_action_types() {
// Test all three action types
let actions = [
ElicitationAction::Accept,
ElicitationAction::Decline,
ElicitationAction::Cancel,
];
// Each action should have a unique string representation
let serialized: Vec<String> = actions
.iter()
.map(|action| serde_json::to_string(action).unwrap())
.collect();
assert_eq!(serialized.len(), 3);
assert!(serialized.contains(&"\"accept\"".to_string()));
assert!(serialized.contains(&"\"decline\"".to_string()));
assert!(serialized.contains(&"\"cancel\"".to_string()));
// Test round-trip serialization
for action in actions {
let json = serde_json::to_string(&action).unwrap();
let deserialized: ElicitationAction = serde_json::from_str(&json).unwrap();
assert_eq!(action, deserialized);
}
}
/// Test MCP 2025-06-18 specification compliance
/// Ensures our implementation matches the latest MCP spec
#[tokio::test]
async fn test_elicitation_spec_compliance() {
// Test that method names match the specification
assert_eq!(ElicitationCreateRequestMethod::VALUE, "elicitation/create");
assert_eq!(
ElicitationResponseNotificationMethod::VALUE,
"notifications/elicitation/response"
);
// Test that enum values match specification
let actions = [
ElicitationAction::Accept,
ElicitationAction::Decline,
ElicitationAction::Cancel,
];
let serialized: Vec<String> = actions
.iter()
.map(|a| serde_json::to_string(a).unwrap())
.collect();
assert_eq!(serialized, vec!["\"accept\"", "\"decline\"", "\"cancel\""]);
}
/// Test error handling and edge cases for elicitation
#[tokio::test]
async fn test_elicitation_error_handling() {
// Test minimal schema handling (empty properties is technically valid)
let minimal_schema_request = CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Test message".to_string(),
requested_schema: ElicitationSchema::builder().build().unwrap(),
};
// Should serialize without error
let _json = serde_json::to_value(&minimal_schema_request).unwrap();
// Test empty message
let empty_message_request = CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "".to_string(),
requested_schema: ElicitationSchema::builder()
.property("value", PrimitiveSchema::String(StringSchema::new()))
.build()
.unwrap(),
};
// Should serialize without error (validation is up to the implementation)
let _json = serde_json::to_value(&empty_message_request).unwrap();
// Test that we can deserialize invalid action types (should fail)
let invalid_action_json = json!("invalid_action");
let result = serde_json::from_value::<ElicitationAction>(invalid_action_json);
assert!(result.is_err());
}
/// Benchmark-style test for elicitation performance
#[tokio::test]
async fn test_elicitation_performance() {
let schema = ElicitationSchema::builder()
.property("data", PrimitiveSchema::String(StringSchema::new()))
.build()
.unwrap();
let request = CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Performance test message".to_string(),
requested_schema: schema,
};
let start = std::time::Instant::now();
// Serialize/deserialize 1000 times
for _ in 0..1000 {
let json = serde_json::to_value(&request).unwrap();
let _deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap();
}
let duration = start.elapsed();
println!(
"1000 elicitation serialization/deserialization cycles took: {:?}",
duration
);
// Should complete in reasonable time (less than 100ms on modern hardware)
assert!(
duration.as_millis() < 1000,
"Performance test took too long: {:?}",
duration
);
}
/// Test elicitation capabilities integration
/// Ensures that elicitation capability can be properly configured and serialized
#[tokio::test]
async fn test_elicitation_capabilities() {
use rmcp::model::{ClientCapabilities, ElicitationCapability};
// Test basic elicitation capability
let mut elicitation_cap = ElicitationCapability::default();
assert_eq!(elicitation_cap.form, None);
assert_eq!(elicitation_cap.url, None);
// Test with schema validation enabled
elicitation_cap.form = Some(FormElicitationCapability {
schema_validation: Some(true),
});
// Test serialization
let json = serde_json::to_value(&elicitation_cap).unwrap();
let expected = json!({"form":{"schemaValidation": true}});
assert_eq!(json, expected);
// Test deserialization
let deserialized: ElicitationCapability = serde_json::from_value(expected).unwrap();
assert_eq!(
deserialized.form.as_ref().unwrap().schema_validation,
Some(true)
);
// Test ClientCapabilities builder with elicitation
let client_caps = ClientCapabilities::builder()
.enable_elicitation()
.enable_elicitation_schema_validation()
.build();
assert!(client_caps.elicitation.is_some());
assert_eq!(
client_caps
.elicitation
.as_ref()
.unwrap()
.form
.as_ref()
.unwrap()
.schema_validation,
Some(true)
);
// Test full client capabilities serialization
let json = serde_json::to_value(&client_caps).unwrap();
assert!(
json["elicitation"]["form"]["schemaValidation"]
.as_bool()
.unwrap_or(false)
);
}
/// Test convenience methods for common elicitation scenarios
/// This ensures the helper methods create proper requests with expected schemas
#[tokio::test]
async fn test_elicitation_convenience_methods() {
// Test that convenience methods produce the expected request parameters
// Test confirmation schema
let confirmation_schema = serde_json::json!({
"type": "boolean",
"description": "User confirmation (true for yes, false for no)"
});
// Verify the schema structure for boolean confirmation
assert_eq!(confirmation_schema["type"], "boolean");
assert!(confirmation_schema["description"].is_string());
// Test text input schema (non-required)
let text_schema = serde_json::json!({
"type": "string",
"description": "User text input"
});
assert_eq!(text_schema["type"], "string");
assert!(text_schema.get("minLength").is_none());
// Test text input schema (required)
let required_text_schema = serde_json::json!({
"type": "string",
"description": "User text input",
"minLength": 1
});
assert_eq!(required_text_schema["minLength"], 1);
// Test choice schema
let options = ["Option A", "Option B", "Option C"];
let choice_schema = serde_json::json!({
"type": "integer",
"minimum": 0,
"maximum": options.len() - 1,
"description": format!("Choose an option: {}", options.join(", "))
});
assert_eq!(choice_schema["type"], "integer");
assert_eq!(choice_schema["minimum"], 0);
assert_eq!(choice_schema["maximum"], 2);
assert!(
choice_schema["description"]
.as_str()
.unwrap()
.contains("Option A")
);
// Test that CreateElicitationRequestParam can be created with type-safe schemas
let confirmation_request = CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Test confirmation".to_string(),
requested_schema: ElicitationSchema::builder()
.property(
"confirmed",
PrimitiveSchema::Boolean(
BooleanSchema::new()
.description("User confirmation (true for yes, false for no)"),
),
)
.build()
.unwrap(),
};
// Test serialization of convenience method request
let json = serde_json::to_value(&confirmation_request).unwrap();
assert_eq!(json["message"], "Test confirmation");
assert_eq!(json["requestedSchema"]["type"], "object");
assert_eq!(
json["requestedSchema"]["properties"]["confirmed"]["type"],
"boolean"
);
}
/// Test structured input with multiple primitive properties
/// Ensures that schemas with multiple primitive properties work correctly with elicitation
#[tokio::test]
async fn test_elicitation_structured_schemas() {
// Test schema with multiple primitive properties
let schema = ElicitationSchema::builder()
.required_string_with("name", |s| s.length(1, 100))
.required_email("email")
.required_integer("age", 0, 150)
.optional_bool("newsletter", false)
.required_enum_schema(
"country",
EnumSchema::builder(vec!["US".to_string(), "UK".to_string(), "CA".to_string()]).build(),
)
.description("User registration information")
.build()
.unwrap();
let request = CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Please provide your user information".to_string(),
requested_schema: schema,
};
// Test that complex schemas serialize/deserialize correctly
let json = serde_json::to_value(&request).unwrap();
let deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap();
match deserialized {
CreateElicitationRequestParams::FormElicitationParams {
message,
requested_schema,
..
} => {
assert_eq!(message, "Please provide your user information");
assert_eq!(requested_schema.properties.len(), 5);
assert!(requested_schema.properties.contains_key("name"));
assert!(requested_schema.properties.contains_key("email"));
assert!(requested_schema.properties.contains_key("age"));
assert!(requested_schema.properties.contains_key("newsletter"));
assert!(requested_schema.properties.contains_key("country"));
assert_eq!(
requested_schema.required,
Some(vec![
"name".to_string(),
"email".to_string(),
"age".to_string(),
"country".to_string()
])
);
}
_ => panic!("Expected FormElicitationParam variant"),
}
}
// Typed elicitation tests using the API with schemars
#[cfg(feature = "schemars")]
mod typed_elicitation_tests {
use super::*;
/// Simple user confirmation with reason
#[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[schemars(description = "User confirmation with optional reasoning")]
struct UserConfirmation {
#[schemars(description = "User's decision (true for yes, false for no)")]
confirmed: bool,
#[schemars(description = "Optional reason for the decision")]
reason: Option<String>,
}
/// User profile with validation constraints
#[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[schemars(description = "Complete user profile information")]
struct UserProfile {
#[schemars(description = "Full name")]
name: String,
#[schemars(description = "Email address")]
email: String,
#[schemars(description = "Age in years")]
age: u8,
#[schemars(description = "User preferences")]
preferences: UserPreferences,
}
/// User preferences
#[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
struct UserPreferences {
#[schemars(description = "UI theme preference")]
theme: Theme,
#[schemars(description = "Enable notifications")]
notifications: bool,
#[schemars(description = "Language preference")]
language: String,
}
/// UI theme options
#[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[schemars(description = "Available UI themes")]
enum Theme {
#[schemars(description = "Light theme")]
Light,
#[schemars(description = "Dark theme")]
Dark,
#[schemars(description = "Auto-detect based on system")]
Auto,
}
// Mark types as safe for elicitation (they generate object schemas)
rmcp::elicit_safe!(UserConfirmation, UserProfile, UserPreferences);
/// Test automatic schema generation for simple types
#[tokio::test]
async fn test_typed_elicitation_simple_schema() {
// Test that schema generation works for simple types
let schema = rmcp::handler::server::tool::schema_for_type::<UserConfirmation>();
// Verify schema contains expected fields
assert!(schema.contains_key("type"));
assert_eq!(schema.get("type"), Some(&json!("object")));
assert!(schema.contains_key("properties"));
if let Some(properties) = schema.get("properties") {
assert!(properties.is_object());
let props = properties.as_object().unwrap();
assert!(props.contains_key("confirmed"));
assert!(props.contains_key("reason"));
// Check confirmed field is boolean
if let Some(confirmed_schema) = props.get("confirmed") {
let confirmed_obj = confirmed_schema.as_object().unwrap();
assert_eq!(confirmed_obj.get("type"), Some(&json!("boolean")));
}
// Check reason field is optional string
if let Some(reason_schema) = props.get("reason") {
assert!(reason_schema.is_object());
}
}
}
/// Test automatic schema generation for complex nested types
#[tokio::test]
async fn test_typed_elicitation_complex_schema() {
// Test complex nested structure schema generation
let schema = rmcp::handler::server::tool::schema_for_type::<UserProfile>();
// Verify schema structure
assert!(schema.contains_key("type"));
assert_eq!(schema.get("type"), Some(&json!("object")));
if let Some(properties) = schema.get("properties") {
let props = properties.as_object().unwrap();
// Check required fields exist
assert!(props.contains_key("name"));
assert!(props.contains_key("email"));
assert!(props.contains_key("age"));
assert!(props.contains_key("preferences"));
// Check validation constraints for name
if let Some(name_schema) = props.get("name") {
let name_obj = name_schema.as_object().unwrap();
assert_eq!(name_obj.get("type"), Some(&json!("string")));
// Note: schemars might generate constraints differently
// The exact structure depends on schemars version
}
// Check email format constraint
if let Some(email_schema) = props.get("email") {
let email_obj = email_schema.as_object().unwrap();
assert_eq!(email_obj.get("type"), Some(&json!("string")));
}
// Check age numeric constraints
if let Some(age_schema) = props.get("age") {
let age_obj = age_schema.as_object().unwrap();
assert_eq!(age_obj.get("type"), Some(&json!("integer")));
}
}
}
/// Test enum schema generation
#[tokio::test]
async fn test_enum_schema_generation() {
// Test enum schema generation
let schema = rmcp::handler::server::tool::schema_for_type::<Theme>();
// Verify enum schema structure - schemars might use oneOf or enum depending on version
assert!(
schema.contains_key("type")
|| schema.contains_key("oneOf")
|| schema.contains_key("enum")
);
// The exact structure depends on schemars configuration, but it should be valid
let json = serde_json::to_string(&schema).unwrap();
assert!(!json.is_empty());
}
/// Test that the schema generation for nested structures works
#[tokio::test]
async fn test_nested_structure_schema() {
// Test that nested structures generate proper schemas
let preferences_schema = rmcp::handler::server::tool::schema_for_type::<UserPreferences>();
// Verify basic structure
assert!(preferences_schema.contains_key("type"));
assert_eq!(preferences_schema.get("type"), Some(&json!("object")));
if let Some(properties) = preferences_schema.get("properties") {
let props = properties.as_object().unwrap();
assert!(props.contains_key("theme"));
assert!(props.contains_key("notifications"));
assert!(props.contains_key("language"));
}
}
}
// =============================================================================
// ELICITATION ENUM TESTS (MCP 2025-11-25 COMPLIANCE)
// =============================================================================
/// Test elicitation with multi-select enum schema
#[tokio::test]
async fn test_elicitation_multi_select_enum() {
let enum_schema = EnumSchema::builder(vec!["A".into(), "B".into(), "C".into()])
.multiselect()
.enum_titles(vec![
"A name".to_string(),
"B name".to_string(),
"C name".to_string(),
])
.expect("Number of title must match number of enum values")
.min_items(1)
.expect("Min items must be less than or equal to number of enum values")
.max_items(2)
.expect("Max items must be less than or equal to number of enum values")
.build();
let schema = ElicitationSchema::builder()
.required_enum_schema("choices", enum_schema)
.build()
.unwrap();
let request = CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Please provide your user information".to_string(),
requested_schema: schema,
};
// Test that complex schemas serialize/deserialize correctly
let json = serde_json::to_value(&request).unwrap();
let deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap();
match deserialized {
CreateElicitationRequestParams::FormElicitationParams {
message,
requested_schema,
..
} => {
assert_eq!(message, "Please provide your user information");
assert_eq!(requested_schema.properties.len(), 1);
assert!(requested_schema.properties.contains_key("choices"));
assert_eq!(requested_schema.required, Some(vec!["choices".to_string()]));
assert!(matches!(
requested_schema.properties.get("choices").unwrap(),
PrimitiveSchema::Enum(EnumSchema::Multi(_))
));
if let Some(PrimitiveSchema::Enum(schema)) = requested_schema.properties.get("choices")
{
assert_eq!(
schema,
&EnumSchema::Multi(MultiSelectEnumSchema::Titled(
TitledMultiSelectEnumSchema::new(TitledItems {
any_of: vec![
ConstTitle {
const_: "A".to_string(),
title: "A name".to_string()
},
ConstTitle {
const_: "B".to_string(),
title: "B name".to_string()
},
ConstTitle {
const_: "C".to_string(),
title: "C name".to_string()
},
],
})
.with_min_items(1)
.with_max_items(2)
))
)
}
}
_ => panic!("Expected FormElicitationParam variant"),
}
}
/// Test elicitation with single-select enum schema
#[tokio::test]
async fn test_elicitation_single_select_enum() {
let enum_schema = EnumSchema::builder(vec!["A".into(), "B".into(), "C".into()])
.enum_titles(vec![
"A name".to_string(),
"B name".to_string(),
"C name".to_string(),
])
.expect("Number of title must match number of enum values")
.build();
let schema = ElicitationSchema::builder()
.required_enum_schema("choices", enum_schema)
.build()
.unwrap();
let request = CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Please provide your user information".to_string(),
requested_schema: schema,
};
// Test that complex schemas serialize/deserialize correctly
let json = serde_json::to_value(&request).unwrap();
let deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap();
match deserialized {
CreateElicitationRequestParams::FormElicitationParams {
message,
requested_schema,
..
} => {
assert_eq!(message, "Please provide your user information");
assert_eq!(requested_schema.properties.len(), 1);
assert!(requested_schema.properties.contains_key("choices"));
assert_eq!(requested_schema.required, Some(vec!["choices".to_string()]));
assert!(matches!(
requested_schema.properties.get("choices").unwrap(),
PrimitiveSchema::Enum(EnumSchema::Single(_))
));
if let Some(PrimitiveSchema::Enum(schema)) = requested_schema.properties.get("choices")
{
assert_eq!(
schema,
&EnumSchema::Single(SingleSelectEnumSchema::Titled(
TitledSingleSelectEnumSchema::new(vec![
ConstTitle {
const_: "A".to_string(),
title: "A name".to_string()
},
ConstTitle {
const_: "B".to_string(),
title: "B name".to_string()
},
ConstTitle {
const_: "C".to_string(),
title: "C name".to_string()
}
])
))
)
}
}
_ => panic!("Expected FormElicitationParam variant"),
}
}
// =============================================================================
// ELICITATION DIRECTION TESTS (MCP 2025-06-18 COMPLIANCE)
// =============================================================================
/// Test that elicitation requests flow from server to client (not client to server)
/// This verifies compliance with MCP 2025-06-18 specification
#[cfg(all(feature = "client", feature = "server"))]
#[tokio::test]
async fn test_elicitation_direction_server_to_client() {
use rmcp::model::*;
use serde_json::json;
// Test that server can create elicitation requests
let schema = ElicitationSchema::builder()
.property(
"name",
PrimitiveSchema::String(StringSchema::new().description("Enter your name")),
)
.build()
.unwrap();
let elicitation_request = CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Please enter your name".to_string(),
requested_schema: schema,
};
// Verify request can be serialized
let serialized = serde_json::to_value(&elicitation_request).unwrap();
assert_eq!(serialized["message"], "Please enter your name");
assert_eq!(serialized["requestedSchema"]["type"], "object");
// Test that elicitation requests are part of ServerRequest
let _server_request =
ServerRequest::CreateElicitationRequest(CreateElicitationRequest::new(elicitation_request));
// Test that client can respond with elicitation results
let client_result = ClientResult::CreateElicitationResult(CreateElicitationResult {
action: ElicitationAction::Accept,
content: Some(json!("John Doe")),
meta: None,
});
// Verify client result can be serialized
match client_result {
ClientResult::CreateElicitationResult(result) => {
assert_eq!(result.action, ElicitationAction::Accept);
assert_eq!(result.content, Some(json!("John Doe")));
}
_ => panic!("CreateElicitationResult should be part of ClientResult"),
}
}
/// Test complete JSON-RPC message flow: Server → Client → Server
#[cfg(all(feature = "client", feature = "server"))]
#[tokio::test]
async fn test_elicitation_json_rpc_direction() {
use rmcp::model::*;
use serde_json::json;
let schema = ElicitationSchema::builder()
.property(
"continue",
PrimitiveSchema::Boolean(BooleanSchema::new().description("Do you want to continue?")),
)
.build()
.unwrap();
// 1. Server creates elicitation request
let server_request = ServerJsonRpcMessage::request(
ServerRequest::CreateElicitationRequest(CreateElicitationRequest::new(
CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Do you want to continue?".to_string(),
requested_schema: schema,
},
)),
RequestId::Number(1),
);
// Serialize server request
let server_json = serde_json::to_value(&server_request).unwrap();
assert_eq!(server_json["method"], "elicitation/create");
assert_eq!(server_json["id"], 1);
assert_eq!(server_json["params"]["message"], "Do you want to continue?");
// 2. Client responds with elicitation result
let client_response = ClientJsonRpcMessage::response(
ClientResult::CreateElicitationResult(CreateElicitationResult {
action: ElicitationAction::Accept,
content: Some(json!(true)),
meta: None,
}),
RequestId::Number(1),
);
// Serialize client response
let client_json = serde_json::to_value(&client_response).unwrap();
assert_eq!(client_json["id"], 1);
if let Some(result) = client_json["result"].as_object() {
assert_eq!(result["action"], "accept");
assert_eq!(result["content"], true);
} else {
panic!("Client response should contain result");
}
}
/// Test all three elicitation actions according to MCP spec
#[cfg(all(feature = "client", feature = "server"))]
#[tokio::test]
async fn test_elicitation_actions_compliance() {
use rmcp::model::*;
// Test all three elicitation actions according to MCP spec
let actions = [
ElicitationAction::Accept,
ElicitationAction::Decline,
ElicitationAction::Cancel,
];
for action in actions {
let result = CreateElicitationResult {
action: action.clone(),
content: match action {
ElicitationAction::Accept => Some(serde_json::json!("some data")),
_ => None,
},
meta: None,
};
let json = serde_json::to_value(&result).unwrap();
match action {
ElicitationAction::Accept => {
assert_eq!(json["action"], "accept");
assert!(json["content"].is_string());
}
ElicitationAction::Decline => {
assert_eq!(json["action"], "decline");
assert!(json.get("content").is_none() || json["content"].is_null());
}
ElicitationAction::Cancel => {
assert_eq!(json["action"], "cancel");
assert!(json.get("content").is_none() || json["content"].is_null());
}
}
}
}
/// Test that CreateElicitationResult IS in ClientResult (response compliance)
#[tokio::test]
async fn test_elicitation_result_in_client_result() {
use rmcp::model::*;
// Test that clients can return elicitation results
let result = ClientResult::CreateElicitationResult(CreateElicitationResult {
action: ElicitationAction::Decline,
content: None,
meta: None,
});
match result {
ClientResult::CreateElicitationResult(elicit_result) => {
assert_eq!(elicit_result.action, ElicitationAction::Decline);
assert_eq!(elicit_result.content, None);
}
_ => panic!("CreateElicitationResult should be part of ClientResult"),
}
}
// =============================================================================
// ELICITATION CAPABILITIES TESTS
// =============================================================================
/// Test ElicitationCapability structure and serialization
#[tokio::test]
async fn test_elicitation_capability_structure() {
// Test default ElicitationCapability
let default_cap = ElicitationCapability::default();
assert!(default_cap.form.is_none());
assert!(default_cap.url.is_none());
// Test ElicitationCapability with schema validation enabled
let cap_with_validation = ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
};
assert_eq!(
cap_with_validation.form.as_ref().unwrap().schema_validation,
Some(true)
);
// Test ElicitationCapability with schema validation disabled
let cap_without_validation = ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(false),
}),
url: None,
};
assert_eq!(
cap_without_validation
.form
.as_ref()
.unwrap()
.schema_validation,
Some(false)
);
// Test JSON serialization
let json = serde_json::to_value(&cap_with_validation).unwrap();
assert_eq!(
json,
serde_json::json!({
"form": {
"schemaValidation": true
}
})
);
// Test JSON deserialization
let deserialized: ElicitationCapability = serde_json::from_value(json).unwrap();
assert_eq!(
deserialized.form.as_ref().unwrap().schema_validation,
Some(true)
);
}
/// Test ClientCapabilities with elicitation capability
#[tokio::test]
async fn test_client_capabilities_with_elicitation() {
// Test ClientCapabilities with elicitation capability
let capabilities = ClientCapabilities::builder()
.enable_elicitation_with(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
})
.build();
// Verify elicitation capability is present
assert!(capabilities.elicitation.is_some());
assert_eq!(
capabilities
.elicitation
.as_ref()
.unwrap()
.form
.as_ref()
.unwrap()
.schema_validation,
Some(true)
);
// Test JSON serialization
let json = serde_json::to_value(&capabilities).unwrap();
assert!(
json["elicitation"]["form"]["schemaValidation"]
.as_bool()
.unwrap_or(false)
);
// Test ClientCapabilities without elicitation
let capabilities_without = ClientCapabilities::default();
assert!(capabilities_without.elicitation.is_none());
}
/// Test InitializeRequestParam with elicitation capability
#[tokio::test]
async fn test_initialize_request_with_elicitation() {
// Test InitializeRequestParam with elicitation capability
let init_param = InitializeRequestParams::new(
ClientCapabilities::builder()
.enable_elicitation_with(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
})
.build(),
Implementation::new("test-client", "1.0.0"),
);
// Verify the structure
assert!(init_param.capabilities.elicitation.is_some());
assert_eq!(
init_param
.capabilities
.elicitation
.as_ref()
.unwrap()
.form
.as_ref()
.unwrap()
.schema_validation,
Some(true)
);
// Test JSON serialization
let json = serde_json::to_value(&init_param).unwrap();
assert!(
json["capabilities"]["elicitation"]["form"]["schemaValidation"]
.as_bool()
.unwrap_or(false)
);
}
/// Test capability checking logic (simulated)
#[tokio::test]
async fn test_capability_checking_logic() {
// Simulate the logic that would be used in supports_elicitation()
// Case 1: Client with elicitation capability
let client_with_capability = InitializeRequestParams::new(
ClientCapabilities::builder()
.enable_elicitation_with(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
})
.build(),
Implementation::new("test-client", "1.0.0"),
);
// Simulate supports_elicitation() logic
let supports_elicitation = client_with_capability.capabilities.elicitation.is_some();
assert!(supports_elicitation);
// Case 2: Client without elicitation capability
let client_without_capability = InitializeRequestParams::new(
ClientCapabilities::default(),
Implementation::new("test-client", "1.0.0"),
);
let supports_elicitation = client_without_capability.capabilities.elicitation.is_some();
assert!(!supports_elicitation);
}
/// Test CapabilityNotSupported error message formatting
#[tokio::test]
async fn test_capability_not_supported_error_message() {
let error = ElicitationError::CapabilityNotSupported;
let message = format!("{}", error);
assert_eq!(
message,
"Client does not support elicitation - capability not declared during initialization"
);
}
/// Test all ElicitationError variants and their messages
#[tokio::test]
async fn test_elicitation_error_variants() {
// Test CapabilityNotSupported
let capability_error = ElicitationError::CapabilityNotSupported;
assert_eq!(
format!("{}", capability_error),
"Client does not support elicitation - capability not declared during initialization"
);
// Test UserDeclined
let user_declined = ElicitationError::UserDeclined;
assert_eq!(
format!("{}", user_declined),
"User explicitly declined the request"
);
// Test UserCancelled
let user_cancelled = ElicitationError::UserCancelled;
assert_eq!(
format!("{}", user_cancelled),
"User cancelled/dismissed the request"
);
// Test NoContent
let no_content = ElicitationError::NoContent;
assert_eq!(format!("{}", no_content), "No response content provided");
// Test Service error
let service_error = ElicitationError::Service(ServiceError::UnexpectedResponse);
let message = format!("{}", service_error);
assert!(message.starts_with("Service error:"));
// Test ParseError
let json_error = serde_json::from_str::<i32>("\"not_an_integer\"").unwrap_err();
let data = serde_json::json!({"key": "value"});
let parse_error = ElicitationError::ParseError {
error: json_error,
data: data.clone(),
};
let message = format!("{}", parse_error);
assert!(message.starts_with("Failed to parse response data:"));
assert!(message.contains("Received data:"));
// Test error matching
match capability_error {
ElicitationError::CapabilityNotSupported => {} // Expected
_ => panic!("Should match CapabilityNotSupported"),
}
match user_declined {
ElicitationError::UserDeclined => {} // Expected
_ => panic!("Should match UserDeclined"),
}
match user_cancelled {
ElicitationError::UserCancelled => {} // Expected
_ => panic!("Should match UserCancelled"),
}
match no_content {
ElicitationError::NoContent => {} // Expected
_ => panic!("Should match NoContent"),
}
}
/// Test ElicitationCapability serialization with schema validation
#[tokio::test]
async fn test_elicitation_capability_serialization() {
use rmcp::model::ElicitationCapability;
// Test default capability (no schema validation)
let default_cap = ElicitationCapability::default();
let json = serde_json::to_value(&default_cap).unwrap();
// Should serialize to empty object when no fields are set
assert_eq!(json, serde_json::json!({}));
// Test capability with schema validation enabled
let cap_with_validation = ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
};
let json = serde_json::to_value(&cap_with_validation).unwrap();
assert_eq!(
json,
serde_json::json!({
"form": {
"schemaValidation": true
}
})
);
// Test capability with schema validation disabled
let cap_without_validation = ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(false),
}),
url: None,
};
let json = serde_json::to_value(&cap_without_validation).unwrap();
assert_eq!(
json,
serde_json::json!({
"form": {
"schemaValidation": false
}
})
);
// Test deserialization
let deserialized: ElicitationCapability = serde_json::from_value(serde_json::json!({
"form":{"schemaValidation": true}
}))
.unwrap();
assert_eq!(deserialized.form.unwrap().schema_validation, Some(true));
}
/// Test ClientCapabilities builder with elicitation capability methods
#[tokio::test]
async fn test_client_capabilities_elicitation_builder() {
use rmcp::model::{ClientCapabilities, ElicitationCapability};
// Test enabling elicitation capability
let caps = ClientCapabilities::builder().enable_elicitation().build();
assert!(caps.elicitation.is_some());
assert_eq!(caps.elicitation.as_ref().unwrap().form, None);
// Test enabling elicitation with schema validation
let caps_with_validation = ClientCapabilities::builder()
.enable_elicitation()
.enable_elicitation_schema_validation()
.build();
assert!(caps_with_validation.elicitation.is_some());
assert_eq!(
caps_with_validation
.elicitation
.as_ref()
.unwrap()
.form
.as_ref()
.unwrap()
.schema_validation,
Some(true)
);
// Test enabling elicitation with custom capability
let custom_elicitation = ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(false),
}),
url: None,
};
let caps_custom = ClientCapabilities::builder()
.enable_elicitation_with(custom_elicitation.clone())
.build();
assert!(caps_custom.elicitation.is_some());
assert_eq!(
caps_custom.elicitation.as_ref().unwrap(),
&custom_elicitation
);
}
// =============================================================================
// TIMEOUT TESTS
// =============================================================================
/// Test basic timeout functionality for create_elicitation_with_timeout
#[tokio::test]
async fn test_create_elicitation_with_timeout_basic() {
use std::time::Duration;
// This test verifies that the method accepts timeout parameter
let schema = ElicitationSchema::builder()
.required_property("name", PrimitiveSchema::String(StringSchema::new()))
.required_property("email", PrimitiveSchema::String(StringSchema::new()))
.build()
.unwrap();
let _params = CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Enter your details".to_string(),
requested_schema: schema,
};
// Test different timeout values
let timeout_short = Duration::from_millis(100);
let timeout_long = Duration::from_secs(30);
let timeout_none: Option<Duration> = None;
// Verify timeout parameter types are correct
assert!(!timeout_short.is_zero());
assert!(!timeout_long.is_zero());
assert!(timeout_none.is_none());
// Verify timeout values are reasonable
assert_eq!(timeout_short.as_millis(), 100);
assert_eq!(timeout_long.as_secs(), 30);
}
/// Test timeout behavior with elicit_with_timeout method
#[tokio::test]
async fn test_elicit_with_timeout_method_signature() {
use std::time::Duration;
// Test that method signature works with different timeout values
let timeout_values = vec![
None,
Some(Duration::from_millis(500)),
Some(Duration::from_secs(1)),
Some(Duration::from_secs(30)),
Some(Duration::from_secs(60)),
];
for timeout in timeout_values {
// Verify timeout value is properly handled
match timeout {
None => assert!(timeout.is_none()),
Some(duration) => {
assert!(duration > Duration::from_millis(0));
assert!(duration <= Duration::from_secs(300)); // Max 5 minutes
}
}
}
}
/// Test timeout value validation
#[tokio::test]
async fn test_timeout_value_validation() {
use std::time::Duration;
// Test valid timeout ranges
let valid_timeouts = vec![
Duration::from_millis(1), // Minimum
Duration::from_millis(100), // Short
Duration::from_secs(1), // 1 second
Duration::from_secs(30), // 30 seconds
Duration::from_secs(300), // 5 minutes
];
for timeout in valid_timeouts {
assert!(timeout >= Duration::from_millis(1));
assert!(timeout <= Duration::from_secs(300));
}
// Test edge cases
let zero_timeout = Duration::from_millis(0);
let very_long_timeout = Duration::from_secs(3600); // 1 hour
// Zero timeout should be handled gracefully
assert_eq!(zero_timeout, Duration::from_millis(0));
// Very long timeout should work but may not be practical
assert!(very_long_timeout > Duration::from_secs(300));
}
/// Test timeout error message formatting
#[tokio::test]
async fn test_timeout_error_formatting() {
use std::time::Duration;
let timeout = Duration::from_secs(30);
// Simulate a timeout error
let timeout_error = ServiceError::Timeout { timeout };
// Verify error contains timeout information
let error_string = format!("{}", timeout_error);
assert!(error_string.contains("timeout"));
assert!(error_string.contains("30"));
}
/// Test elicitation error handling with timeout
#[tokio::test]
async fn test_elicitation_timeout_error_conversion() {
use std::time::Duration;
let timeout = Duration::from_millis(500);
let service_timeout_error = ServiceError::Timeout { timeout };
let elicitation_error = ElicitationError::Service(service_timeout_error);
// Verify error chain is preserved
match elicitation_error {
ElicitationError::Service(ServiceError::Timeout { timeout: t }) => {
assert_eq!(t, timeout);
}
_ => panic!("Expected timeout error"),
}
}
/// Test timeout parameter pass-through in PeerRequestOptions
#[tokio::test]
async fn test_peer_request_options_timeout() {
use std::time::Duration;
let timeout = Some(Duration::from_secs(15));
let mut options = PeerRequestOptions::default();
options.timeout = timeout;
// Verify timeout is properly stored
assert_eq!(options.timeout, timeout);
assert!(options.meta.is_none());
// Test with no timeout
let options_no_timeout = PeerRequestOptions::default();
assert!(options_no_timeout.timeout.is_none());
}
/// Test realistic timeout scenarios
#[tokio::test]
async fn test_realistic_timeout_scenarios() {
use std::time::Duration;
// Test common timeout scenarios users might encounter
// Quick response (5 seconds)
let quick_timeout = Duration::from_secs(5);
assert!(quick_timeout >= Duration::from_secs(1));
assert!(quick_timeout <= Duration::from_secs(10));
// Normal interaction (30 seconds)
let normal_timeout = Duration::from_secs(30);
assert!(normal_timeout >= Duration::from_secs(10));
assert!(normal_timeout <= Duration::from_secs(60));
// Long form input (2 minutes)
let long_timeout = Duration::from_secs(120);
assert!(long_timeout >= Duration::from_secs(60));
assert!(long_timeout <= Duration::from_secs(300));
}
/// Test that different ElicitationAction values map to correct error types
#[tokio::test]
async fn test_elicitation_action_error_mapping() {
use rmcp::{model::ElicitationAction, service::ElicitationError};
// Test that each action type produces the expected error
let test_cases = vec![
(ElicitationAction::Decline, "UserDeclined"),
(ElicitationAction::Cancel, "UserCancelled"),
];
for (action, _expected_error_type) in test_cases {
// Verify that the action exists and has the expected semantics
match action {
ElicitationAction::Accept => {
// Accept should not produce an error (it provides content)
}
ElicitationAction::Decline => {
// Should map to UserDeclined error
let error = ElicitationError::UserDeclined;
assert!(format!("{}", error).contains("explicitly declined"));
}
ElicitationAction::Cancel => {
// Should map to UserCancelled error
let error = ElicitationError::UserCancelled;
assert!(format!("{}", error).contains("cancelled/dismissed"));
}
}
}
}
/// Test elicitation action semantics according to MCP specification
#[tokio::test]
async fn test_elicitation_action_semantics() {
use rmcp::model::ElicitationAction;
// According to MCP spec:
// - Accept: User explicitly approved and submitted with data
// - Decline: User explicitly declined the request
// - Cancel: User dismissed without making an explicit choice
// Test that all three actions are available
let actions = vec![
ElicitationAction::Accept,
ElicitationAction::Decline,
ElicitationAction::Cancel,
];
assert_eq!(actions.len(), 3);
// Test serialization/deserialization
for action in actions {
let serialized = serde_json::to_string(&action).expect("Should serialize");
let deserialized: ElicitationAction =
serde_json::from_str(&serialized).expect("Should deserialize");
// Actions should round-trip correctly
match (action, deserialized) {
(ElicitationAction::Accept, ElicitationAction::Accept) => {}
(ElicitationAction::Decline, ElicitationAction::Decline) => {}
(ElicitationAction::Cancel, ElicitationAction::Cancel) => {}
_ => panic!("Action serialization round-trip failed"),
}
}
}
/// Test compile-time type safety for elicitation
#[tokio::test]
async fn test_elicitation_type_safety() {
use rmcp::service::ElicitationSafe;
use schemars::JsonSchema;
// Test that our types implement ElicitationSafe
#[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
struct SafeType {
name: String,
value: i32,
}
rmcp::elicit_safe!(SafeType);
// Verify that SafeType implements the required traits
fn assert_elicitation_safe<T: ElicitationSafe>() {}
assert_elicitation_safe::<SafeType>();
// Test that SafeType can generate schema (compile-time check)
let _schema = schemars::schema_for!(SafeType);
}
/// Test that elicit_safe! macro works with multiple types
#[tokio::test]
async fn test_elicit_safe_macro() {
use schemars::JsonSchema;
#[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
struct TypeA {
field_a: String,
}
#[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
struct TypeB {
field_b: i32,
}
#[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
struct TypeC {
field_c: bool,
}
// Test macro with multiple types
rmcp::elicit_safe!(TypeA, TypeB, TypeC);
// All should implement ElicitationSafe
fn assert_all_safe<T: rmcp::service::ElicitationSafe>() {}
assert_all_safe::<TypeA>();
assert_all_safe::<TypeB>();
assert_all_safe::<TypeC>();
}
/// Test ElicitationSafe trait behavior
#[tokio::test]
async fn test_elicitation_safe_trait() {
use schemars::JsonSchema;
// Test object type validation
#[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
struct ObjectType {
name: String,
count: usize,
active: bool,
}
rmcp::elicit_safe!(ObjectType);
// Test that ObjectType can generate schema (compile-time check)
let _schema = schemars::schema_for!(ObjectType);
}
/// Test documentation examples compile correctly
#[tokio::test]
async fn test_elicitation_examples_compile() {
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
// Example from trait documentation
#[allow(dead_code)]
#[derive(Serialize, Deserialize, JsonSchema)]
struct UserProfile {
name: String,
email: String,
}
rmcp::elicit_safe!(UserProfile);
// This should compile and work
fn _example_usage() {
fn _assert_safe<T: rmcp::service::ElicitationSafe>() {}
_assert_safe::<UserProfile>();
}
}
// =============================================================================
// BUILD-TIME VALIDATION TESTS
// =============================================================================
/// Test that build() validates required fields exist in properties
#[tokio::test]
async fn test_build_validation_required_field_not_in_properties() {
// Try to mark a field as required that doesn't exist in properties
let result = ElicitationSchema::builder()
.property("email", PrimitiveSchema::String(StringSchema::email()))
.mark_required("nonexistent_field")
.build();
// Should return an error
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Required field does not exist in properties"
);
}
/// Test that build() succeeds when all required fields exist
#[tokio::test]
async fn test_build_validation_required_field_exists() {
let result = ElicitationSchema::builder()
.property("email", PrimitiveSchema::String(StringSchema::email()))
.property("name", PrimitiveSchema::String(StringSchema::new()))
.mark_required("email")
.mark_required("name")
.build();
// Should succeed
assert!(result.is_ok());
let schema = result.unwrap();
assert_eq!(schema.properties.len(), 2);
assert_eq!(
schema.required,
Some(vec!["email".to_string(), "name".to_string()])
);
}
/// Test that build_unchecked() panics on validation errors
#[tokio::test]
#[should_panic(expected = "Invalid elicitation schema")]
async fn test_build_unchecked_panics_on_invalid() {
// build_unchecked validates but panics instead of returning Result
let _schema = ElicitationSchema::builder()
.property("email", PrimitiveSchema::String(StringSchema::email()))
.mark_required("nonexistent_field")
.build_unchecked();
}
/// Test convenience methods handle validation correctly
#[tokio::test]
async fn test_convenience_methods_validation() {
// required_string_property should add both property and mark as required
let result = ElicitationSchema::builder()
.required_string_property("name", |s| s)
.required_email("email")
.build();
assert!(result.is_ok());
let schema = result.unwrap();
assert_eq!(schema.properties.len(), 2);
assert!(
schema
.required
.as_ref()
.unwrap()
.contains(&"name".to_string())
);
assert!(
schema
.required
.as_ref()
.unwrap()
.contains(&"email".to_string())
);
}
/// Test typed property methods work correctly
#[tokio::test]
async fn test_typed_property_methods() {
let result = ElicitationSchema::builder()
.string_property("name", |s| s.length(1, 100))
.number_property("price", |n| n.range(0.0, 1000.0))
.integer_property("quantity", |i| i.range(1, 100))
.bool_property("in_stock", |b| b.with_default(true))
.build();
assert!(result.is_ok());
let schema = result.unwrap();
assert_eq!(schema.properties.len(), 4);
// Verify types are correct
if let Some(PrimitiveSchema::String(_)) = schema.properties.get("name") {
// Expected
} else {
panic!("name should be StringSchema");
}
if let Some(PrimitiveSchema::Number(_)) = schema.properties.get("price") {
// Expected
} else {
panic!("price should be NumberSchema");
}
if let Some(PrimitiveSchema::Integer(_)) = schema.properties.get("quantity") {
// Expected
} else {
panic!("quantity should be IntegerSchema");
}
if let Some(PrimitiveSchema::Boolean(_)) = schema.properties.get("in_stock") {
// Expected
} else {
panic!("in_stock should be BooleanSchema");
}
}
/// Test required typed property methods
#[tokio::test]
async fn test_required_typed_property_methods() {
let result = ElicitationSchema::builder()
.required_string_property("name", |s| s)
.required_number_property("price", |n| n)
.required_integer_property("age", |i| i)
.required_bool_property("active", |b| b)
.build();
assert!(result.is_ok());
let schema = result.unwrap();
assert_eq!(schema.properties.len(), 4);
assert_eq!(schema.required.as_ref().unwrap().len(), 4);
// All should be marked as required
let required = schema.required.as_ref().unwrap();
assert!(required.contains(&"name".to_string()));
assert!(required.contains(&"price".to_string()));
assert!(required.contains(&"age".to_string()));
assert!(required.contains(&"active".to_string()));
}
// =============================================================================
// URL ELICITATION TESTS
// =============================================================================
/// Test URL elicitation request parameter serialization/deserialization
#[tokio::test]
async fn test_url_elicitation_request_param_serialization() {
let request_param = CreateElicitationRequestParams::UrlElicitationParams {
meta: None,
message: "Please visit the following URL to complete verification".to_string(),
url: "https://example.com/verify".to_string(),
elicitation_id: "elicit-123".to_string(),
};
// Test serialization
let json = serde_json::to_value(&request_param).unwrap();
let expected = json!({
"mode": "url",
"message": "Please visit the following URL to complete verification",
"url": "https://example.com/verify",
"elicitationId": "elicit-123"
});
assert_eq!(json, expected);
// Test deserialization
let deserialized: CreateElicitationRequestParams = serde_json::from_value(expected).unwrap();
match deserialized {
CreateElicitationRequestParams::UrlElicitationParams {
message,
url,
elicitation_id,
..
} => {
assert_eq!(
message,
"Please visit the following URL to complete verification"
);
assert_eq!(url, "https://example.com/verify");
assert_eq!(elicitation_id, "elicit-123");
}
_ => panic!("Expected UrlElicitationParam variant"),
}
}
/// Test URL elicitation request in JSON-RPC protocol
#[tokio::test]
async fn test_url_elicitation_json_rpc_protocol() {
// Create a complete JSON-RPC request for URL elicitation
let request = JsonRpcRequest {
jsonrpc: JsonRpcVersion2_0,
id: RequestId::Number(1),
request: CreateElicitationRequest::new(
CreateElicitationRequestParams::UrlElicitationParams {
meta: None,
message: "Please authorize this action at the following URL".to_string(),
url: "https://auth.example.com/authorize/abc123".to_string(),
elicitation_id: "auth-request-456".to_string(),
},
),
};
// Test serialization of complete request
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["jsonrpc"], "2.0");
assert_eq!(json["id"], 1);
assert_eq!(json["method"], "elicitation/create");
assert_eq!(json["params"]["mode"], "url");
assert_eq!(
json["params"]["message"],
"Please authorize this action at the following URL"
);
assert_eq!(
json["params"]["url"],
"https://auth.example.com/authorize/abc123"
);
assert_eq!(json["params"]["elicitationId"], "auth-request-456");
// Test deserialization
let deserialized: JsonRpcRequest<CreateElicitationRequest> =
serde_json::from_value(json).unwrap();
assert_eq!(deserialized.id, RequestId::Number(1));
match &deserialized.request.params {
CreateElicitationRequestParams::UrlElicitationParams {
message,
url,
elicitation_id,
..
} => {
assert_eq!(message, "Please authorize this action at the following URL");
assert_eq!(url, "https://auth.example.com/authorize/abc123");
assert_eq!(elicitation_id, "auth-request-456");
}
_ => panic!("Expected UrlElicitationParam variant"),
}
}
/// Test ElicitationCompletionNotification serialization/deserialization
#[tokio::test]
async fn test_elicitation_completion_notification() {
let notification_params = ElicitationResponseNotificationParam {
elicitation_id: "elicit-789".to_string(),
};
// Test serialization
let json = serde_json::to_value(&notification_params).unwrap();
let expected = json!({
"elicitationId": "elicit-789"
});
assert_eq!(json, expected);
// Test deserialization
let deserialized: ElicitationResponseNotificationParam =
serde_json::from_value(expected).unwrap();
assert_eq!(deserialized.elicitation_id, "elicit-789");
// Test complete notification structure
let notification = ElicitationCompletionNotification::new(notification_params);
let json = serde_json::to_value(&notification).unwrap();
assert_eq!(json["method"], "notifications/elicitation/complete");
assert_eq!(json["params"]["elicitationId"], "elicit-789");
}
/// Test UrlElicitationCapability structure and serialization
#[tokio::test]
async fn test_url_elicitation_capability() {
// Test default UrlElicitationCapability
let url_cap = UrlElicitationCapability::default();
// Test serialization - should be empty object
let json = serde_json::to_value(&url_cap).unwrap();
assert_eq!(json, json!({}));
// Test deserialization
let deserialized: UrlElicitationCapability = serde_json::from_value(json!({})).unwrap();
assert_eq!(deserialized, url_cap);
// Test ElicitationCapability with URL mode enabled
let elicitation_cap = ElicitationCapability {
form: None,
url: Some(UrlElicitationCapability::default()),
};
let json = serde_json::to_value(&elicitation_cap).unwrap();
assert_eq!(
json,
json!({
"url": {}
})
);
// Test ElicitationCapability with both form and URL modes
let both_cap = ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: Some(UrlElicitationCapability::default()),
};
let json = serde_json::to_value(&both_cap).unwrap();
assert_eq!(
json,
json!({
"form": {
"schemaValidation": true
},
"url": {}
})
);
}
/// Test backward compatibility: CreateElicitationRequestParam without mode tag
#[tokio::test]
async fn test_elicitation_backward_compatibility_no_mode() {
// JSON without "mode" field should deserialize as FormElicitationParam
let json_without_mode = json!({
"message": "Please enter your details",
"requestedSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": ["name"]
}
});
let deserialized: CreateElicitationRequestParams =
serde_json::from_value(json_without_mode).unwrap();
match deserialized {
CreateElicitationRequestParams::FormElicitationParams {
message,
requested_schema,
..
} => {
assert_eq!(message, "Please enter your details");
assert_eq!(requested_schema.properties.len(), 1);
assert!(requested_schema.properties.contains_key("name"));
}
_ => panic!("Expected FormElicitationParam for backward compatibility"),
}
}
/// Test both form and URL elicitation modes in the same test
#[tokio::test]
async fn test_elicitation_both_modes() {
// Form mode
let form_schema = ElicitationSchema::builder()
.required_property("email", PrimitiveSchema::String(StringSchema::email()))
.build()
.unwrap();
let form_request = CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Enter email".to_string(),
requested_schema: form_schema,
};
let form_json = serde_json::to_value(&form_request).unwrap();
assert_eq!(form_json["mode"], "form");
assert!(form_json.get("requestedSchema").is_some());
assert!(form_json.get("url").is_none());
// URL mode
let url_request = CreateElicitationRequestParams::UrlElicitationParams {
meta: None,
message: "Visit URL".to_string(),
url: "https://example.com".to_string(),
elicitation_id: "id-123".to_string(),
};
let url_json = serde_json::to_value(&url_request).unwrap();
assert_eq!(url_json["mode"], "url");
assert!(url_json.get("url").is_some());
assert!(url_json.get("elicitationId").is_some());
assert!(url_json.get("requestedSchema").is_none());
}
/// Test URL_ELICITATION_REQUIRED error code
#[tokio::test]
async fn test_url_elicitation_required_error_code() {
// Test the error code constant
assert_eq!(ErrorCode::URL_ELICITATION_REQUIRED.0, -32042);
// Test creating error data with URL_ELICITATION_REQUIRED
let error_data = ErrorData::url_elicitation_required(
"URL elicitation is required for this operation",
Some(json!({
"url": "https://example.com/complete",
"elicitationId": "elicit-999"
})),
);
assert_eq!(error_data.code, ErrorCode::URL_ELICITATION_REQUIRED);
assert_eq!(
error_data.message,
"URL elicitation is required for this operation"
);
assert!(error_data.data.is_some());
// Test serialization
let json = serde_json::to_value(&error_data).unwrap();
assert_eq!(json["code"], -32042);
assert_eq!(
json["message"],
"URL elicitation is required for this operation"
);
assert_eq!(json["data"]["url"], "https://example.com/complete");
assert_eq!(json["data"]["elicitationId"], "elicit-999");
}
/// Test ClientCapabilities with different elicitation mode combinations
#[tokio::test]
async fn test_client_capabilities_elicitation_modes() {
// Test with form-only capability
let form_only_caps = ClientCapabilities::builder()
.enable_elicitation_with(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
})
.build();
let json = serde_json::to_value(&form_only_caps).unwrap();
assert!(json["elicitation"]["form"].is_object());
assert!(
json["elicitation"]["url"].is_null()
|| !json["elicitation"].as_object().unwrap().contains_key("url")
);
// Test with URL-only capability
let url_only_caps = ClientCapabilities::builder()
.enable_elicitation_with(ElicitationCapability {
form: None,
url: Some(UrlElicitationCapability::default()),
})
.build();
let json = serde_json::to_value(&url_only_caps).unwrap();
assert!(json["elicitation"]["url"].is_object());
assert!(
json["elicitation"]["form"].is_null()
|| !json["elicitation"]
.as_object()
.unwrap()
.contains_key("form")
);
// Test with both capabilities
let both_caps = ClientCapabilities::builder()
.enable_elicitation_with(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(false),
}),
url: Some(UrlElicitationCapability::default()),
})
.build();
let json = serde_json::to_value(&both_caps).unwrap();
assert!(json["elicitation"]["form"].is_object());
assert!(json["elicitation"]["url"].is_object());
}
/// Test ElicitationCompletionNotification in ServerNotification enum
#[tokio::test]
async fn test_elicitation_completion_in_server_notification() {
let notification_param = ElicitationResponseNotificationParam {
elicitation_id: "notify-123".to_string(),
};
let completion_notification =
ElicitationCompletionNotification::new(notification_param.clone());
// Test that it's part of ServerNotification
let server_notification =
ServerNotification::ElicitationCompletionNotification(completion_notification);
// Test serialization
let json = serde_json::to_value(&server_notification).unwrap();
assert_eq!(json["method"], "notifications/elicitation/complete");
assert_eq!(json["params"]["elicitationId"], "notify-123");
// Test deserialization
let deserialized: ServerNotification = serde_json::from_value(json).unwrap();
match deserialized {
ServerNotification::ElicitationCompletionNotification(notif) => {
assert_eq!(notif.params.elicitation_id, "notify-123");
}
_ => panic!("Expected ElicitationCompletionNotification variant"),
}
}
/// Test ElicitationAction with URL elicitation workflow
#[tokio::test]
async fn test_url_elicitation_action_workflow() {
// Test Accept action for URL elicitation (user visited URL and confirmed)
let accept_result = CreateElicitationResult {
action: ElicitationAction::Accept,
content: None, // URL elicitation doesn't return content, just confirmation
meta: None,
};
let json = serde_json::to_value(&accept_result).unwrap();
assert_eq!(json["action"], "accept");
// content should be omitted when None
assert!(json.get("content").is_none() || json["content"].is_null());
// Test Decline action for URL elicitation
let decline_result = CreateElicitationResult {
action: ElicitationAction::Decline,
content: None,
meta: None,
};
let json = serde_json::to_value(&decline_result).unwrap();
assert_eq!(json["action"], "decline");
// Test Cancel action for URL elicitation
let cancel_result = CreateElicitationResult {
action: ElicitationAction::Cancel,
content: None,
meta: None,
};
let json = serde_json::to_value(&cancel_result).unwrap();
assert_eq!(json["action"], "cancel");
}
/// Test method constants for URL elicitation
#[tokio::test]
async fn test_elicitation_method_constants() {
// Test existing methods
assert_eq!(ElicitationCreateRequestMethod::VALUE, "elicitation/create");
assert_eq!(
ElicitationResponseNotificationMethod::VALUE,
"notifications/elicitation/response"
);
// Test new completion notification method
assert_eq!(
ElicitationCompletionNotificationMethod::VALUE,
"notifications/elicitation/complete"
);
}