use crank_core::{ ToolDescription, ToolQualityCatalogTool, ToolQualityFinding, ToolQualityMappingRule, ToolQualityMappingSet, ToolQualityReport, ToolQualitySchemaNode, ToolQualitySeverity, analyze_agent_tool_catalog_quality, analyze_tool_identity_quality, analyze_tool_response_projection_quality, analyze_tool_schema_quality, }; #[test] fn serializes_tool_quality_finding_contract() { let finding = ToolQualityFinding { severity: ToolQualitySeverity::Warning, code: "tool_description_too_short".to_owned(), message: "Описание инструмента слишком короткое.".to_owned(), suggested_action: Some("Добавьте назначение и условия применения инструмента.".to_owned()), field_path: Some("tool_description.description".to_owned()), }; let value = serde_json::to_value(finding).unwrap(); assert_eq!(value["severity"], "warning"); assert_eq!(value["code"], "tool_description_too_short"); assert_eq!(value["field_path"], "tool_description.description"); } #[test] fn report_is_blocking_when_error_finding_exists() { let report = ToolQualityReport::new(vec![ToolQualityFinding { severity: ToolQualitySeverity::Error, code: "tool_name_missing".to_owned(), message: "Имя инструмента обязательно.".to_owned(), suggested_action: None, field_path: Some("name".to_owned()), }]); assert!(report.blocking); assert_eq!(report.findings.len(), 1); } #[test] fn reports_missing_tool_name_as_blocking_error() { let report = analyze_tool_identity_quality( " ", &ToolDescription { title: "Create lead".to_owned(), description: "Создает новый лид в CRM, когда пользователь просит добавить потенциального клиента.".to_owned(), tags: Vec::new(), examples: Vec::new(), }, ); assert!(report.blocking); assert!(has_finding( &report, "tool_name_missing", ToolQualitySeverity::Error )); } #[test] fn warns_about_generic_tool_name() { let report = analyze_tool_identity_quality( "call_api", &ToolDescription { title: "Call API".to_owned(), description: "Получает данные клиента по идентификатору, когда пользователю нужна карточка клиента.".to_owned(), tags: Vec::new(), examples: Vec::new(), }, ); assert!(!report.blocking); assert!(has_finding( &report, "tool_name_too_generic", ToolQualitySeverity::Warning )); } #[test] fn reports_unsafe_tool_name_format_as_blocking_error() { let report = analyze_tool_identity_quality( "Create Lead!", &ToolDescription { title: "Create lead".to_owned(), description: "Создает новый лид в CRM, когда пользователь просит добавить потенциального клиента.".to_owned(), tags: Vec::new(), examples: Vec::new(), }, ); assert!(report.blocking); assert!(has_finding( &report, "tool_name_invalid_format", ToolQualitySeverity::Error )); } #[test] fn warns_about_short_description() { let report = analyze_tool_identity_quality( "create_lead", &ToolDescription { title: "Create lead".to_owned(), description: "Создает лид.".to_owned(), tags: Vec::new(), examples: Vec::new(), }, ); assert!(!report.blocking); assert!(has_finding( &report, "tool_description_too_short", ToolQualitySeverity::Warning )); } #[test] fn warns_about_object_schema_without_fields() { let schema = ToolQualitySchemaNode::object(None, vec![]); let report = analyze_tool_schema_quality("input_schema", &schema); assert!(has_finding( &report, "schema_object_without_fields", ToolQualitySeverity::Warning )); } #[test] fn warns_about_parameter_without_description() { let schema = ToolQualitySchemaNode::object( Some("Входные параметры"), vec![("customer_id", ToolQualitySchemaNode::string(None))], ); let report = analyze_tool_schema_quality("input_schema", &schema); assert!(has_finding( &report, "schema_field_missing_description", ToolQualitySeverity::Warning )); } #[test] fn warns_about_multi_action_parameter_names() { let schema = ToolQualitySchemaNode::object( Some("Входные параметры"), vec![( "action", ToolQualitySchemaNode::string(Some("Что нужно сделать")), )], ); let report = analyze_tool_schema_quality("input_schema", &schema); assert!(has_finding( &report, "schema_multi_action_parameter", ToolQualitySeverity::Warning )); } #[test] fn recommends_enum_for_mode_like_string_parameter() { let schema = ToolQualitySchemaNode::object( Some("Входные параметры"), vec![( "mode", ToolQualitySchemaNode::string(Some("Режим поиска клиента")), )], ); let report = analyze_tool_schema_quality("input_schema", &schema); assert!(has_finding( &report, "schema_enum_recommended", ToolQualitySeverity::Info )); } #[test] fn warns_when_output_mapping_returns_full_response_body() { let mapping = ToolQualityMappingSet { rules: vec![ToolQualityMappingRule { source: "$.response.body".to_owned(), target: "$.output".to_owned(), }], }; let report = analyze_tool_response_projection_quality(&mapping); assert!(has_finding( &report, "response_projection_full_body", ToolQualitySeverity::Warning )); } #[test] fn accepts_output_mapping_with_selected_fields() { let mapping = ToolQualityMappingSet { rules: vec![ToolQualityMappingRule { source: "$.response.body.rates.EUR".to_owned(), target: "$.output.rate".to_owned(), }], }; let report = analyze_tool_response_projection_quality(&mapping); assert!(!report.blocking); assert!(report.findings.is_empty()); } #[test] fn marks_large_output_projection_as_info() { let mapping = ToolQualityMappingSet { rules: (0..12) .map(|index| ToolQualityMappingRule { source: format!("$.response.body.field_{index}"), target: format!("$.output.field_{index}"), }) .collect(), }; let report = analyze_tool_response_projection_quality(&mapping); assert!(has_finding( &report, "response_projection_many_fields", ToolQualitySeverity::Info )); } #[test] fn warns_about_large_agent_tool_catalog() { let tools: Vec = (0..9) .map(|index| { catalog_tool( &format!("tool_{index}"), &format!("Tool {index}"), "Does one task", ) }) .collect(); let report = analyze_agent_tool_catalog_quality(&tools); assert!(has_finding( &report, "agent_catalog_too_large", ToolQualitySeverity::Warning )); } #[test] fn warns_about_similar_agent_tools() { let tools = vec![ catalog_tool( "get_exchange_rate", "Получить курс валюты", "Получает последний курс валюты", ), catalog_tool( "fetch_exchange_rate", "Запросить курс валюты", "Получает текущий курс валюты", ), ]; let report = analyze_agent_tool_catalog_quality(&tools); assert!(has_finding( &report, "agent_catalog_similar_tools", ToolQualitySeverity::Warning )); } #[test] fn accepts_small_catalog_with_distinct_tools() { let tools = vec![ catalog_tool( "get_exchange_rate", "Получить курс валюты", "Получает курс валюты", ), catalog_tool( "get_weather_forecast", "Получить прогноз погоды", "Получает прогноз погоды", ), ]; let report = analyze_agent_tool_catalog_quality(&tools); assert!(report.findings.is_empty()); } fn catalog_tool(name: &str, display_name: &str, description: &str) -> ToolQualityCatalogTool { ToolQualityCatalogTool { name: name.to_owned(), display_name: display_name.to_owned(), description: description.to_owned(), } } fn has_finding(report: &ToolQualityReport, code: &str, severity: ToolQualitySeverity) -> bool { report .findings .iter() .any(|finding| finding.code == code && finding.severity == severity) }