use std::path::{Path, PathBuf};
fn skill_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("skills/ilo")
}
fn skill_md_text() -> String {
let p = skill_dir().join("SKILL.md");
std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))
}
fn split_frontmatter(text: &str) -> (&str, &str) {
let rest = text
.strip_prefix("---\n")
.or_else(|| text.strip_prefix("---\r\n"))
.expect("SKILL.md must start with `---` frontmatter delimiter");
let end = rest
.find("\n---\n")
.or_else(|| rest.find("\n---\r\n"))
.expect("SKILL.md must close its frontmatter with a `---` line");
let fm = &rest[..end];
let body_start = end
+ if rest[end..].starts_with("\n---\r\n") {
6
} else {
5
};
(fm, &rest[body_start..])
}
fn top_level_scalar(fm: &str, key: &str) -> Option<String> {
let prefix = format!("{key}:");
for line in fm.lines() {
if line.starts_with(&prefix) {
let rest = line[prefix.len()..].trim();
if rest.is_empty() {
return None; }
let unquoted = if (rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2)
|| (rest.starts_with('\'') && rest.ends_with('\'') && rest.len() >= 2)
{
&rest[1..rest.len() - 1]
} else {
rest
};
return Some(unquoted.to_string());
}
}
None
}
fn has_top_level_key(fm: &str, key: &str) -> bool {
let prefix = format!("{key}:");
fm.lines().any(|l| l.starts_with(&prefix))
}
#[test]
fn skill_md_exists() {
let p = skill_dir().join("SKILL.md");
assert!(
p.is_file(),
"skills/ilo/SKILL.md missing at {}",
p.display()
);
}
#[test]
fn frontmatter_name_is_valid() {
let text = skill_md_text();
let (fm, _) = split_frontmatter(&text);
let name = top_level_scalar(fm, "name").expect("frontmatter must have `name`");
assert_eq!(name, "ilo", "skill name must be `ilo`");
assert!(name.len() <= 64, "name must be <= 64 chars");
let bytes = name.as_bytes();
assert!(
bytes.len() >= 2,
"name must be at least 2 chars to satisfy spec pattern"
);
assert!(
bytes[0].is_ascii_lowercase(),
"name must start with a lowercase letter"
);
let last = bytes[bytes.len() - 1];
assert!(
last.is_ascii_lowercase() || last.is_ascii_digit(),
"name must end with [a-z0-9]"
);
for &b in bytes {
assert!(
b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-',
"name must match ^[a-z][a-z0-9-]*[a-z0-9]$, got byte {b:#x}"
);
}
}
#[test]
fn frontmatter_description_is_valid() {
let text = skill_md_text();
let (fm, _) = split_frontmatter(&text);
let desc =
top_level_scalar(fm, "description").expect("frontmatter must have a scalar `description`");
assert!(!desc.is_empty(), "description must not be empty");
assert!(
desc.len() <= 1024,
"description must be <= 1024 chars (got {})",
desc.len()
);
}
#[test]
fn allowed_tools_is_string_form() {
let text = skill_md_text();
let (fm, _) = split_frontmatter(&text);
if !has_top_level_key(fm, "allowed-tools") {
return; }
let val = top_level_scalar(fm, "allowed-tools")
.expect("`allowed-tools`, if present, must be a scalar string (not a YAML sequence)");
assert!(
!val.is_empty(),
"`allowed-tools` must not be the empty string"
);
for tok in val.split_whitespace() {
assert!(
tok.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'),
"unexpected token in allowed-tools: {tok:?}"
);
}
}
#[test]
fn no_top_level_argument_hint() {
let text = skill_md_text();
let (fm, _) = split_frontmatter(&text);
assert!(
!has_top_level_key(fm, "argument-hint"),
"`argument-hint` must live under `metadata:`, not at the top level"
);
}
#[test]
fn body_has_no_claude_env_vars() {
let text = skill_md_text();
let (_fm, body) = split_frontmatter(&text);
if let Some(idx) = body.find("${CLAUDE_") {
let snippet_end = (idx + 60).min(body.len());
panic!(
"SKILL.md body references a Claude-Code-only env var; this breaks portability.\n\
First match: {:?}",
&body[idx..snippet_end]
);
}
}
#[test]
fn referenced_scripts_exist() {
let text = skill_md_text();
let (_fm, body) = split_frontmatter(&text);
let scripts_dir = skill_dir().join("scripts");
let mut checked = 0usize;
let mut search_from = 0usize;
while let Some(rel) = body[search_from..].find("scripts/") {
let abs = search_from + rel;
let after = &body[abs + "scripts/".len()..];
let end = after
.find(|c: char| {
c.is_whitespace() || c == '`' || c == ')' || c == '"' || c == '\'' || c == ','
})
.unwrap_or(after.len());
let name = &after[..end];
if !name.is_empty() {
let candidate = scripts_dir.join(name);
assert!(
candidate.is_file(),
"SKILL.md references `scripts/{name}` but {} does not exist",
candidate.display()
);
checked += 1;
}
search_from = abs + "scripts/".len() + end;
}
assert!(
checked > 0,
"expected at least one `scripts/<name>` reference in SKILL.md (e.g. ensure-ilo.sh)"
);
}
#[test]
fn body_has_canonical_sections() {
let text = skill_md_text();
let (_fm, body) = split_frontmatter(&text);
let required_headings = [
"## Setup",
"## Load the Full Spec",
"## Overview",
"## Core Syntax",
"## Types",
"## Guards",
"## Match",
"## Results and Error Handling",
"## Loops",
"## Higher-Order Functions",
"## Pipe Operator",
"## Records",
"## Maps",
"## Builtins Reference",
"## Naming Convention",
"## Running",
"## Multi-Function File Rules",
"## Examples",
"## Common Mistakes",
];
for h in required_headings {
assert!(
body.contains(h),
"SKILL.md is missing canonical heading: {h}"
);
}
}
#[test]
fn ensure_ilo_script_runs_without_claude_env_vars() {
let script = skill_dir().join("scripts/ensure-ilo.sh");
assert!(
script.is_file(),
"ensure-ilo.sh missing at {}",
script.display()
);
let syntax = std::process::Command::new("sh")
.arg("-n")
.arg(&script)
.output()
.expect("invoke sh -n");
assert!(
syntax.status.success(),
"ensure-ilo.sh failed `sh -n` syntax check: {}",
String::from_utf8_lossy(&syntax.stderr)
);
let path = std::env::var("PATH").unwrap_or_default();
let home = std::env::var("HOME").unwrap_or_default();
let out = std::process::Command::new("sh")
.arg(skill_dir().join("scripts/ensure-ilo.sh"))
.env_clear()
.env("PATH", &path)
.env("HOME", &home)
.current_dir(skill_dir())
.output()
.expect("run ensure-ilo.sh");
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stderr.trim().is_empty() || !stdout.trim().is_empty(),
"ensure-ilo.sh failed silently with no output (exit {:?}); a non-Claude host \
would have no idea what went wrong",
out.status.code()
);
}
}
#[allow(dead_code)]
fn _unused(_: &Path) {}