use std::fs;
use std::path::Path;
use assert_cmd::cargo::cargo_bin_cmd;
use assert_cmd::Command;
fn aigent() -> Command {
cargo_bin_cmd!("aigent")
}
#[test]
fn validate_builder_skill() {
aigent()
.args(["validate", "skills/aigent-builder/"])
.assert()
.success();
}
#[test]
fn validate_validator_skill() {
aigent()
.args(["validate", "skills/aigent-validator/"])
.assert()
.success();
}
fn read_plugin_json() -> serde_json::Value {
let content =
fs::read_to_string(".claude-plugin/plugin.json").expect("plugin.json should exist");
serde_json::from_str(&content).expect("plugin.json should be valid JSON")
}
#[test]
fn plugin_json_is_valid() {
read_plugin_json();
}
#[test]
fn plugin_json_has_name() {
let json = read_plugin_json();
assert_eq!(json["name"], "aigent");
}
#[test]
fn plugin_json_has_version() {
let json = read_plugin_json();
let version = json["version"]
.as_str()
.expect("version should be a string");
assert!(!version.is_empty(), "version should be non-empty");
}
#[test]
fn plugin_json_has_description() {
let json = read_plugin_json();
let desc = json["description"]
.as_str()
.expect("description should be a string");
assert!(!desc.is_empty(), "description should be non-empty");
}
#[test]
fn plugin_version_matches_cargo_version() {
let json = read_plugin_json();
let plugin_version = json["version"].as_str().unwrap();
let cargo_version = env!("CARGO_PKG_VERSION");
assert_eq!(
plugin_version, cargo_version,
"plugin.json version ({plugin_version}) must match Cargo.toml version ({cargo_version})"
);
}
#[test]
fn changes_md_has_entry_for_current_version() {
let content = fs::read_to_string("CHANGES.md").expect("CHANGES.md should exist");
let cargo_version = env!("CARGO_PKG_VERSION");
let expected_header = format!("## [{cargo_version}]");
assert!(
content.contains(&expected_header),
"CHANGES.md must contain a '{expected_header}' entry matching Cargo.toml version ({cargo_version})"
);
}
#[test]
fn builder_skill_has_allowed_tools() {
let props = aigent::read_properties(Path::new("skills/aigent-builder")).unwrap();
assert!(
props.allowed_tools.is_some(),
"builder skill should have allowed-tools"
);
}
#[test]
fn validator_skill_has_allowed_tools() {
let props = aigent::read_properties(Path::new("skills/aigent-validator")).unwrap();
assert!(
props.allowed_tools.is_some(),
"validator skill should have allowed-tools"
);
}
#[test]
fn builder_skill_name_matches_directory() {
let props = aigent::read_properties(Path::new("skills/aigent-builder")).unwrap();
assert_eq!(props.name, "aigent-builder");
}
#[test]
fn validator_skill_name_matches_directory() {
let props = aigent::read_properties(Path::new("skills/aigent-validator")).unwrap();
assert_eq!(props.name, "aigent-validator");
}
#[test]
fn install_script_exists_and_executable() {
let path = Path::new("install.sh");
assert!(path.exists(), "install.sh should exist");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = fs::metadata(path).unwrap().permissions();
assert!(perms.mode() & 0o111 != 0, "install.sh should be executable");
}
}
#[test]
fn install_script_has_posix_shebang() {
let content = fs::read_to_string("install.sh").unwrap();
assert!(
content.starts_with("#!/bin/sh"),
"install.sh should start with #!/bin/sh"
);
}
#[test]
fn install_script_has_checksum_verification() {
let content = fs::read_to_string("install.sh").unwrap();
assert!(
content.contains("sha256sum") || content.contains("shasum"),
"install.sh should reference sha256sum or shasum for checksum verification"
);
}
#[test]
fn install_script_downloads_checksums_txt() {
let content = fs::read_to_string("install.sh").unwrap();
assert!(
content.contains("checksums.txt"),
"install.sh should download checksums.txt"
);
}
#[test]
fn release_workflow_generates_checksums() {
let content =
fs::read_to_string(".github/workflows/release.yml").expect("release.yml should exist");
assert!(
content.contains("sha256sum"),
"release.yml should generate SHA256 checksums"
);
assert!(
content.contains("checksums.txt"),
"release.yml should create checksums.txt"
);
}
#[test]
fn hooks_json_exists_and_valid() {
let content = fs::read_to_string("hooks/hooks.json").expect("hooks/hooks.json should exist");
let json: serde_json::Value =
serde_json::from_str(&content).expect("hooks.json should be valid JSON");
assert!(json.is_object(), "hooks.json should be a JSON object");
assert!(
json.get("hooks").is_some(),
"hooks.json should have a top-level 'hooks' key"
);
}
#[test]
fn hooks_json_has_post_tool_use() {
let content = fs::read_to_string("hooks/hooks.json").unwrap();
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
let hooks = json["hooks"]
.as_object()
.expect("hooks should be an object");
assert!(
hooks.contains_key("PostToolUse"),
"hooks should contain PostToolUse event"
);
let post_tool_use = hooks["PostToolUse"]
.as_array()
.expect("PostToolUse should be an array");
assert!(
!post_tool_use.is_empty(),
"PostToolUse should have at least one matcher group"
);
}
#[test]
fn hooks_json_targets_write_and_edit() {
let content = fs::read_to_string("hooks/hooks.json").unwrap();
assert!(
content.contains("Write") && content.contains("Edit"),
"hooks should target Write and Edit tools"
);
}
#[test]
fn hooks_json_reads_input_from_stdin() {
let content = fs::read_to_string("hooks/hooks.json").unwrap();
assert!(
!content.contains("$TOOL_INPUT"),
"hooks should read input from stdin via jq, not from $TOOL_INPUT env variable"
);
assert!(
content.contains(".tool_input.file_path"),
"hooks should extract file_path from stdin JSON via jq"
);
}
#[test]
fn builder_skill_has_context_fork() {
let content = fs::read_to_string("skills/aigent-builder/SKILL.md").unwrap();
assert!(
content.contains("context: fork"),
"builder skill should have context: fork"
);
}
#[test]
fn validate_scorer_skill() {
aigent()
.args([
"validate",
"skills/aigent-scorer/",
"--target",
"claude-code",
])
.assert()
.success();
}
#[test]
fn scorer_skill_name_matches_directory() {
let props = aigent::read_properties(Path::new("skills/aigent-scorer")).unwrap();
assert_eq!(props.name, "aigent-scorer");
}
#[test]
fn scorer_skill_has_allowed_tools() {
let props = aigent::read_properties(Path::new("skills/aigent-scorer")).unwrap();
assert!(
props.allowed_tools.is_some(),
"scorer skill should have allowed-tools"
);
}
#[test]
fn scorer_skill_has_trigger_phrase() {
let props = aigent::read_properties(Path::new("skills/aigent-scorer")).unwrap();
let desc_lower = props.description.to_lowercase();
assert!(
desc_lower.contains("use when"),
"scorer skill description should contain trigger phrase"
);
}
#[test]
fn builder_skill_valid_with_claude_code_target() {
aigent()
.args([
"validate",
"skills/aigent-builder/",
"--target",
"claude-code",
])
.assert()
.success();
}