use std::fs;
use std::path::Path;
use super::*;
fn write(path: &Path, body: &str) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create parent");
}
fs::write(path, body).expect("write file");
}
const VALID: &str = "---\n\
name: rust-test\n\
description: Run the Rust test suite and interpret failures\n\
when_to_use: User asks about failing cargo tests\n\
allowed-tools: shell, grep\n\
---\n\
Run cargo test from the repo root.\n";
#[test]
fn parse_skill_md_happy_path() {
let s = parse_skill_md(VALID).expect("parse");
assert_eq!(s.name, "rust-test");
assert_eq!(s.description, "Run the Rust test suite and interpret failures");
assert_eq!(s.when_to_use.as_deref(), Some("User asks about failing cargo tests"));
assert_eq!(s.allowed_tools, vec!["shell", "grep"]);
assert!(s.instructions.contains("cargo test"));
}
#[test]
fn parse_skill_md_allowed_tools_space_separated() {
let src = "---\n\
name: x\n\
description: y\n\
allowed-tools: shell grep file_read\n\
---\nbody\n";
let s = parse_skill_md(src).expect("parse");
assert_eq!(s.allowed_tools, vec!["shell", "grep", "file_read"]);
}
#[test]
fn parse_skill_md_when_to_use_optional() {
let src = "---\nname: x\ndescription: y\n---\nbody\n";
let s = parse_skill_md(src).expect("parse");
assert!(s.when_to_use.is_none(), "absent key → None");
assert!(s.allowed_tools.is_empty());
}
#[test]
fn parse_skill_md_accepts_underscore_aliases() {
let src = "---\n\
name: x\n\
description: y\n\
when-to-use: trigger phrase\n\
allowed_tools: shell\n\
---\nbody\n";
let s = parse_skill_md(src).expect("parse");
assert_eq!(s.when_to_use.as_deref(), Some("trigger phrase"));
assert_eq!(s.allowed_tools, vec!["shell"]);
}
#[test]
fn parse_skill_md_tolerates_context_fork_key() {
let src = "---\nname: x\ndescription: y\ncontext: fork\n---\nbody\n";
let s = parse_skill_md(src).expect("parse");
assert_eq!(s.name, "x");
}
#[test]
fn parse_skill_md_strips_bom() {
let bom = "\u{feff}---\nname: b\ndescription: d\n---\nbody\n";
let s = parse_skill_md(bom).expect("parse");
assert_eq!(s.name, "b");
}
#[test]
fn parse_skill_md_accepts_crlf() {
let crlf = "---\r\nname: x\r\ndescription: y\r\n---\r\nbody\r\n";
let s = parse_skill_md(crlf).expect("parse");
assert_eq!(s.instructions, "body");
}
#[test]
fn parse_skill_md_missing_frontmatter_errors() {
let err = parse_skill_md("no fence\njust text\n").unwrap_err();
assert!(matches!(err, SkillError::MissingFrontmatter));
}
#[test]
fn parse_skill_md_missing_name_errors() {
let err = parse_skill_md("---\ndescription: d\n---\nbody\n").unwrap_err();
assert!(matches!(err, SkillError::MissingName));
}
#[test]
fn parse_skill_md_missing_description_errors() {
let err = parse_skill_md("---\nname: x\n---\nbody\n").unwrap_err();
assert!(matches!(err, SkillError::MissingDescription));
}
#[test]
fn parse_skill_md_empty_body_errors() {
let err = parse_skill_md("---\nname: x\ndescription: d\n---\n \n").unwrap_err();
assert!(matches!(err, SkillError::EmptyBody));
}
#[test]
fn load_skills_flat_layout() {
let dir = tempfile::tempdir().expect("tempdir");
write(&dir.path().join("a.md"), VALID);
write(&dir.path().join("b.md"), "---\nname: b\ndescription: db\n---\ninstructions-b\n");
write(&dir.path().join("readme.txt"), "skip");
let skills = load_skills_from(dir.path());
assert_eq!(skills.len(), 2);
}
#[test]
fn load_skills_subdir_layout() {
let dir = tempfile::tempdir().expect("tempdir");
write(&dir.path().join("rust-test").join("SKILL.md"), VALID);
write(&dir.path().join("ignored").join("notes.md"), "nope");
let skills = load_skills_from(dir.path());
assert_eq!(skills.len(), 1);
assert_eq!(skills[0].name, "rust-test");
}
#[test]
fn load_skills_silently_skips_malformed() {
let dir = tempfile::tempdir().expect("tempdir");
write(&dir.path().join("good.md"), VALID);
write(&dir.path().join("bad.md"), "no fence\n");
let skills = load_skills_from(dir.path());
assert_eq!(skills.len(), 1);
}
#[test]
fn discover_skills_prefers_apr_over_claude() {
let cwd = tempfile::tempdir().expect("tempdir");
write(
&cwd.path().join(".apr").join("skills").join("x.md"),
"---\nname: x\ndescription: FROM_APR\n---\nbody\n",
);
write(
&cwd.path().join(".claude").join("skills").join("x.md"),
"---\nname: x\ndescription: FROM_CLAUDE\n---\nbody\n",
);
let skills = discover_skills(cwd.path());
let x = skills.iter().find(|s| s.name == "x").expect("found");
assert_eq!(x.description, "FROM_APR");
}
#[test]
fn discover_skills_falls_back_to_claude_dir() {
let cwd = tempfile::tempdir().expect("tempdir");
write(&cwd.path().join(".claude").join("skills").join("y.md"), VALID);
let skills = discover_skills(cwd.path());
assert!(skills.iter().any(|s| s.name == "rust-test"));
}
#[test]
fn discover_skills_missing_dir_returns_empty() {
let cwd = tempfile::tempdir().expect("tempdir");
assert!(discover_skills(cwd.path()).is_empty());
}
#[test]
fn registry_register_resolve_names() {
let mut r = SkillRegistry::new();
assert!(r.is_empty());
let s = parse_skill_md(VALID).unwrap();
r.register(s);
assert_eq!(r.len(), 1);
assert!(r.resolve("rust-test").is_some());
assert_eq!(r.names(), vec!["rust-test"]);
assert!(r.resolve("nonexistent").is_none());
}
#[test]
fn registry_register_replaces_by_name() {
let mut r = SkillRegistry::new();
let s1 = parse_skill_md(VALID).unwrap();
let s2 = Skill {
name: "rust-test".into(),
description: "overridden".into(),
when_to_use: None,
allowed_tools: Vec::new(),
instructions: "override".into(),
};
r.register(s1);
r.register(s2);
assert_eq!(r.len(), 1);
assert_eq!(r.resolve("rust-test").unwrap().description, "overridden");
}
#[test]
fn registry_auto_match_by_when_to_use() {
let mut r = SkillRegistry::new();
r.register(parse_skill_md(VALID).unwrap());
let matched = r.auto_match("My failing cargo tests are broken");
assert!(matched.is_some());
assert_eq!(matched.unwrap().name, "rust-test");
}
#[test]
fn registry_auto_match_returns_none_when_no_when_to_use() {
let mut r = SkillRegistry::new();
let s = Skill {
name: "x".into(),
description: "d".into(),
when_to_use: None,
allowed_tools: Vec::new(),
instructions: "body".into(),
};
r.register(s);
assert!(r.auto_match("anything").is_none());
}
#[test]
fn registry_auto_match_case_insensitive() {
let mut r = SkillRegistry::new();
let s = Skill {
name: "x".into(),
description: "d".into(),
when_to_use: Some("Deployment Issue".into()),
allowed_tools: Vec::new(),
instructions: "body".into(),
};
r.register(s);
assert!(r.auto_match("seeing a deployment issue in prod").is_some());
}
#[test]
fn register_discovered_skills_into_counts() {
let cwd = tempfile::tempdir().expect("tempdir");
write(&cwd.path().join(".apr").join("skills").join("a.md"), VALID);
write(
&cwd.path().join(".apr").join("skills").join("b.md"),
"---\nname: b\ndescription: d\n---\nbody\n",
);
let mut r = SkillRegistry::new();
let n = register_discovered_skills_into(&mut r, cwd.path());
assert_eq!(n, 2);
assert!(r.resolve("rust-test").is_some());
assert!(r.resolve("b").is_some());
}
#[test]
fn register_discovered_skills_into_empty_dir_returns_zero() {
let cwd = tempfile::tempdir().expect("tempdir");
let mut r = SkillRegistry::new();
assert_eq!(register_discovered_skills_into(&mut r, cwd.path()), 0);
}
#[test]
fn error_display_messages() {
assert!(SkillError::MissingFrontmatter.to_string().contains("frontmatter"));
assert!(SkillError::MissingName.to_string().contains("name"));
assert!(SkillError::MissingDescription.to_string().contains("description"));
assert!(SkillError::EmptyBody.to_string().contains("empty"));
assert!(SkillError::Io("boom".into()).to_string().contains("boom"));
}