use crank_community_mcp::tool_error::{ tool_error_contract_from_failure, tool_error_contract_from_runtime, }; use crank_core::{ CorrelationContext, ExecutionErrorCode, ExecutionFailure, ExecutionLocale, ProtocolAdapterError, Retryability, }; use crank_runtime::RuntimeError; #[test] fn maps_upstream_429_to_recoverable_structured_tool_error() { let contract = tool_error_contract_from_runtime( &RuntimeError::ProtocolAdapter(ProtocolAdapterError::UnexpectedStatus { status: 429, dispatch: crank_core::DispatchEvidence::MayHaveDispatched, }), "req-429", "0af7651916cd43dd8448eb211c80319c", ExecutionLocale::Ru, ); assert_eq!(contract.error_code, "upstream_rate_limited"); assert_eq!(contract.stage, "upstream"); assert_eq!(contract.retryability, "after_delay"); assert_eq!(contract.outcome_certainty, "certain"); assert!(contract.recoverable); assert_eq!(contract.upstream_status, Some(429)); assert_eq!(contract.request_id, "req-429"); assert_eq!(contract.trace_id, "0af7651916cd43dd8448eb211c80319c"); assert_eq!(contract.suggested_action, Some("Повторите запрос позже.")); assert!(!contract.message.contains("internal_trace")); } #[test] fn maps_upstream_429_to_english_suggested_action() { let contract = tool_error_contract_from_runtime( &RuntimeError::ProtocolAdapter(ProtocolAdapterError::UnexpectedStatus { status: 429, dispatch: crank_core::DispatchEvidence::MayHaveDispatched, }), "req-429", "0af7651916cd43dd8448eb211c80319c", ExecutionLocale::En, ); assert_eq!(contract.error_code, "upstream_rate_limited"); assert_eq!(contract.suggested_action, Some("Retry the request later.")); } #[test] fn maps_mapping_error_to_non_recoverable_structured_tool_error() { let contract = tool_error_contract_from_runtime( &RuntimeError::InvalidPreparedRequest { field: "query.base".to_owned(), reason: "expected string".to_owned(), }, "req-map", "0af7651916cd43dd8448eb211c80319c", ExecutionLocale::Ru, ); assert_eq!(contract.error_code, "prepared_request_invalid"); assert_eq!(contract.stage, "request_preparation"); assert_eq!(contract.retryability, "never"); assert!(!contract.recoverable); assert_eq!(contract.upstream_status, None); assert_eq!(contract.request_id, "req-map"); assert_eq!(contract.trace_id, "0af7651916cd43dd8448eb211c80319c"); assert_eq!(contract.suggested_action, None); } #[test] fn ambiguous_timeout_requires_manual_reconciliation() { let contract = tool_error_contract_from_runtime( &RuntimeError::ExecutionDeadlineElapsed { may_have_dispatched: true, }, "req-timeout", "0af7651916cd43dd8448eb211c80319c", ExecutionLocale::Ru, ); assert_eq!(contract.error_code, "upstream_timeout"); assert_eq!(contract.retryability, "manual_reconcile"); assert_eq!(contract.outcome_certainty, "outcome_unknown"); assert!(!contract.recoverable); } #[test] fn every_execution_code_projects_the_same_canonical_semantics_as_admin() { for code in ExecutionErrorCode::ALL { let contract = tool_error_contract_from_failure( &ExecutionFailure::new(code, CorrelationContext::generate()), ExecutionLocale::En, ); assert_eq!(contract.error_code, code.as_str()); assert_eq!(contract.stage, code.stage().as_str()); assert_eq!(contract.retryability, code.retryability().as_str()); assert_eq!( contract.outcome_certainty, code.outcome_certainty().as_str() ); assert_eq!( contract.recoverable, matches!( code.retryability(), Retryability::Safe | Retryability::AfterDelay | Retryability::RequiresConfirmation ) ); assert!(!contract.message.is_empty()); } } #[test] fn confirmation_challenge_is_localized_for_mcp_clients() { let failure = ExecutionFailure::new( ExecutionErrorCode::ConfirmationRequired, CorrelationContext::generate(), ) .try_with_confirmation("ct_safe", 30_000) .expect("valid confirmation challenge"); let en = tool_error_contract_from_failure(&failure, ExecutionLocale::En); assert!( en.message.contains( "Repeat the call with _crank_confirmation_token=\"ct_safe\" within 30 seconds." ), "{:?}", en.message ); assert_eq!( en.suggested_action, Some("Repeat the call with the provided confirmation token.") ); let ru = tool_error_contract_from_failure(&failure, ExecutionLocale::Ru); assert!( ru.message .contains("Повторите вызов с _crank_confirmation_token=\"ct_safe\""), "{:?}", ru.message ); assert_eq!( ru.suggested_action, Some("Повторите вызов с указанным токеном подтверждения.") ); }