use std::collections::HashSet;
use serde_json::{Value, json};
use super::ctx_callgraph::CtxCallgraphTool;
use super::ctx_execute::CtxExecuteTool;
use super::ctx_expand::CtxExpandTool;
use super::ctx_graph::CtxGraphTool;
use super::ctx_knowledge::CtxKnowledgeTool;
use super::ctx_patch::CtxPatchTool;
use super::ctx_search::CtxSearchTool;
use crate::server::tool_trait::McpTool;
fn validator(tool: &dyn McpTool) -> jsonschema::Validator {
let schema = Value::Object((*tool.tool_def().input_schema).clone());
jsonschema::validator_for(&schema).expect("published tool schema must compile")
}
#[test]
fn callgraph_expand_and_graph_require_action_inputs() {
let callgraph = validator(&CtxCallgraphTool);
assert!(callgraph.is_valid(&json!({"action":"callers","symbol":"f"})));
assert!(callgraph.is_valid(&json!({"action":"trace","from":"a","to":"b"})));
assert!(!callgraph.is_valid(&json!({})));
assert!(!callgraph.is_valid(&json!({"action":"trace","from":"a"})));
let expand = validator(&CtxExpandTool);
assert!(expand.is_valid(&json!({"id":"F1"})));
assert!(expand.is_valid(&json!({"action":"list"})));
assert!(!expand.is_valid(&json!({})));
assert!(!expand.is_valid(&json!({"action":"search_all"})));
let graph = validator(&CtxGraphTool);
assert!(graph.is_valid(&json!({"action":"status"})));
assert!(graph.is_valid(&json!({"action":"path","path":"a","to":"b"})));
assert!(!graph.is_valid(&json!({"action":"symbol"})));
assert!(!graph.is_valid(&json!({"action":"path","path":"a"})));
}
#[test]
fn knowledge_search_and_execute_require_mode_specific_inputs() {
let knowledge = validator(&CtxKnowledgeTool);
assert!(knowledge.is_valid(&json!({"action":"remember","category":"decision","value":"v"})));
assert!(knowledge.is_valid(&json!({"action":"recall"})));
assert!(!knowledge.is_valid(&json!({"action":"remember","value":"v"})));
assert!(!knowledge.is_valid(&json!({"action":"gotcha","trigger":"t"})));
let search = validator(&CtxSearchTool);
assert!(search.is_valid(&json!({"pattern":"needle"})));
assert!(search.is_valid(&json!({"action":"symbol","handle":"f.rs#f@L1"})));
assert!(!search.is_valid(&json!({})));
assert!(!search.is_valid(&json!({"action":"semantic"})));
let execute = validator(&CtxExecuteTool);
assert!(execute.is_valid(&json!({"language":"python","code":"print(1)"})));
assert!(execute.is_valid(&json!({"action":"file","path":"a.py"})));
assert!(!execute.is_valid(&json!({})));
assert!(!execute.is_valid(&json!({"action":"batch"})));
}
fn collect_property_names(node: &Value, out: &mut HashSet<String>) {
match node {
Value::Object(map) => {
if let Some(Value::Object(props)) = map.get("properties") {
out.extend(props.keys().cloned());
}
for v in map.values() {
collect_property_names(v, out);
}
}
Value::Array(arr) => arr.iter().for_each(|v| collect_property_names(v, out)),
_ => {}
}
}
fn collect_required(node: &Value, out: &mut Vec<String>) {
match node {
Value::Object(map) => {
if let Some(Value::Array(req)) = map.get("required") {
out.extend(req.iter().filter_map(|r| r.as_str().map(str::to_string)));
}
for v in map.values() {
collect_required(v, out);
}
}
Value::Array(arr) => arr.iter().for_each(|v| collect_required(v, out)),
_ => {}
}
}
#[test]
fn every_required_param_is_a_declared_property() {
let registry = crate::server::registry::build_registry();
let mut violations = Vec::new();
for def in registry.tool_defs() {
let schema = Value::Object((*def.input_schema).clone());
let mut props = HashSet::new();
collect_property_names(&schema, &mut props);
let mut required = Vec::new();
collect_required(&schema, &mut required);
for r in required {
if !props.contains(&r) {
violations.push(format!(
"{}: schema requires `{r}` but declares no such property",
def.name
));
}
}
}
assert!(
violations.is_empty(),
"tool schemas require undeclared params (retired/typo'd field?):\n {}",
violations.join("\n ")
);
}
#[test]
fn patch_schema_encodes_per_op_required_params() {
let patch = validator(&CtxPatchTool);
assert!(
patch.is_valid(&json!({"op":"set_line","path":"a","line":1,"hash":"aa","new_text":"x"}))
);
assert!(
!patch.is_valid(&json!({"op":"set_line","path":"a","line":1,"hash":"aa","new_body":"x"}))
);
assert!(patch.is_valid(&json!({
"op":"replace_lines","path":"a",
"start_line":1,"start_hash":"aa","end_line":2,"end_hash":"bb","new_text":"y"
})));
assert!(!patch.is_valid(&json!({
"op":"replace_lines","path":"a",
"start_line":1,"start_hash":"aa","end_line":2,"end_hash":"bb"
})));
assert!(!patch.is_valid(&json!({"op":"create","path":"a"})));
assert!(patch.is_valid(&json!({"op":"create","path":"a","new_text":""})));
assert!(!patch.is_valid(&json!({"op":"replace_all","path":"a","find":"x"})));
assert!(patch.is_valid(&json!({"op":"replace_all","path":"a","find":"x","replace":"y"})));
assert!(patch.is_valid(&json!({
"ops":[{"op":"set_line","path":"a","line":1,"hash":"aa","new_text":"x"}]
})));
}