use super::*;
fn names(items: &[&str]) -> Vec<String> {
items.iter().map(|s| (*s).to_string()).collect()
}
fn intercept(event: &str, action: &str) -> InterceptorDef {
InterceptorDef {
event: event.into(),
action: action.into(),
priority: 100,
}
}
fn mandatory_publishes() -> Vec<String> {
MANDATORY_PUBLISH
.iter()
.map(|(pattern, _, _)| (*pattern).to_string())
.collect()
}
fn rules(findings: &[Finding]) -> Vec<&'static str> {
findings.iter().map(|f| f.rule).collect()
}
#[test]
fn source_scan_extracts_literal_names() {
assert_eq!(
tool_names_in_source(
r#"
#[astrid::tool("reverse_text")]
fn reverse_text() {}
#[astrid::tool("upcase", mutable)]
fn upcase() {}
#[astrid::tool]
fn inferred_name() {}
impl Tools {
#[astrid::tool]
fn inferred_method(&self) {}
}
"#,
)
.unwrap(),
names(&["reverse_text", "upcase", "inferred_name", "inferred_method",]),
);
}
#[test]
fn source_scan_ignores_comments_calls_and_embedded_templates() {
let source = r##"
// #[astrid::tool("line_comment")]
/* #[astrid::tool("block_comment")] */
const ORDINARY: &str = "#[astrid::tool(\"ordinary_string\")]";
const TEMPLATE: &str = r#"
#[astrid::tool("raw_string")]
fn example() {}
"#;
fn unrelated() {
astrid::tool_helper("helper_call");
astrid::tool_describe();
}
#[astrid::tool("real_tool")]
fn real_tool() {}
"##;
assert_eq!(tool_names_in_source(source).unwrap(), names(&["real_tool"]));
}
#[test]
fn source_scan_fails_closed_on_unparseable_rust() {
let error = tool_names_in_source("fn incomplete(").unwrap_err();
assert!(!error.to_string().is_empty());
}
#[test]
fn clean_capsule_has_no_findings() {
let findings = check_capsule(
&names(&["reverse_text"]),
&[intercept(
"tool.v1.execute.reverse_text",
"tool_execute_reverse_text",
)],
&mandatory_publishes(),
);
assert!(findings.is_empty(), "expected clean, got {findings:?}");
}
#[test]
fn flags_advertised_but_unrouted_tool() {
let findings = check_capsule(&names(&["reverse_text"]), &[], &mandatory_publishes());
assert_eq!(rules(&findings), vec!["unrouted-tool"]);
assert!(findings[0].message.contains("tool.v1.execute.reverse_text"));
assert!(
findings[0]
.message
.contains("wit = \"@unicity-astrid/wit/types/tool-call\"")
);
}
#[test]
fn flags_missing_mandatory_publish_boilerplate() {
let findings = check_capsule(
&names(&["reverse_text"]),
&[intercept(
"tool.v1.execute.reverse_text",
"tool_execute_reverse_text",
)],
&[], );
assert_eq!(rules(&findings), vec!["missing-publish", "missing-publish"]);
assert!(
findings
.iter()
.any(|f| f.message.contains("tool.v1.execute.*.result"))
);
assert!(
findings
.iter()
.any(|f| f.message.contains("tool.v1.response.describe.*"))
);
}
#[test]
fn missing_publish_not_flagged_for_toolless_capsule() {
let findings = check_capsule(&[], &[], &[]);
assert!(findings.is_empty(), "got {findings:?}");
}
#[test]
fn flags_dangling_tool_subscription() {
let findings = check_capsule(
&names(&["reverse_text"]),
&[
intercept("tool.v1.execute.reverse_text", "tool_execute_reverse_text"),
intercept("tool.v1.execute.reverze_text", "tool_execute_reverze_text"),
],
&mandatory_publishes(),
);
assert_eq!(rules(&findings), vec!["dangling-subscription"]);
assert!(findings[0].message.contains("reverze_text"));
}
#[test]
fn flags_handler_name_mismatch() {
let findings = check_capsule(
&names(&["reverse_text"]),
&[intercept("tool.v1.execute.reverse_text", "do_reverse")],
&mandatory_publishes(),
);
assert_eq!(rules(&findings), vec!["handler-mismatch"]);
assert!(findings[0].message.contains("tool_execute_reverse_text"));
assert!(findings[0].message.contains("do_reverse"));
}
#[test]
fn wildcard_subscription_routes_all_tools_no_false_positives() {
let findings = check_capsule(
&names(&["alpha", "beta"]),
&[intercept("tool.v1.execute.*", "tool_execute_dispatch")],
&mandatory_publishes(),
);
assert!(
findings.is_empty(),
"wildcard should route all, got {findings:?}"
);
}
#[test]
fn reserved_result_topic_is_not_flagged_dangling() {
let findings = check_capsule(
&names(&["reverse_text"]),
&[
intercept("tool.v1.execute.reverse_text", "tool_execute_reverse_text"),
intercept("tool.v1.execute.result", "handle_result"),
],
&mandatory_publishes(),
);
assert!(findings.is_empty(), "got {findings:?}");
}
#[test]
fn per_tool_result_topic_is_not_treated_as_a_tool() {
let findings = check_capsule(
&names(&["reverse_text"]),
&[
intercept("tool.v1.execute.reverse_text", "tool_execute_reverse_text"),
intercept("tool.v1.execute.reverse_text.result", "handle_result"),
],
&mandatory_publishes(),
);
assert!(findings.is_empty(), "got {findings:?}");
}
#[test]
fn findings_are_sorted_deterministically_by_rule() {
let findings = check_capsule(
&names(&["alpha"]),
&[intercept("tool.v1.execute.ghost", "tool_execute_ghost")],
&[],
);
assert_eq!(
rules(&findings),
vec![
"dangling-subscription",
"missing-publish",
"missing-publish",
"unrouted-tool",
]
);
}