use std::path::PathBuf;
fn repo_root() -> PathBuf {
let manifest = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
PathBuf::from(manifest)
}
const SKILL_NAMES: &[&str] = &[
"ilo-language",
"ilo-language-records",
"ilo-builtins-core",
"ilo-builtins-math",
"ilo-builtins-io",
"ilo-builtins-text",
"ilo-errors",
"ilo-tools",
"ilo-engines",
"ilo-agent",
"ilo-examples",
"ilo-edit-loop",
];
const BYTE_BUDGET_PER_MODULE: usize = 8_500;
const BYTE_BUDGET_TOTAL: usize = 52_000;
fn read_skill(name: &str) -> String {
let p = repo_root().join("skills/ilo").join(format!("{name}.md"));
std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("failed to read {}: {e}", p.display()))
}
#[test]
fn every_skill_file_exists() {
for n in SKILL_NAMES {
let p = repo_root().join("skills/ilo").join(format!("{n}.md"));
assert!(p.exists(), "missing skill file: {}", p.display());
}
}
#[test]
fn every_skill_has_valid_frontmatter() {
for n in SKILL_NAMES {
let s = read_skill(n);
assert!(
s.starts_with("---\n"),
"{n}.md must start with `---` frontmatter delimiter"
);
let after = &s[4..];
let end = after
.find("\n---\n")
.unwrap_or_else(|| panic!("{n}.md must close frontmatter with `---`"));
let fm = &after[..end];
assert!(
fm.lines().any(|l| l.trim_start().starts_with("name:")),
"{n}.md frontmatter must have `name:`"
);
assert!(
fm.lines()
.any(|l| l.trim_start().starts_with("description:")),
"{n}.md frontmatter must have `description:`"
);
}
}
#[test]
fn every_description_starts_with_use_this_when() {
for n in SKILL_NAMES {
let s = read_skill(n);
let line = s
.lines()
.find(|l| l.trim_start().starts_with("description:"))
.unwrap_or_else(|| panic!("{n}.md missing description line"));
let value = line.split_once(':').map(|(_, v)| v.trim()).unwrap_or("");
let value = value.trim_matches('"');
assert!(
value.starts_with("Use this when"),
"{n}.md description must start with 'Use this when' (got: {value:.40})"
);
}
}
#[test]
fn every_skill_name_matches_filename() {
for n in SKILL_NAMES {
let s = read_skill(n);
let name_line = s
.lines()
.find(|l| l.trim_start().starts_with("name:"))
.unwrap_or_else(|| panic!("{n}.md missing name line"));
let value = name_line
.split_once(':')
.map(|(_, v)| v.trim())
.unwrap_or("");
let value = value.trim_matches('"');
assert_eq!(value, *n, "{n}.md frontmatter name must match filename");
}
}
#[test]
fn per_module_byte_budget_respected() {
for n in SKILL_NAMES {
let s = read_skill(n);
let len = s.len();
assert!(
len <= BYTE_BUDGET_PER_MODULE,
"{n}.md is {len} bytes, over the per-module budget of {BYTE_BUDGET_PER_MODULE}. \
Trim or split. The hard per-module token cap (1,000 for category modules, \
1,500 for ilo-language) is enforced by the tiktoken CI job."
);
}
}
#[test]
fn total_byte_budget_respected() {
let total: usize = SKILL_NAMES.iter().map(|n| read_skill(n).len()).sum();
assert!(
total <= BYTE_BUDGET_TOTAL,
"total skills size is {total} bytes, over the aggregate budget of {BYTE_BUDGET_TOTAL}. \
The hard 8,500-token cap is enforced by the tiktoken CI job."
);
}
#[test]
fn marketplace_lists_every_skill() {
let mp = repo_root().join(".claude-plugin/marketplace.json");
let raw = std::fs::read_to_string(&mp).expect("marketplace.json present");
for n in SKILL_NAMES {
assert!(raw.contains(n), "marketplace.json must list skill {n}");
}
}