Files
crank/crates/crank-core/tests/unit/tool_quality.rs
T
github-ops ce773e6196
Deploy / deploy (push) Successful in 1m40s
CI / Rust Checks (push) Failing after 5m3s
CI / UI Checks (push) Has been skipped
CI / Frontend E2E (push) Has been skipped
CI / Deployment Manifests (push) Has been skipped
Warn about broad tool response projections
2026-06-20 21:05:30 +00:00

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)
}