147 lines
5.0 KiB
Rust
147 lines
5.0 KiB
Rust
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("Повторите вызов с указанным токеном подтверждения.")
|
||
);
|
||
}
|