feat: complete Epic 1 production foundation
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::Instant;
|
||||
|
||||
use crank_community_mcp::manifest::{analyze_published_tool_catalog, tool_definitions};
|
||||
use crank_community_mcp::manifest::{
|
||||
analyze_published_tool_catalog, catalog_tool_definitions, tool_definitions,
|
||||
};
|
||||
use crank_core::{
|
||||
ExecutionConfig, HttpMethod, Operation, OperationId, OperationSecurityLevel, OperationStatus,
|
||||
Protocol, RestTarget, Target, ToolDescription, WorkspaceId,
|
||||
Protocol, RestTarget, Target, ToolAccessMode, ToolDescription, ToolSelectionPolicy,
|
||||
WorkspaceId,
|
||||
};
|
||||
use crank_mapping::MappingSet;
|
||||
use crank_registry::{PublishedAgentTool, RegistryOperation};
|
||||
use crank_registry::{PublishedAgentCatalog, PublishedAgentTool, RegistryOperation};
|
||||
use crank_schema::{Schema, SchemaKind};
|
||||
use serde_json::json;
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
@@ -71,6 +75,57 @@ fn catalog_budget_uses_the_same_definitions_as_tools_list() {
|
||||
assert!(!analysis.budget.exceeds_recommended_budget);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_catalog_discovery_profile_handles_200_agents_2000_operations_under_budget() {
|
||||
const AGENT_COUNT: usize = 200;
|
||||
const TOOLS_PER_AGENT: usize = 10;
|
||||
const P95_BUDGET_MS: u128 = 150;
|
||||
|
||||
let mut durations = Vec::with_capacity(AGENT_COUNT);
|
||||
let mut total_definitions = 0usize;
|
||||
let started = Instant::now();
|
||||
|
||||
for agent_index in 0..AGENT_COUNT {
|
||||
let tools = (0..TOOLS_PER_AGENT)
|
||||
.map(|tool_index| {
|
||||
let mut tool = published_tool();
|
||||
tool.agent_id = format!("agent_{agent_index:03}").into();
|
||||
tool.agent_slug = format!("agent-{agent_index:03}");
|
||||
tool.operation.id =
|
||||
OperationId::new(format!("op_{agent_index:03}_{tool_index:03}"));
|
||||
tool.tool_name = format!("tool_{agent_index:03}_{tool_index:03}");
|
||||
tool.tool_title = format!("Tool {agent_index:03}-{tool_index:03}");
|
||||
tool
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let catalog = PublishedAgentCatalog {
|
||||
agent_version: 1,
|
||||
catalog_revision: format!("agent-{agent_index:03}-revision-1"),
|
||||
tool_selection_policy: ToolSelectionPolicy {
|
||||
mode: ToolAccessMode::Direct,
|
||||
..Default::default()
|
||||
},
|
||||
tools,
|
||||
};
|
||||
|
||||
let catalog_started = Instant::now();
|
||||
let definitions = catalog_tool_definitions(&catalog);
|
||||
durations.push(catalog_started.elapsed());
|
||||
total_definitions += definitions.len();
|
||||
}
|
||||
|
||||
durations.sort_unstable();
|
||||
let p95 = durations[((durations.len() * 95).div_ceil(100)).saturating_sub(1)];
|
||||
|
||||
assert_eq!(total_definitions, AGENT_COUNT * TOOLS_PER_AGENT);
|
||||
assert!(
|
||||
p95.as_millis() <= P95_BUDGET_MS,
|
||||
"agent catalog discovery p95={}ms total={}ms",
|
||||
p95.as_millis(),
|
||||
started.elapsed().as_millis()
|
||||
);
|
||||
}
|
||||
|
||||
fn published_tool() -> PublishedAgentTool {
|
||||
PublishedAgentTool {
|
||||
workspace_id: WorkspaceId::new("ws_01"),
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
use crank_adapter_rest::RestAdapterError;
|
||||
use crank_community_mcp::tool_error::tool_error_contract_from_runtime;
|
||||
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;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn maps_upstream_429_to_recoverable_structured_tool_error() {
|
||||
let contract = tool_error_contract_from_runtime(
|
||||
&RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus {
|
||||
&RuntimeError::ProtocolAdapter(ProtocolAdapterError::UnexpectedStatus {
|
||||
status: 429,
|
||||
body: json!({
|
||||
"error": "rate limit exceeded",
|
||||
"internal_trace": "do not leak"
|
||||
}),
|
||||
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");
|
||||
@@ -26,6 +31,22 @@ fn maps_upstream_429_to_recoverable_structured_tool_error() {
|
||||
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(
|
||||
@@ -35,15 +56,91 @@ fn maps_mapping_error_to_non_recoverable_structured_tool_error() {
|
||||
},
|
||||
"req-map",
|
||||
"0af7651916cd43dd8448eb211c80319c",
|
||||
ExecutionLocale::Ru,
|
||||
);
|
||||
|
||||
assert_eq!(contract.error_code, "runtime_error");
|
||||
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!(
|
||||
contract.suggested_action,
|
||||
Some("Проверьте параметры вызова инструмента.")
|
||||
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("Повторите вызов с указанным токеном подтверждения.")
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user