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 tool_name_in_line_extracts_literal_names() {
assert_eq!(
tool_name_in_line(" #[astrid::tool(\"reverse_text\")]"),
Some("reverse_text".into())
);
assert_eq!(
tool_name_in_line("#[astrid::tool(\"upcase\", mutable)]"),
Some("upcase".into())
);
assert_eq!(
tool_name_in_line("#[astrid::tool (\"spaced\")]"),
Some("spaced".into())
);
}
#[test]
fn tool_name_in_line_ignores_non_tool_and_commented() {
assert_eq!(tool_name_in_line("// #[astrid::tool(\"nope\")]"), None);
assert_eq!(
tool_name_in_line("let x = astrid::tool_helper(\"nope\");"),
None
);
assert_eq!(tool_name_in_line("astrid::tool_describe()"), None);
assert_eq!(tool_name_in_line("#[astrid::tool()]"), None);
assert_eq!(tool_name_in_line("fn reverse(s: &str) -> String {"), None);
}
#[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",
]
);
}