use std::fs;
use std::path::Path;
fn find_workspace_root() -> Option<std::path::PathBuf> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
Path::new(manifest_dir)
.ancestors()
.find(|path| {
path.join("Cargo.toml")
.exists()
.then(|| fs::read_to_string(path.join("Cargo.toml")).ok())
.flatten()
.is_some_and(|content| {
content.contains("[workspace]") || content.contains("[workspace.")
})
})
.map(|p| p.to_path_buf())
}
#[test]
fn test_rules_json_parity() {
let workspace_root = find_workspace_root();
let Some(root) = workspace_root else {
eprintln!("Skipping parity test: workspace root not found");
return;
};
let crate_rules_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("rules.json");
let kb_rules_path = root.join("knowledge-base/rules.json");
if !crate_rules_path.exists() || !kb_rules_path.exists() {
eprintln!("Skipping parity test: one or both rules.json files not found");
return;
}
let crate_rules = fs::read_to_string(&crate_rules_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", crate_rules_path.display(), e));
let kb_rules = fs::read_to_string(&kb_rules_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", kb_rules_path.display(), e));
let crate_json: serde_json::Value = serde_json::from_str(&crate_rules)
.unwrap_or_else(|e| panic!("Failed to parse {}: {}", crate_rules_path.display(), e));
let kb_json: serde_json::Value = serde_json::from_str(&kb_rules)
.unwrap_or_else(|e| panic!("Failed to parse {}: {}", kb_rules_path.display(), e));
assert_eq!(
crate_json, kb_json,
"rules.json files are out of sync!\n\
crates/agnix-rules/rules.json and knowledge-base/rules.json must be identical.\n\
Copy the updated file: cp knowledge-base/rules.json crates/agnix-rules/rules.json"
);
}
#[test]
fn test_rules_count_matches_source() {
let rules_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("rules.json");
if !rules_path.exists() {
eprintln!("Skipping rule count test: rules.json not found");
return;
}
let rules_json = fs::read_to_string(&rules_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", rules_path.display(), e));
let rules: serde_json::Value = serde_json::from_str(&rules_json)
.unwrap_or_else(|e| panic!("Failed to parse {}: {}", rules_path.display(), e));
let expected_count = rules["rules"]
.as_array()
.expect("rules.json must have 'rules' array")
.len();
assert_eq!(
agnix_rules::rule_count(),
expected_count,
"RULES_DATA count ({}) doesn't match rules.json count ({})",
agnix_rules::rule_count(),
expected_count
);
}
#[test]
fn test_rules_data_accessible_and_valid() {
for (id, name) in agnix_rules::RULES_DATA {
assert!(!id.is_empty(), "Rule ID should not be empty");
assert!(
id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'),
"Rule ID '{}' contains invalid characters",
id
);
assert!(!name.is_empty(), "Rule '{}' name should not be empty", id);
assert!(
!name.chars().any(|c| c.is_control() && c != ' '),
"Rule '{}' name contains control characters",
id
);
}
}
#[test]
fn test_empty_tool_string_treated_as_generic() {
let valid_tools = agnix_rules::valid_tools();
assert!(
!valid_tools.contains(&""),
"VALID_TOOLS should not contain empty string. \
Empty tool values in rules.json should be treated as generic, not as a valid tool."
);
for (prefix, tool) in agnix_rules::TOOL_RULE_PREFIXES {
assert!(
!tool.is_empty(),
"TOOL_RULE_PREFIXES contains empty tool for prefix '{}'. \
This indicates a bug in build.rs.",
prefix
);
}
}