use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
const TOOLS: &[&str] = &[
"ct",
"ct-search",
"ct-view",
"ct-tree",
"ct-edit",
"ct-patch",
"ct-test",
"ct-each",
"ct-outline",
"ct-rules",
"ct-check",
"ct-await",
];
const LEAVES: &[&str] = &[
"ct-search",
"ct-view",
"ct-tree",
"ct-edit",
"ct-patch",
"ct-test",
"ct-each",
"ct-outline",
"ct-rules",
"ct-check",
"ct-await",
];
const BUILTIN_CHECKS: &[&str] = &["deps", "mods"];
fn explain_path(tool: &str, ext: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("docs/explain")
.join(format!("{tool}.{ext}"))
}
fn read_json(tool: &str) -> serde_json::Value {
let path = explain_path(tool, "json");
let text =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
serde_json::from_str(&text).unwrap_or_else(|e| panic!("parse {}: {e}", path.display()))
}
#[test]
fn json_payloads_are_well_formed_tool_definitions() {
for tool in TOOLS {
let v = read_json(tool);
assert_eq!(v["name"], *tool, "name must equal the tool name");
assert!(v["description"].is_string(), "description must be a string");
assert_eq!(v["input_schema"]["type"], "object");
assert!(
v["input_schema"]["properties"].is_object(),
"input_schema.properties must be an object"
);
}
}
#[test]
fn md_payloads_lead_with_the_tool_heading() {
for tool in TOOLS {
let path = explain_path(tool, "md");
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
assert!(
text.starts_with(&format!("# {tool}")),
"{} should begin with an H1 naming the tool",
path.display()
);
}
}
#[test]
fn ct_manifest_bundles_each_leaf_definition_verbatim() {
let manifest = read_json("ct");
let tools = manifest["tools"]
.as_array()
.expect("ct.json must carry a `tools` array for agent hoisting");
for leaf in LEAVES {
let bundled = tools
.iter()
.find(|t| t["name"] == *leaf)
.unwrap_or_else(|| panic!("ct manifest is missing the '{leaf}' tool definition"));
let standalone = read_json(leaf);
assert_eq!(
*bundled, standalone,
"ct manifest's '{leaf}' definition has drifted from docs/explain/{leaf}.json"
);
}
}
fn non_flag_props(tool: &str) -> &'static [&'static str] {
match tool {
"ct-view" => &["path"], "ct-test" => &["args"], "ct-each" => &["command"], "ct-rules" => &["probe"], "ct-await" => &["probe"], _ => &[],
}
}
fn normalize_kind(ty: &str) -> String {
if matches!(ty, "number" | "integer") { "string" } else { ty }.to_string()
}
#[test]
fn schema_matches_clap_grammar() {
let mut grammars: Vec<(&str, coding_tools::deps::Grammar)> = coding_tools::cli::grammars();
grammars.push(("deps", coding_tools::deps::check_grammar()));
grammars.push(("mods", coding_tools::modgraph::check_grammar()));
for (tool, grammar) in grammars {
let v = read_json(tool);
let props = v["input_schema"]["properties"]
.as_object()
.unwrap_or_else(|| panic!("{tool}.json input_schema.properties must be an object"));
let skip = non_flag_props(tool);
let cli_kinds: BTreeMap<String, String> =
grammar.flags.iter().map(|f| (f.name.clone(), normalize_kind(f.kind))).collect();
let doc_kinds: BTreeMap<String, String> = props
.iter()
.filter(|(k, _)| !skip.contains(&k.as_str()))
.map(|(k, spec)| {
let ty = spec["type"]
.as_str()
.unwrap_or_else(|| panic!("{tool}.json: property {k} needs a string type"));
(k.clone(), normalize_kind(ty))
})
.collect();
assert_eq!(
doc_kinds, cli_kinds,
"{tool}: docs/explain/{tool}.json (left) disagrees with the clap grammar (right) \
on flag names or kinds"
);
for f in grammar.flags.iter().filter(|f| f.kind != "boolean" && !f.values.is_empty()) {
let spec = &props[&f.name];
let enum_node = if f.kind == "array" { &spec["items"]["enum"] } else { &spec["enum"] };
let doc_vals: BTreeSet<&str> = enum_node
.as_array()
.unwrap_or_else(|| panic!("{tool}.json: --{} should carry an enum array", f.name))
.iter()
.map(|x| x.as_str().expect("enum entries are strings"))
.collect();
let cli_vals: BTreeSet<&str> = f.values.iter().map(String::as_str).collect();
assert_eq!(
doc_vals, cli_vals,
"{tool}: --{} enum disagrees with the clap grammar",
f.name
);
}
let doc_required: BTreeSet<&str> = v["input_schema"]["required"]
.as_array()
.map(|a| a.iter().filter_map(|x| x.as_str()).collect())
.unwrap_or_default();
for name in &grammar.required {
assert!(
doc_required.contains(name.as_str()),
"{tool}: clap requires '{name}' but docs/explain/{tool}.json omits it from `required`"
);
}
}
}
#[test]
fn md_documents_every_flag() {
for (tool, grammar) in coding_tools::cli::grammars() {
let path = explain_path(tool, "md");
let md = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let mentioned: BTreeSet<&str> = md
.match_indices("--")
.map(|(i, _)| {
md[i + 2..]
.split(|c: char| !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'))
.next()
.unwrap_or("")
})
.collect();
for f in &grammar.flags {
assert!(
mentioned.contains(f.name.as_str()),
"{tool}: --{} is accepted by the CLI but never mentioned in docs/explain/{tool}.md",
f.name
);
}
}
}
#[test]
fn builtin_check_defs_are_well_formed_and_bundled() {
let manifest = read_json("ct");
let bundled = manifest["builtin_checks"]
.as_array()
.expect("ct.json must carry a `builtin_checks` array for agent hoisting");
for check in BUILTIN_CHECKS {
let standalone = read_json(check);
assert_eq!(standalone["name"], *check, "name must equal the check name");
assert!(standalone["description"].is_string(), "description must be a string");
assert_eq!(standalone["input_schema"]["type"], "object");
assert!(
standalone["input_schema"]["properties"].is_object(),
"input_schema.properties must be an object"
);
let entry = bundled
.iter()
.find(|c| c["name"] == *check)
.unwrap_or_else(|| panic!("ct manifest is missing the '{check}' built-in check"));
assert_eq!(
*entry, standalone,
"ct manifest's '{check}' check has drifted from docs/explain/{check}.json"
);
}
}