use serde_json::Value;
use std::collections::HashSet;
fn load_alignment_fixture() -> Value {
let fixture_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/cli_mcp_alignment.json"
);
let data = std::fs::read_to_string(fixture_path)
.unwrap_or_else(|e| panic!("Failed to read cli_mcp_alignment.json: {}", e));
serde_json::from_str(&data).expect("cli_mcp_alignment.json should be valid JSON")
}
fn load_cli_commands_fixture() -> Value {
let fixture_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../jacs-cli/contract/cli_commands.json"
);
let data = std::fs::read_to_string(fixture_path)
.unwrap_or_else(|e| panic!("Failed to read cli_commands.json: {}", e));
serde_json::from_str(&data).expect("cli_commands.json should be valid JSON")
}
fn load_mcp_contract() -> Value {
let fixture_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../jacs-mcp/contract/jacs-mcp-contract.json"
);
let data = std::fs::read_to_string(fixture_path)
.unwrap_or_else(|e| panic!("Failed to read jacs-mcp-contract.json: {}", e));
serde_json::from_str(&data).expect("jacs-mcp-contract.json should be valid JSON")
}
#[test]
fn test_all_cli_commands_are_in_alignment_fixture() {
let alignment = load_alignment_fixture();
let cli = load_cli_commands_fixture();
let mut cli_commands: HashSet<String> = HashSet::new();
for cmd in cli["commands"].as_array().expect("commands array") {
cli_commands.insert(cmd["path"].as_str().unwrap().to_string());
}
let mut alignment_cli: HashSet<String> = HashSet::new();
for entry in alignment["alignments"]
.as_array()
.expect("alignments array")
{
alignment_cli.insert(entry["cli_command"].as_str().unwrap().to_string());
}
for entry in alignment["cli_only"].as_array().expect("cli_only array") {
alignment_cli.insert(entry["cli_command"].as_str().unwrap().to_string());
}
let missing: Vec<&String> = cli_commands.difference(&alignment_cli).collect();
let extra: Vec<&String> = alignment_cli.difference(&cli_commands).collect();
assert!(
missing.is_empty(),
"CLI commands in cli_commands.json but NOT in cli_mcp_alignment.json: {:?}\n\
Add them to either 'alignments' or 'cli_only' in the alignment fixture.",
missing
);
assert!(
extra.is_empty(),
"CLI commands in cli_mcp_alignment.json but NOT in cli_commands.json: {:?}\n\
Remove stale entries from the alignment fixture.",
extra
);
}
#[test]
fn test_all_feature_gated_cli_commands_are_in_alignment_fixture() {
let alignment = load_alignment_fixture();
let cli = load_cli_commands_fixture();
let mut cli_gated: HashSet<String> = HashSet::new();
for cmd in cli["feature_gated_commands"]
.as_array()
.expect("feature_gated_commands array")
{
cli_gated.insert(cmd["path"].as_str().unwrap().to_string());
}
let mut alignment_gated: HashSet<String> = HashSet::new();
for entry in alignment["cli_only_feature_gated"]
.as_array()
.expect("cli_only_feature_gated array")
{
alignment_gated.insert(entry["cli_command"].as_str().unwrap().to_string());
}
let missing: Vec<&String> = cli_gated.difference(&alignment_gated).collect();
let extra: Vec<&String> = alignment_gated.difference(&cli_gated).collect();
assert!(
missing.is_empty(),
"Feature-gated CLI commands not in alignment fixture: {:?}",
missing
);
assert!(
extra.is_empty(),
"Stale feature-gated entries in alignment fixture: {:?}",
extra
);
}
#[test]
fn test_all_mcp_tools_are_in_alignment_fixture() {
let alignment = load_alignment_fixture();
let mcp = load_mcp_contract();
let mut mcp_tools: HashSet<String> = HashSet::new();
for tool in mcp["tools"].as_array().expect("tools array") {
mcp_tools.insert(tool["name"].as_str().unwrap().to_string());
}
let mut alignment_mcp: HashSet<String> = HashSet::new();
for entry in alignment["alignments"]
.as_array()
.expect("alignments array")
{
alignment_mcp.insert(entry["mcp_tool"].as_str().unwrap().to_string());
}
for entry in alignment["mcp_only"].as_array().expect("mcp_only array") {
alignment_mcp.insert(entry["mcp_tool"].as_str().unwrap().to_string());
}
let missing: Vec<&String> = mcp_tools.difference(&alignment_mcp).collect();
let extra: Vec<&String> = alignment_mcp.difference(&mcp_tools).collect();
assert!(
missing.is_empty(),
"MCP tools in jacs-mcp-contract.json but NOT in cli_mcp_alignment.json: {:?}\n\
Add them to either 'alignments' or 'mcp_only' in the alignment fixture.",
missing
);
assert!(
extra.is_empty(),
"MCP tools in cli_mcp_alignment.json but NOT in jacs-mcp-contract.json: {:?}\n\
Remove stale entries from the alignment fixture.",
extra
);
}
#[test]
fn test_alignment_summary_counts_are_accurate() {
let alignment = load_alignment_fixture();
let summary = &alignment["summary"];
let alignments_count = alignment["alignments"]
.as_array()
.expect("alignments array")
.len();
let cli_only_count = alignment["cli_only"]
.as_array()
.expect("cli_only array")
.len();
let cli_only_gated_count = alignment["cli_only_feature_gated"]
.as_array()
.expect("cli_only_feature_gated array")
.len();
let mcp_only_arr = alignment["mcp_only"].as_array().expect("mcp_only array");
let mcp_only_count = mcp_only_arr.len();
let mcp_only_intentional = mcp_only_arr
.iter()
.filter(|e| e["classification"].as_str() == Some("intentional"))
.count();
let mcp_only_gap = mcp_only_arr
.iter()
.filter(|e| e["classification"].as_str() == Some("gap"))
.count();
assert_eq!(
summary["aligned_pairs"].as_u64().unwrap() as usize,
alignments_count,
"summary.aligned_pairs does not match alignments array length"
);
assert_eq!(
summary["cli_only_count"].as_u64().unwrap() as usize,
cli_only_count,
"summary.cli_only_count does not match cli_only array length"
);
assert_eq!(
summary["cli_only_feature_gated_count"].as_u64().unwrap() as usize,
cli_only_gated_count,
"summary.cli_only_feature_gated_count does not match"
);
assert_eq!(
summary["mcp_only_count"].as_u64().unwrap() as usize,
mcp_only_count,
"summary.mcp_only_count does not match mcp_only array length"
);
assert_eq!(
summary["mcp_only_intentional"].as_u64().unwrap() as usize,
mcp_only_intentional,
"summary.mcp_only_intentional does not match actual count"
);
assert_eq!(
summary["mcp_only_gap"].as_u64().unwrap() as usize,
mcp_only_gap,
"summary.mcp_only_gap does not match actual count"
);
}
#[test]
fn test_mcp_only_entries_have_valid_classification() {
let alignment = load_alignment_fixture();
let valid_classifications = ["intentional", "gap"];
for entry in alignment["mcp_only"].as_array().expect("mcp_only array") {
let tool = entry["mcp_tool"].as_str().unwrap();
let classification = entry["classification"]
.as_str()
.unwrap_or_else(|| panic!("MCP-only tool {} missing 'classification' field", tool));
assert!(
valid_classifications.contains(&classification),
"MCP-only tool {} has invalid classification '{}'. Must be one of: {:?}",
tool,
classification,
valid_classifications
);
assert!(
entry["reason"].as_str().is_some() && !entry["reason"].as_str().unwrap().is_empty(),
"MCP-only tool {} missing 'reason' field",
tool
);
}
}