244 lines
7.1 KiB
Rust
244 lines
7.1 KiB
Rust
use crank_core::{
|
|
ToolDescription, ToolQualityFinding, ToolQualityMappingRule, ToolQualityMappingSet,
|
|
ToolQualityReport, ToolQualitySchemaNode, ToolQualitySeverity, 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
|
|
));
|
|
}
|
|
|
|
fn has_finding(report: &ToolQualityReport, code: &str, severity: ToolQualitySeverity) -> bool {
|
|
report
|
|
.findings
|
|
.iter()
|
|
.any(|finding| finding.code == code && finding.severity == severity)
|
|
}
|