use skref::errors::SkillError;
use skref::parser::{find_skill_md, parse_frontmatter, read_properties};
use std::fs;
use tempfile::tempdir;
#[test]
fn valid_frontmatter() {
let content =
"---\nname: my-skill\ndescription: A test skill\n---\n# My Skill\n\nInstructions here.\n";
let (metadata, body) = parse_frontmatter(content).unwrap();
assert_eq!(metadata.get("name").unwrap().as_str(), Some("my-skill"));
assert_eq!(
metadata.get("description").unwrap().as_str(),
Some("A test skill")
);
assert!(body.contains("# My Skill"));
}
#[test]
fn missing_frontmatter() {
let err = parse_frontmatter("# No frontmatter here").unwrap_err();
assert!(matches!(err, SkillError::Parse(_)));
assert!(err.to_string().contains("must start with YAML frontmatter"));
}
#[test]
fn unclosed_frontmatter() {
let content = "---\nname: my-skill\ndescription: A test skill\n";
let err = parse_frontmatter(content).unwrap_err();
assert!(err.to_string().contains("not properly closed"));
}
#[test]
fn invalid_yaml() {
let content = "---\nname: [invalid\ndescription: broken\n---\nBody here\n";
let err = parse_frontmatter(content).unwrap_err();
assert!(err.to_string().contains("Invalid YAML"));
}
#[test]
fn non_dict_frontmatter() {
let content = "---\n- just\n- a\n- list\n---\nBody\n";
let err = parse_frontmatter(content).unwrap_err();
assert!(err.to_string().contains("must be a YAML mapping"));
}
#[test]
fn read_valid_skill() {
let tmp = tempdir().unwrap();
let skill_dir = tmp.path().join("my-skill");
fs::create_dir(&skill_dir).unwrap();
fs::write(
skill_dir.join("SKILL.md"),
"---\nname: my-skill\ndescription: A test skill\nlicense: MIT\n---\n# My Skill\n",
)
.unwrap();
let props = read_properties(&skill_dir).unwrap();
assert_eq!(props.name, "my-skill");
assert_eq!(props.description, "A test skill");
assert_eq!(props.license.as_deref(), Some("MIT"));
}
#[test]
fn read_with_metadata() {
let tmp = tempdir().unwrap();
let skill_dir = tmp.path().join("my-skill");
fs::create_dir(&skill_dir).unwrap();
fs::write(
skill_dir.join("SKILL.md"),
"---\nname: my-skill\ndescription: A test skill\nmetadata:\n author: Test Author\n version: 1.0\n---\nBody\n",
)
.unwrap();
let props = read_properties(&skill_dir).unwrap();
assert_eq!(
props.metadata,
vec![
("author".to_string(), "Test Author".to_string()),
("version".to_string(), "1.0".to_string()),
]
);
}
#[test]
fn missing_skill_md() {
let tmp = tempdir().unwrap();
let err = read_properties(tmp.path()).unwrap_err();
assert!(err.to_string().contains("SKILL.md not found"));
}
#[test]
fn missing_name() {
let tmp = tempdir().unwrap();
let skill_dir = tmp.path().join("my-skill");
fs::create_dir(&skill_dir).unwrap();
fs::write(
skill_dir.join("SKILL.md"),
"---\ndescription: A test skill\n---\nBody\n",
)
.unwrap();
let err = read_properties(&skill_dir).unwrap_err();
assert!(matches!(err, SkillError::Validation { .. }));
assert!(err.to_string().contains("Missing required field") && err.to_string().contains("name"));
}
#[test]
fn missing_description() {
let tmp = tempdir().unwrap();
let skill_dir = tmp.path().join("my-skill");
fs::create_dir(&skill_dir).unwrap();
fs::write(
skill_dir.join("SKILL.md"),
"---\nname: my-skill\n---\nBody\n",
)
.unwrap();
let err = read_properties(&skill_dir).unwrap_err();
assert!(
err.to_string().contains("Missing required field")
&& err.to_string().contains("description")
);
}
#[test]
fn find_skill_md_prefers_uppercase() {
let tmp = tempdir().unwrap();
let skill_dir = tmp.path().join("my-skill");
fs::create_dir(&skill_dir).unwrap();
fs::write(skill_dir.join("SKILL.md"), "uppercase").unwrap();
let lower = skill_dir.join("skill.md");
if !lower.exists() {
fs::write(&lower, "lowercase").unwrap();
}
let result = find_skill_md(&skill_dir).unwrap();
assert_eq!(
result.file_name().unwrap().to_string_lossy().to_lowercase(),
"skill.md"
);
}
#[test]
fn find_skill_md_accepts_lowercase() {
let tmp = tempdir().unwrap();
let skill_dir = tmp.path().join("my-skill");
fs::create_dir(&skill_dir).unwrap();
fs::write(skill_dir.join("skill.md"), "lowercase").unwrap();
let result = find_skill_md(&skill_dir).unwrap();
assert_eq!(
result.file_name().unwrap().to_string_lossy().to_lowercase(),
"skill.md"
);
}
#[test]
fn find_skill_md_returns_none_when_missing() {
let tmp = tempdir().unwrap();
let skill_dir = tmp.path().join("my-skill");
fs::create_dir(&skill_dir).unwrap();
assert!(find_skill_md(&skill_dir).is_none());
}
#[test]
fn read_properties_with_lowercase_skill_md() {
let tmp = tempdir().unwrap();
let skill_dir = tmp.path().join("my-skill");
fs::create_dir(&skill_dir).unwrap();
fs::write(
skill_dir.join("skill.md"),
"---\nname: my-skill\ndescription: A test skill\n---\n# My Skill\n",
)
.unwrap();
let props = read_properties(&skill_dir).unwrap();
assert_eq!(props.name, "my-skill");
assert_eq!(props.description, "A test skill");
}
#[test]
fn read_with_allowed_tools() {
let tmp = tempdir().unwrap();
let skill_dir = tmp.path().join("my-skill");
fs::create_dir(&skill_dir).unwrap();
fs::write(
skill_dir.join("SKILL.md"),
"---\nname: my-skill\ndescription: A test skill\nallowed-tools: Bash(jq:*) Bash(git:*)\n---\nBody\n",
)
.unwrap();
let props = read_properties(&skill_dir).unwrap();
assert_eq!(
props.allowed_tools.as_deref(),
Some("Bash(jq:*) Bash(git:*)")
);
let d = props.to_dict();
assert_eq!(d["allowed-tools"], "Bash(jq:*) Bash(git:*)");
}