use super::path::with_home_unset;
use super::{
CURSOR_VENDOR_DENYLIST, DiscoveryOptions, ExtraPathMd, classify_extra_path_md, discover,
extra_path_is_loose_collection, find_skill_by_name, host_token_collapses_after_whitespace,
load_classified_extra_path_package, path_has_line_separator, str_has_line_separator,
unknown_or_skipped_skill, unknown_or_skipped_skill_message, unknown_or_skipped_skill_named,
validate_path, validate_path_with_options, walk_cwd_to_git_root, watch_dirs,
with_home_override,
};
use crate::skill::SKILL_MD_MAX_BYTES;
use crate::skip::{SkillSkip, SkipKind};
use crate::source::SkillSource;
use std::fs;
use std::path::PathBuf;
fn empty_home_discover(
cwd: &std::path::Path,
opts: &DiscoveryOptions,
) -> crate::skip::DiscoveryReport {
let home = tempfile::tempdir().expect("home");
with_home_override(Some(home.path().to_path_buf()), || discover(cwd, opts))
}
fn write_skill(dir: &std::path::Path, name: &str, body: &str) {
fs::create_dir_all(dir).expect("mkdir");
fs::write(
dir.join("SKILL.md"),
format!("---\nname: {name}\ndescription: {name} skill\n---\n{body}\n"),
)
.expect("write");
}
#[test]
fn cursor_vendor_denylist_is_frozen() {
assert_eq!(CURSOR_VENDOR_DENYLIST, &["shell", "canvas", "statusline"]);
}
#[test]
fn load_miss_names_parse_skip_not_unknown() {
let skip = SkillSkip {
path: PathBuf::from("/tmp/Bad_Name/SKILL.md"),
name: Some("Bad_Name".to_owned()),
kind: SkipKind::ParseError,
detail: "invalid YAML: name must be lowercase alphanumeric and hyphens only".to_owned(),
winner_path: None,
host_token: None,
};
let msg = unknown_or_skipped_skill_message("bad_name", &[skip]);
assert!(msg.contains("skipped skill: Bad_Name"), "msg={msg}");
assert!(msg.contains("parse_error"), "msg={msg}");
assert!(
msg.contains("/tmp/Bad_Name/SKILL.md"),
"load must name the skip path: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
assert_eq!(
unknown_or_skipped_skill_message("no-such", &[]),
"unknown skill: no-such"
);
assert_eq!(
unknown_or_skipped_skill_message("no\nsuch", &[]),
"unknown skill: no?such"
);
}
#[test]
fn load_miss_exposes_error_kind() {
let skip = SkillSkip {
path: PathBuf::from("/tmp/Bad_Name/SKILL.md"),
name: Some("Bad_Name".to_owned()),
kind: SkipKind::ParseError,
detail: "invalid YAML: name must be lowercase alphanumeric and hyphens only".to_owned(),
winner_path: None,
host_token: None,
};
let skipped = unknown_or_skipped_skill("bad_name", std::slice::from_ref(&skip));
assert_eq!(skipped.error_kind, "parse_error");
assert_eq!(
skipped.error,
unknown_or_skipped_skill_message("bad_name", std::slice::from_ref(&skip))
);
assert!(!skipped.is_not_found());
let json = serde_json::to_string(&skipped).expect("ser");
let v: serde_json::Value = serde_json::from_str(&json).expect("json");
assert_eq!(v["error_kind"], "parse_error", "json={json}");
assert_eq!(v["error"], skipped.error, "json={json}");
assert!(
v.get("errorKind").is_none(),
"error_kind must stay snake_case: {json}"
);
for kind in SkipKind::all() {
let row = SkillSkip {
path: PathBuf::from("/tmp/x/SKILL.md"),
name: Some("x".to_owned()),
kind,
detail: "d".to_owned(),
winner_path: None,
host_token: None,
};
let miss = unknown_or_skipped_skill("x", &[row]);
assert_eq!(miss.error_kind, kind.as_str(), "kind={kind}");
assert!(!miss.is_not_found(), "kind={kind}");
}
let unknown = unknown_or_skipped_skill("no-such", &[]);
assert_eq!(unknown.error_kind, "unknown_skill");
assert_eq!(unknown.error, "unknown skill: no-such");
assert!(unknown.is_not_found());
let path_like = unknown_or_skipped_skill("./pkg", &[]);
assert_eq!(path_like.error_kind, "unknown_skill");
assert_eq!(
path_like.error,
"unknown skill: ./pkg (looks like a path; pass the frontmatter name and --path / paths)"
);
assert!(
path_like.is_not_found(),
"path-like unknown stays unknown_skill: {}",
path_like.error
);
let slash_nl = unknown_or_skipped_skill("./foo\u{2028}bar", &[]);
assert_eq!(
slash_nl.error,
"unknown skill: ./foo?bar (looks like a path; pass the frontmatter name and --path / paths)"
);
assert_eq!(slash_nl.error.lines().count(), 1, "{}", slash_nl.error);
let injected = unknown_or_skipped_skill("no\u{2014}such", &[]);
assert_eq!(injected.error_kind, "unknown_skill");
assert_eq!(injected.error, "unknown skill: no-such");
let json = serde_json::to_string(&unknown).expect("ser");
let v: serde_json::Value = serde_json::from_str(&json).expect("json");
assert_eq!(v["error_kind"], "unknown_skill", "json={json}");
assert_eq!(v["error"], "unknown skill: no-such", "json={json}");
let mut keys: Vec<_> = v.as_object().expect("object").keys().cloned().collect();
keys.sort();
assert_eq!(
keys,
["error".to_owned(), "error_kind".to_owned()],
"SkillMiss peel is {{ error_kind, error }}; MCP/CLI must not invent kind: {json}"
);
}
#[test]
fn load_miss_suggests_one_close_hyphen_name() {
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("review-pr"), "review-pr", "body");
let cwd = tempfile::tempdir().expect("cwd");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(
find_skill_by_name(&report.skills, "review-pr").is_some(),
"fixture must load: {:?}",
report.skills
);
let names: Vec<&str> = report.skills.iter().map(|s| s.name.as_str()).collect();
let insert = unknown_or_skipped_skill_named("reviewpr", &report.skips, names.iter().copied());
assert!(
insert.error.contains("did you mean review-pr"),
"edit-distance-1 must hint: {}",
insert.error
);
assert!(
insert.error.starts_with("unknown skill: reviewpr"),
"{}",
insert.error
);
assert_eq!(insert.error.lines().count(), 1, "{}", insert.error);
let swap = unknown_or_skipped_skill_named("review_pr", &report.skips, names.iter().copied());
assert!(
swap.error.contains("did you mean review-pr"),
"hyphen/underscore swap must hint: {}",
swap.error
);
let nope = unknown_or_skipped_skill_named("nope", &report.skips, names.iter().copied());
assert_eq!(nope.error, "unknown skill: nope");
assert!(!nope.error.contains("did you mean"), "{}", nope.error);
let hostile =
unknown_or_skipped_skill_named("reviewpr\u{2028}x", &report.skips, names.iter().copied());
assert_eq!(hostile.error.lines().count(), 1, "{}", hostile.error);
assert!(
!hostile.error.contains('\u{2028}'),
"typed U+2028 must stay one sanitized line: {}",
hostile.error
);
assert!(
hostile.error.contains("unknown skill: reviewpr?x"),
"{}",
hostile.error
);
}
#[test]
fn load_miss_peeked_skip_name_can_hint() {
let skip = SkillSkip {
path: PathBuf::from("/tmp/review-pr/SKILL.md"),
name: Some("review-pr".to_owned()),
kind: SkipKind::ParseError,
detail: "invalid YAML".to_owned(),
winner_path: None,
host_token: None,
};
let msg = unknown_or_skipped_skill_message("reviewpr", std::slice::from_ref(&skip));
assert!(
msg.contains("did you mean review-pr"),
"peeked skip names are already on the miss path: {msg}"
);
assert!(
msg.starts_with("unknown skill: reviewpr"),
"close skip name is a typo, not a skip identity: {msg}"
);
}
#[test]
fn load_miss_two_close_tokens_does_not_guess() {
let miss = unknown_or_skipped_skill_named("foz", &[], ["foo", "fox"]);
assert_eq!(miss.error, "unknown skill: foz");
assert!(!miss.error.contains("did you mean"), "{}", miss.error);
}
#[test]
fn load_miss_exposes_path_when_skip_is_known() {
let skip = SkillSkip {
path: PathBuf::from("/tmp/Bad_Name/SKILL.md"),
name: Some("Bad_Name".to_owned()),
kind: SkipKind::ParseError,
detail: "invalid YAML: name must be lowercase alphanumeric and hyphens only".to_owned(),
winner_path: None,
host_token: None,
};
let skipped = unknown_or_skipped_skill("bad_name", std::slice::from_ref(&skip));
assert_eq!(
skipped.path.as_deref(),
Some(skip.path.as_path()),
"host must locate the skip without scraping Display"
);
let json = serde_json::to_string(&skipped).expect("ser");
let v: serde_json::Value = serde_json::from_str(&json).expect("json");
assert_eq!(
v["path"].as_str().map(std::path::Path::new),
Some(skip.path.as_path()),
"json={json}"
);
let unknown = unknown_or_skipped_skill("no-such", &[]);
assert!(
unknown.path.is_none(),
"unknown_skill has no SKILL.md to name"
);
let json = serde_json::to_string(&unknown).expect("ser");
let v: serde_json::Value = serde_json::from_str(&json).expect("json");
assert!(
v.get("path").is_none(),
"omit path on unknown_skill: {json}"
);
assert!(
unknown.winner_path.is_none(),
"unknown_skill has no collision winner"
);
assert!(
v.get("winner_path").is_none(),
"omit winner_path on unknown_skill: {json}"
);
}
#[test]
fn load_miss_exposes_winner_path_on_name_collision() {
let skip = SkillSkip {
path: PathBuf::from("/tmp/b/foo/SKILL.md"),
name: Some("foo".to_owned()),
kind: SkipKind::NameCollision,
detail: "lost to /tmp/a/foo/SKILL.md".to_owned(),
winner_path: Some(PathBuf::from("/tmp/a/foo/SKILL.md")),
host_token: None,
};
let miss = unknown_or_skipped_skill("foo", std::slice::from_ref(&skip));
assert_eq!(miss.error_kind, "name_collision");
assert_eq!(
miss.path.as_deref(),
Some(skip.path.as_path()),
"collision peel path is the loser SKILL.md"
);
assert_eq!(
miss.winner_path.as_deref(),
Some(std::path::Path::new("/tmp/a/foo/SKILL.md")),
"host must locate the winner without scraping lost to: {miss:?}"
);
let json = serde_json::to_string(&miss).expect("ser");
let v: serde_json::Value = serde_json::from_str(&json).expect("json");
assert_eq!(v["error_kind"], "name_collision", "json={json}");
assert_eq!(
v["winner_path"].as_str().map(std::path::Path::new),
Some(std::path::Path::new("/tmp/a/foo/SKILL.md")),
"json={json}"
);
assert!(
v.get("winnerPath").is_none(),
"SkillMiss winner_path must stay snake_case like error_kind: {json}"
);
let parse = SkillSkip {
path: PathBuf::from("/tmp/demo/SKILL.md"),
name: Some("demo".to_owned()),
kind: SkipKind::ParseError,
detail: "missing required field: name".to_owned(),
winner_path: None,
host_token: None,
};
let parse_miss = unknown_or_skipped_skill("demo", &[parse]);
assert!(
parse_miss.winner_path.is_none(),
"non-collision skip must omit winner_path"
);
let json = serde_json::to_string(&parse_miss).expect("ser");
let v: serde_json::Value = serde_json::from_str(&json).expect("json");
assert!(
v.get("winner_path").is_none(),
"omit winner_path when there is no winner: {json}"
);
}
#[test]
fn load_miss_skipped_tokens_stay_one_line() {
let skip = SkillSkip {
path: PathBuf::from("/tmp/foo\nbar/SKILL.md"),
name: Some("foo\u{2028}bar".to_owned()),
kind: SkipKind::ParseError,
detail: "invalid YAML: expected `key: value`, got: x\u{2029}y".to_owned(),
winner_path: None,
host_token: None,
};
let msg = unknown_or_skipped_skill_message("foo\u{2028}bar", &[skip]);
assert!(
msg.contains("skipped skill:"),
"peeked U+2028 name is still a skip, not unknown: {msg}"
);
assert!(
msg.contains("parse_error"),
"one-line skip must keep kind: {msg}"
);
assert!(
msg.contains("/tmp/foo?bar/SKILL.md"),
"one-line skip must keep the path: {msg}"
);
assert_eq!(
msg.lines().count(),
1,
"skipped-skill stderr must stay one line: {msg:?}"
);
assert!(
!msg.contains('\n') && !msg.contains('\u{2028}') && !msg.contains('\u{2029}'),
"skipped-skill must not echo line separators: {msg:?}"
);
}
#[test]
fn load_miss_names_unreadable_package_skip_not_unknown() {
let skip = SkillSkip {
path: PathBuf::from("/tmp/demo/SKILL.md"),
name: None,
kind: SkipKind::Unreadable,
detail: "Permission denied (os error 13)".to_owned(),
winner_path: None,
host_token: None,
};
let msg = unknown_or_skipped_skill_message("demo", &[skip]);
assert!(
msg.contains("skipped skill: demo"),
"unreadable package must not look missing: {msg}"
);
assert!(msg.contains("unreadable"), "msg={msg}");
assert!(!msg.contains("unknown skill"), "msg={msg}");
}
#[test]
fn load_miss_parse_skip_without_frontmatter_name_uses_package_dir() {
let skip = SkillSkip {
path: PathBuf::from("/tmp/demo/skill.md"),
name: None,
kind: SkipKind::ParseError,
detail: "missing required field: name".to_owned(),
winner_path: None,
host_token: None,
};
let msg = unknown_or_skipped_skill_message("DEMO", &[skip]);
assert!(
msg.contains("skipped skill: demo"),
"package dir is the identity when peek name is missing: {msg}"
);
assert!(msg.contains("parse_error"), "msg={msg}");
assert!(!msg.contains("unknown skill"), "msg={msg}");
}
#[test]
fn load_miss_blank_peeked_name_uses_package_dir() {
let skip = SkillSkip {
path: PathBuf::from("/tmp/demo/skill.md"),
name: Some(" ".to_owned()),
kind: SkipKind::ParseError,
detail: "name must be lowercase alphanumeric and hyphens only".to_owned(),
winner_path: None,
host_token: None,
};
let msg = unknown_or_skipped_skill_message("demo", &[skip]);
assert!(
msg.contains("skipped skill: demo"),
"blank peek name must not hide the package dir: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
}
#[test]
fn load_and_why_agree_on_named_and_nameless_collision() {
let named = SkillSkip {
path: PathBuf::from("/tmp/other/SKILL.md"),
name: Some("alpha".to_owned()),
kind: SkipKind::ParseError,
detail: "invalid YAML".to_owned(),
winner_path: None,
host_token: None,
};
let nameless = SkillSkip {
path: PathBuf::from("/tmp/alpha/SKILL.md"),
name: None,
kind: SkipKind::ParseError,
detail: "missing required field: name".to_owned(),
winner_path: None,
host_token: None,
};
let skips = [named, nameless];
let load_alpha = unknown_or_skipped_skill_message("alpha", &skips);
let why_alpha = crate::why(
&crate::skip::DiscoveryReport {
skills: vec![],
skips: skips.to_vec(),
},
Some("alpha"),
None,
None,
);
assert!(
load_alpha.contains("skipped skill"),
"load alpha must not be unknown: {load_alpha}"
);
assert!(why_alpha.unknown_skill_message().is_none());
assert_eq!(why_alpha.skips.len(), 2);
let load_other = unknown_or_skipped_skill_message("other", &skips);
let why_other = crate::why(
&crate::skip::DiscoveryReport {
skills: vec![],
skips: skips.to_vec(),
},
Some("other"),
None,
None,
);
assert!(
load_other.contains("skipped skill: alpha"),
"named skip in other/ must not look missing: {load_other}"
);
assert!(why_other.unknown_skill_message().is_none());
assert_eq!(why_other.skips.len(), 1);
}
#[test]
fn padded_frontmatter_name_discover_is_skipped_not_unknown() {
let root = tempfile::tempdir().expect("tmp");
let pkg = root.path().join(".agents").join("skills").join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("SKILL.md"),
"---\nname: \"demo \"\ndescription: trailing space in name\n---\nbody\n",
)
.expect("write");
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(report.skills.is_empty(), "skills={:?}", report.skills);
assert_eq!(report.skips.len(), 1, "skips={:?}", report.skips);
assert_eq!(report.skips[0].kind, SkipKind::ParseError);
let msg = unknown_or_skipped_skill_message("demo", &report.skips);
assert!(
msg.contains("skipped skill"),
"padded peek name must still be the demo package: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
let why = crate::why(&report, Some("demo"), None, None);
assert_eq!(why.skips.len(), 1);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn present_null_user_invocable_skip_is_parse_error_with_path() {
let root = tempfile::tempdir().expect("tmp");
let extra = root.path().join("extra");
let pkg = extra.join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
let skill_md = pkg.join("SKILL.md");
fs::write(
&skill_md,
"---\nname: demo\ndescription: d\nuser_invocable: null\n---\nbody\n",
)
.expect("write");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(report.skills.is_empty(), "skills={:?}", report.skills);
assert_eq!(report.skips.len(), 1, "skips={:?}", report.skips);
let skip = &report.skips[0];
assert_eq!(skip.kind, SkipKind::ParseError);
assert_eq!(skip.code(), "parse_error");
assert_eq!(skip.path, skill_md);
assert_eq!(skip.name.as_deref(), Some("demo"));
assert!(
skip.detail.contains("user_invocable") && skip.detail.contains("boolean"),
"detail must name the present-null bool: {}",
skip.detail
);
let miss = unknown_or_skipped_skill("demo", &report.skips);
assert_eq!(miss.error_kind, "parse_error");
assert!(!miss.is_not_found());
assert_eq!(miss.path.as_deref(), Some(skill_md.as_path()));
assert!(
!miss.error.contains("unknown skill"),
"present-null must not look missing: {}",
miss.error
);
let peel = serde_json::to_value(&miss).expect("ser");
assert_eq!(peel["error_kind"], "parse_error", "json={peel}");
assert_eq!(
peel["path"].as_str().map(std::path::Path::new),
Some(skill_md.as_path()),
"json={peel}"
);
let why = crate::why(&report, Some("demo"), None, None);
assert_eq!(why.skips.len(), 1);
assert_eq!(why.skips[0].kind, SkipKind::ParseError);
assert_eq!(why.skips[0].code(), "parse_error");
assert!(why.unknown_skill_miss().is_none());
let validated = validate_path(&skill_md);
assert!(!validated.ok, "validate={validated:?}");
let vmiss = validated.miss().expect("validate peel");
assert_eq!(vmiss.error_kind, "parse_error");
assert_eq!(vmiss.path.as_deref(), Some(skill_md.as_path()));
assert!(
vmiss.error.contains("user_invocable") && vmiss.error.contains("boolean"),
"validate peel must name the present-null bool: {}",
vmiss.error
);
}
#[test]
fn present_tilde_hyphen_user_invocable_skip_is_parse_error_with_path() {
let root = tempfile::tempdir().expect("tmp");
let extra = root.path().join("extra");
let pkg = extra.join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
let skill_md = pkg.join("SKILL.md");
fs::write(
&skill_md,
"---\nname: demo\ndescription: d\nuser-invocable: ~\n---\nbody\n",
)
.expect("write");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(report.skills.is_empty(), "skills={:?}", report.skills);
assert_eq!(report.skips.len(), 1, "skips={:?}", report.skips);
let skip = &report.skips[0];
assert_eq!(skip.kind, SkipKind::ParseError);
assert_eq!(skip.code(), "parse_error");
assert_eq!(skip.path, skill_md);
assert_eq!(skip.name.as_deref(), Some("demo"));
assert!(
skip.detail.contains("user_invocable") && skip.detail.contains("boolean"),
"hyphen present-null must peel the canonical bool name: {}",
skip.detail
);
let miss = unknown_or_skipped_skill("demo", &report.skips);
assert_eq!(miss.error_kind, "parse_error");
assert!(!miss.is_not_found());
assert_eq!(miss.path.as_deref(), Some(skill_md.as_path()));
assert!(
!miss.error.contains("unknown skill"),
"hyphen present-null must not look missing: {}",
miss.error
);
assert!(
miss.error.contains("user_invocable") && miss.error.contains("boolean"),
"load peel must name the canonical bool: {}",
miss.error
);
let peel = serde_json::to_value(&miss).expect("ser");
assert_eq!(peel["error_kind"], "parse_error", "json={peel}");
assert_eq!(
peel["path"].as_str().map(std::path::Path::new),
Some(skill_md.as_path()),
"json={peel}"
);
let why = crate::why(&report, Some("demo"), None, None);
assert_eq!(why.skips.len(), 1);
assert_eq!(why.skips[0].kind, SkipKind::ParseError);
assert_eq!(why.skips[0].code(), "parse_error");
assert!(why.unknown_skill_miss().is_none());
let validated = validate_path(&skill_md);
assert!(!validated.ok, "validate={validated:?}");
let vmiss = validated.miss().expect("validate peel");
assert_eq!(vmiss.error_kind, "parse_error");
assert_eq!(vmiss.path.as_deref(), Some(skill_md.as_path()));
assert!(
vmiss.error.contains("user_invocable") && vmiss.error.contains("boolean"),
"validate peel must name the canonical bool: {}",
vmiss.error
);
}
#[test]
fn present_null_disable_model_invocation_skip_is_parse_error_with_path() {
let root = tempfile::tempdir().expect("tmp");
let extra = root.path().join("extra");
let pkg = extra.join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
let skill_md = pkg.join("SKILL.md");
fs::write(
&skill_md,
"---\nname: demo\ndescription: d\ndisable_model_invocation: null\n---\nbody\n",
)
.expect("write");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(report.skills.is_empty(), "skills={:?}", report.skills);
assert_eq!(report.skips.len(), 1, "skips={:?}", report.skips);
let skip = &report.skips[0];
assert_eq!(skip.kind, SkipKind::ParseError);
assert_eq!(skip.code(), "parse_error");
assert_eq!(skip.path, skill_md);
assert_eq!(skip.name.as_deref(), Some("demo"));
assert!(
skip.detail.contains("disable_model_invocation") && skip.detail.contains("boolean"),
"detail must name the present-null bool: {}",
skip.detail
);
let miss = unknown_or_skipped_skill("demo", &report.skips);
assert_eq!(miss.error_kind, "parse_error");
assert!(!miss.is_not_found());
assert_eq!(miss.path.as_deref(), Some(skill_md.as_path()));
assert!(
!miss.error.contains("unknown skill"),
"present-null must not look missing: {}",
miss.error
);
let peel = serde_json::to_value(&miss).expect("ser");
assert_eq!(peel["error_kind"], "parse_error", "json={peel}");
assert_eq!(
peel["path"].as_str().map(std::path::Path::new),
Some(skill_md.as_path()),
"json={peel}"
);
let why = crate::why(&report, Some("demo"), None, None);
assert_eq!(why.skips.len(), 1);
assert_eq!(why.skips[0].kind, SkipKind::ParseError);
assert_eq!(why.skips[0].code(), "parse_error");
assert!(why.unknown_skill_miss().is_none());
let validated = validate_path(&skill_md);
assert!(!validated.ok, "validate={validated:?}");
let vmiss = validated.miss().expect("validate peel");
assert_eq!(vmiss.error_kind, "parse_error");
assert_eq!(vmiss.path.as_deref(), Some(skill_md.as_path()));
assert!(
vmiss.error.contains("disable_model_invocation") && vmiss.error.contains("boolean"),
"validate peel must name the present-null bool: {}",
vmiss.error
);
}
#[test]
fn present_tilde_hyphen_disable_model_invocation_skip_is_parse_error_with_path() {
let root = tempfile::tempdir().expect("tmp");
let extra = root.path().join("extra");
let pkg = extra.join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
let skill_md = pkg.join("SKILL.md");
fs::write(
&skill_md,
"---\nname: demo\ndescription: d\ndisable-model-invocation: ~\n---\nbody\n",
)
.expect("write");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(report.skills.is_empty(), "skills={:?}", report.skills);
assert_eq!(report.skips.len(), 1, "skips={:?}", report.skips);
let skip = &report.skips[0];
assert_eq!(skip.kind, SkipKind::ParseError);
assert_eq!(skip.code(), "parse_error");
assert_eq!(skip.path, skill_md);
assert_eq!(skip.name.as_deref(), Some("demo"));
assert!(
skip.detail.contains("disable_model_invocation") && skip.detail.contains("boolean"),
"hyphen present-null must peel the canonical bool name: {}",
skip.detail
);
let miss = unknown_or_skipped_skill("demo", &report.skips);
assert_eq!(miss.error_kind, "parse_error");
assert!(!miss.is_not_found());
assert_eq!(miss.path.as_deref(), Some(skill_md.as_path()));
assert!(
!miss.error.contains("unknown skill"),
"hyphen present-null must not look missing: {}",
miss.error
);
assert!(
miss.error.contains("disable_model_invocation") && miss.error.contains("boolean"),
"load peel must name the canonical bool: {}",
miss.error
);
let peel = serde_json::to_value(&miss).expect("ser");
assert_eq!(peel["error_kind"], "parse_error", "json={peel}");
assert_eq!(
peel["path"].as_str().map(std::path::Path::new),
Some(skill_md.as_path()),
"json={peel}"
);
let why = crate::why(&report, Some("demo"), None, None);
assert_eq!(why.skips.len(), 1);
assert_eq!(why.skips[0].kind, SkipKind::ParseError);
assert_eq!(why.skips[0].code(), "parse_error");
assert!(why.unknown_skill_miss().is_none());
let validated = validate_path(&skill_md);
assert!(!validated.ok, "validate={validated:?}");
let vmiss = validated.miss().expect("validate peel");
assert_eq!(vmiss.error_kind, "parse_error");
assert_eq!(vmiss.path.as_deref(), Some(skill_md.as_path()));
assert!(
vmiss.error.contains("disable_model_invocation") && vmiss.error.contains("boolean"),
"validate peel must name the canonical bool: {}",
vmiss.error
);
}
#[test]
fn lowercase_skill_md_nameless_parse_matches_case_fold() {
let cwd = tempfile::tempdir().expect("cwd");
let pkg = cwd.path().join("extra").join("Demo");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("skill.md"),
"---\ndescription: no name\n---\nbody\n",
)
.expect("write");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![cwd.path().join("extra").display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(report.skills.is_empty(), "skills={:?}", report.skills);
assert_eq!(report.skips.len(), 1, "skips={:?}", report.skips);
let msg = unknown_or_skipped_skill_message("demo", &report.skips);
assert!(
msg.contains("skipped skill: Demo"),
"lowercase skill.md parent dir must case-fold: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
let why = crate::why(&report, Some("DEMO"), None, None);
assert_eq!(why.skips.len(), 1);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn load_miss_root_file_and_empty_name_stay_unknown() {
let root = SkillSkip {
path: PathBuf::from("/tmp/.agents/skills/SKILL.md"),
name: None,
kind: SkipKind::RootFile,
detail: "put the file in a named subdirectory.".to_owned(),
winner_path: None,
host_token: None,
};
let dir = SkillSkip {
path: PathBuf::from("/tmp/.agents/skills"),
name: None,
kind: SkipKind::Unreadable,
detail: "Permission denied (os error 13)".to_owned(),
winner_path: None,
host_token: None,
};
assert_eq!(
unknown_or_skipped_skill_message("skills", std::slice::from_ref(&root)),
"unknown skill: skills"
);
assert_eq!(
unknown_or_skipped_skill_message("skills", &[dir]),
"unknown skill: skills"
);
assert_eq!(
unknown_or_skipped_skill_message(" ", &[root]),
"unknown skill: "
);
}
#[test]
fn agents_root_loads_named_package() {
let root = tempfile::tempdir().expect("tmp");
write_skill(
&root.path().join(".agents").join("skills").join("demo"),
"demo",
"hi",
);
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert_eq!(report.skills.len(), 1);
assert_eq!(report.skills[0].name, "demo");
assert_eq!(report.skills[0].source, SkillSource::Agents);
assert!(report.skips.is_empty());
}
#[test]
fn vendor_bline_is_off_by_default() {
let root = tempfile::tempdir().expect("tmp");
write_skill(
&root.path().join(".bline").join("skills").join("legacy"),
"legacy",
"x",
);
let off = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(off.skills.is_empty());
let on = empty_home_discover(
root.path(),
&DiscoveryOptions {
vendor_roots: vec!["bline".to_owned()],
..DiscoveryOptions::default()
},
);
assert_eq!(on.skills.len(), 1);
assert_eq!(
on.skills[0].source,
SkillSource::Vendor {
name: "bline".to_owned()
}
);
}
#[test]
fn first_name_wins_and_records_winner_path() {
let root = tempfile::tempdir().expect("tmp");
write_skill(
&root.path().join(".agents").join("skills").join("foo"),
"foo",
"first",
);
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("foo"), "foo", "second");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(report.skills.len(), 1);
assert_eq!(report.skills[0].content.trim(), "first");
assert_eq!(report.skips.len(), 1);
assert_eq!(report.skips[0].kind, SkipKind::NameCollision);
let winner = root
.path()
.join(".agents")
.join("skills")
.join("foo")
.join("SKILL.md");
let winner = winner.canonicalize().unwrap_or(winner);
let loser = extra.path().join("foo").join("SKILL.md");
assert_eq!(
report.skips[0].winner_path.as_deref(),
Some(winner.as_path()),
"collision winner_path must be the agents-tree foo, not any Some(_): {:?}",
report.skips[0].winner_path
);
assert_eq!(
report.skips[0].path, loser,
"collision skip path must be the extra-path loser"
);
}
#[test]
fn same_file_seen_twice_is_not_a_name_collision() {
let root = tempfile::tempdir().expect("tmp");
let collection = root.path().join("skills");
write_skill(&collection.join("foo"), "foo", "once");
let collection_arg = collection.display().to_string();
let repeated = empty_home_discover(
root.path(),
&DiscoveryOptions {
implicit_roots: false,
paths: vec![collection_arg.clone(), collection_arg],
..DiscoveryOptions::default()
},
);
assert_eq!(repeated.skills.len(), 1, "skills={:?}", repeated.skills);
assert_eq!(repeated.skills[0].content.trim(), "once");
assert!(
repeated
.skips
.iter()
.all(|skip| skip.kind != SkipKind::NameCollision),
"the same --path twice must not skip foo as a collision with itself: {:?}",
repeated.skips
);
let package = collection.join("foo");
let package_arg = package.display().to_string();
let package_twice = empty_home_discover(
root.path(),
&DiscoveryOptions {
implicit_roots: false,
paths: vec![package_arg.clone(), package_arg],
..DiscoveryOptions::default()
},
);
assert_eq!(
package_twice.skills.len(),
1,
"skills={:?}",
package_twice.skills
);
assert!(
package_twice
.skips
.iter()
.all(|skip| skip.kind != SkipKind::NameCollision),
"the same package --path twice must not self-collide: {:?}",
package_twice.skips
);
let repo = tempfile::tempdir().expect("repo");
write_skill(
&repo.path().join(".agents").join("skills").join("bar"),
"bar",
"implicit",
);
let agents = repo.path().join(".agents").join("skills");
let overlapped = empty_home_discover(
repo.path(),
&DiscoveryOptions {
paths: vec![agents.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(overlapped.skills.len(), 1, "skills={:?}", overlapped.skills);
assert_eq!(overlapped.skills[0].name, "bar");
assert!(
overlapped
.skips
.iter()
.all(|skip| skip.kind != SkipKind::NameCollision),
"explicit --path of the implicit .agents/skills tree must not self-collide: {:?}",
overlapped.skips
);
}
#[test]
fn extra_path_sibling_packages_list_in_name_order() {
let extra = tempfile::tempdir().expect("extra");
for name in ["zed", "mid", "aaa"] {
write_skill(&extra.path().join(name), name, name);
}
let cwd = tempfile::tempdir().expect("cwd");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
implicit_roots: false,
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
let names: Vec<&str> = report.skills.iter().map(|s| s.name.as_str()).collect();
assert_eq!(names, ["aaa", "mid", "zed"], "skills={names:?}");
}
#[test]
fn first_name_wins_uses_nfkc_identity_like_why_and_load() {
const WIN: &str = "ᾼ-pack";
const LOSE: &str = "ᾳ-pack";
assert_ne!(WIN, LOSE);
assert!(
crate::parse::skill_names_equal(WIN, LOSE),
"fixture must be one identity"
);
assert!(crate::parse::validate_skill_name(WIN).is_ok());
assert!(crate::parse::validate_skill_name(LOSE).is_ok());
let cwd = tempfile::tempdir().expect("cwd");
let first = tempfile::tempdir().expect("first");
let second = tempfile::tempdir().expect("second");
write_skill(&first.path().join(WIN), WIN, "first");
write_skill(&second.path().join(LOSE), LOSE, "second");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![
first.path().display().to_string(),
second.path().display().to_string(),
],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
[WIN],
"first-name-wins must use skill_names_equal, not == : skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(report.skills[0].content.trim(), "first");
assert_eq!(report.skips.len(), 1, "skips={:?}", report.skips);
assert_eq!(report.skips[0].kind, SkipKind::NameCollision);
assert!(report.skips[0].winner_path.is_some());
assert!(find_skill_by_name(&report.skills, LOSE).is_some());
assert!(find_skill_by_name(&report.skills, WIN).is_some());
let why = crate::why(&report, Some(LOSE), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert_eq!(why.skips.len(), 1, "why skips={:?}", why.skips);
assert_eq!(why.skips[0].kind, SkipKind::NameCollision);
assert!(why.unknown_skill_message().is_none());
let load_msg = unknown_or_skipped_skill_message(LOSE, &report.skips);
assert!(
load_msg.contains("skipped skill"),
"loser must be a named collision, not unknown: {load_msg}"
);
}
#[test]
fn first_name_wins_collides_nfkc_fullwidth_names() {
let cwd = tempfile::tempdir().expect("cwd");
let first = tempfile::tempdir().expect("first");
let second = tempfile::tempdir().expect("second");
write_skill(&first.path().join("foo"), "foo", "first");
write_skill(&second.path().join("foo"), "foo", "second");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![
first.path().display().to_string(),
second.path().display().to_string(),
],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["foo"],
"NFKC fullwidth name must lose to the first foo: {:?}",
report
);
assert_eq!(report.skills[0].content.trim(), "first");
assert_eq!(report.skips.len(), 1, "skips={:?}", report.skips);
assert_eq!(report.skips[0].kind, SkipKind::NameCollision);
assert!(find_skill_by_name(&report.skills, "foo").is_some());
let why = crate::why(&report, Some("foo"), None, None);
assert_eq!(why.loaded.len(), 1);
assert_eq!(why.skips.len(), 1);
assert_eq!(why.skips[0].kind, SkipKind::NameCollision);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn loose_skill_md_in_agents_root_is_root_file() {
let root = tempfile::tempdir().expect("tmp");
let skills = root.path().join(".agents").join("skills");
fs::create_dir_all(&skills).expect("mkdir");
fs::write(
skills.join("SKILL.md"),
"---\nname: loose\ndescription: loose\n---\nbody\n",
)
.expect("write");
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(report.skills.is_empty());
assert_eq!(report.skips.len(), 1);
assert_eq!(report.skips[0].kind, SkipKind::RootFile);
assert_eq!(
report.skips[0].name.as_deref(),
Some("loose"),
"root_file skip must keep the frontmatter name so load/why do not call it unknown"
);
let msg = unknown_or_skipped_skill_message("loose", &report.skips);
assert!(
msg.contains("skipped skill: loose"),
"load must name the skipped root file: {msg}"
);
assert!(msg.contains("root_file"), "msg={msg}");
assert!(!msg.contains("unknown skill"), "msg={msg}");
assert_eq!(
unknown_or_skipped_skill_message("skills", &report.skips),
"unknown skill: skills",
"skills-root parent dir is still not a package identity"
);
let why = crate::why(&report, Some("loose"), None, None);
assert_eq!(why.skips.len(), 1);
assert_eq!(why.skips[0].kind, SkipKind::RootFile);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn nameless_root_file_stays_unknown_on_load_and_why() {
let root = tempfile::tempdir().expect("tmp");
let skills = root.path().join(".agents").join("skills");
fs::create_dir_all(&skills).expect("mkdir");
fs::write(
skills.join("SKILL.md"),
"---\ndescription: no name\n---\nbody\n",
)
.expect("write");
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(report.skills.is_empty(), "skills={:?}", report.skills);
assert_eq!(report.skips.len(), 1, "skips={:?}", report.skips);
assert_eq!(report.skips[0].kind, SkipKind::RootFile);
assert_eq!(report.skips[0].name, None);
assert_eq!(
unknown_or_skipped_skill_message("skills", &report.skips),
"unknown skill: skills"
);
let why = crate::why(&report, Some("skills"), None, None);
assert!(why.skips.is_empty());
assert_eq!(
why.unknown_skill_message().as_deref(),
Some("unknown skill: skills")
);
}
#[test]
fn root_file_same_name_as_package_does_not_block_load() {
let root = tempfile::tempdir().expect("tmp");
let skills = root.path().join(".agents").join("skills");
fs::create_dir_all(&skills).expect("mkdir");
fs::write(
skills.join("SKILL.md"),
"---\nname: demo\ndescription: loose\n---\nloose body\n",
)
.expect("write");
write_skill(&skills.join("demo"), "demo", "package body");
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["demo"],
"package must still load: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(report.skills[0].content.trim(), "package body");
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::RootFile && s.name.as_deref() == Some("demo")),
"loose file must stay a named root_file skip: {:?}",
report.skips
);
let loaded = find_skill_by_name(&report.skills, "demo");
assert!(loaded.is_some(), "load demo must hit the package");
let load_msg = unknown_or_skipped_skill_message("demo", &report.skips);
let why = crate::why(&report, Some("demo"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert_eq!(why.skips.len(), 1, "why skips={:?}", why.skips);
assert_eq!(why.skips[0].kind, SkipKind::RootFile);
assert!(
why.unknown_skill_message().is_none(),
"why must not call a loaded name unknown"
);
assert!(
loaded.is_some(),
"load/why disagree: load={load_msg} why_unknown={:?}",
why.unknown_skill_message()
);
}
#[test]
fn extra_path_skills_subdir_root_file_and_package_same_name() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let skills = extra.path().join("skills");
fs::create_dir_all(&skills).expect("mkdir");
fs::write(
skills.join("SKILL.md"),
"---\nname: demo\ndescription: loose\n---\nloose\n",
)
.expect("write");
write_skill(&skills.join("demo"), "demo", "from extra");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["demo"],
"extra-path skills/ package must load: {:?}",
report
);
assert_eq!(
report.skills[0].content.trim(),
"from extra",
"must load skills/demo, not the loose skills/SKILL.md body: {:?}",
report.skills[0]
);
assert_eq!(
report.skills[0].source_path.as_deref(),
Some(skills.join("demo").join("SKILL.md").as_path()),
"loaded path must be the skills/ package, not the loose file"
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::RootFile && s.name.as_deref() == Some("demo")),
"skips={:?}",
report.skips
);
let why = crate::why(&report, Some("DEMO"), None, None);
assert_eq!(why.loaded.len(), 1);
assert_eq!(why.loaded[0].name, "demo");
assert_eq!(why.skips.len(), 1);
assert_eq!(why.skips[0].kind, SkipKind::RootFile);
assert!(why.unknown_skill_message().is_none());
let loaded = find_skill_by_name(&report.skills, "demo").expect("demo");
assert_eq!(loaded.content.trim(), "from extra");
}
#[test]
fn extra_path_root_skill_md_does_not_hide_skills_subdir_package() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
fs::write(
extra.path().join("SKILL.md"),
"---\nname: loose\ndescription: leftover root file\n---\nloose\n",
)
.expect("write");
write_skill(&extra.path().join("skills").join("public"), "public", "ok");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load when extra-path has a leftover SKILL.md: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::RootFile && s.name.as_deref() == Some("loose") }),
"leftover extra-path SKILL.md must be root_file: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "public").is_some());
let load_msg = unknown_or_skipped_skill_message("loose", &report.skips);
assert!(
load_msg.contains("skipped skill: loose"),
"leftover root SKILL.md must stay a named skip: {load_msg}"
);
assert!(!load_msg.contains("unknown skill"), "{load_msg}");
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let why_loose = crate::why(&report, Some("loose"), None, None);
assert!(why_loose.loaded.is_empty());
assert_eq!(why_loose.skips[0].kind, SkipKind::RootFile);
assert!(why_loose.unknown_skill_message().is_none());
}
#[test]
fn extra_path_empty_skills_subdir_does_not_hide_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = corpus_dir().join("leftover/empty-nested-skills");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"empty extra/skills must not hide leftover sibling packages: skills={:?} skips={:?}",
report.skills,
report.skips
);
let loaded = find_skill_by_name(&report.skills, "wanted").expect("wanted");
assert!(
loaded.content.contains("from-sibling"),
"loaded leftover sibling body: {:?}",
loaded.content
);
assert!(
report.skips.is_empty(),
"empty extra/skills is not a skip: {:?}",
report.skips
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, &extra),
"leftover extra-path root must be watched: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &extra.join("skills")),
"empty extra/skills is not a discover walk: {dirs:?}"
);
}
#[test]
fn extra_path_collection_root_loads_siblings_next_to_empty_skills() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("wanted"), "wanted", "from-wanted");
write_skill(&extra.path().join("other"), "other", "from-other");
fs::create_dir_all(extra.path().join("skills")).expect("mkdir skills");
fs::write(
extra.path().join("skills").join("junk.txt"),
"not a package",
)
.expect("write junk");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
let mut names: Vec<_> = report.skills.iter().map(|s| s.name.as_str()).collect();
names.sort_unstable();
assert_eq!(
names,
["other", "wanted"],
"empty extra/skills plus junk must not hide sibling packages: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report.skips.is_empty(),
"empty extra/skills junk is not a skip: {:?}",
report.skips
);
}
#[test]
fn extra_path_collection_root_leftover_skill_md_still_loads_siblings() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
fs::write(
extra.path().join("SKILL.md"),
"---\nname: loose\ndescription: leftover root file\n---\nloose\n",
)
.expect("write leftover");
write_skill(&extra.path().join("wanted"), "wanted", "from-wanted");
write_skill(&extra.path().join("other"), "other", "from-other");
fs::create_dir_all(extra.path().join("skills")).expect("mkdir skills");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
let mut names: Vec<_> = report.skills.iter().map(|s| s.name.as_str()).collect();
names.sort_unstable();
assert_eq!(
names,
["other", "wanted"],
"leftover extra/SKILL.md plus empty extra/skills must not hide siblings: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::RootFile && s.name.as_deref() == Some("loose")),
"leftover extra/SKILL.md must stay root_file: {:?}",
report.skips
);
assert!(
find_skill_by_name(&report.skills, "loose").is_none(),
"leftover extra/SKILL.md must not load: {:?}",
report.skills
);
}
#[test]
fn extra_path_collection_root_loads_siblings_next_to_skills_packages() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("wanted"), "wanted", "from-wanted");
write_skill(&extra.path().join("other"), "other", "from-other");
write_skill(
&extra.path().join("skills").join("nested"),
"nested",
"from-nested",
);
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
let mut names: Vec<_> = report.skills.iter().map(|s| s.name.as_str()).collect();
names.sort_unstable();
assert_eq!(
names,
["nested", "other", "wanted"],
"extra/skills packages must not hide leftover siblings: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report.skips.is_empty(),
"sibling plus extra/skills packages is not a skip: {:?}",
report.skips
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"must watch extra-path root: {dirs:?}"
);
assert!(
watch_paths_contain(&dirs, &extra.path().join("skills")),
"must watch extra/skills when discover walks it: {dirs:?}"
);
}
#[test]
fn extra_path_collection_root_leftover_plus_skills_packages_loads_siblings() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
fs::write(
extra.path().join("SKILL.md"),
"---\nname: loose\ndescription: leftover root file\n---\nloose\n",
)
.expect("write leftover");
write_skill(&extra.path().join("wanted"), "wanted", "from-wanted");
write_skill(
&extra.path().join("skills").join("nested"),
"nested",
"from-nested",
);
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
let mut names: Vec<_> = report.skills.iter().map(|s| s.name.as_str()).collect();
names.sort_unstable();
assert_eq!(
names,
["nested", "wanted"],
"leftover extra/SKILL.md plus extra/skills packages must not hide siblings: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::RootFile && s.name.as_deref() == Some("loose")),
"leftover extra/SKILL.md must stay root_file: {:?}",
report.skips
);
}
#[test]
fn path_has_line_separator_detects_lf() {
assert!(path_has_line_separator(std::path::Path::new("evil\nroot")));
assert!(path_has_line_separator(std::path::Path::new(
"evil\u{2028}root"
)));
assert!(!path_has_line_separator(std::path::Path::new("wanted")));
assert!(str_has_line_separator("evil\n/.."));
assert!(str_has_line_separator("evil\r/.."));
assert!(str_has_line_separator("evil\u{2028}/.."));
assert!(!str_has_line_separator("evil/.."));
}
#[test]
fn host_token_collapses_padded_slash_dotdot_like_validate() {
for raw in ["/ ..", "/\t..", "/\u{00a0}.."] {
assert!(
host_token_collapses_after_whitespace(raw),
"padded-slash token {raw:?} must collapse like validate"
);
}
assert!(
!host_token_collapses_after_whitespace(".."),
"bare .. is cwd parent, not a collapse"
);
assert!(
!host_token_collapses_after_whitespace("‥"),
"NFKC ‥ stays an extra-path rewrite"
);
assert!(
!host_token_collapses_after_whitespace("/"),
"explicit / is a host-chosen root"
);
assert!(
!host_token_collapses_after_whitespace(".agents/skills/secret/evil/ ‥ "),
"relative padded NFKC ‥ stays an ignore rewrite"
);
}
#[test]
fn extra_path_skills_named_package_does_not_hide_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("skills"), "skills", "PACKAGE_BODY");
write_skill(
&extra.path().join("skills").join("evil"),
"evil",
"NESTED_SECRET",
);
write_skill(&extra.path().join("wanted"), "wanted", "from-sibling");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
let mut names: Vec<_> = report.skills.iter().map(|s| s.name.as_str()).collect();
names.sort_unstable();
assert_eq!(
names,
["skills", "wanted"],
"extra/skills named package must not hide leftover sibling: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"extra/skills named package is not a leftover root file: {:?}",
report.skips
);
assert!(
find_skill_by_name(&report.skills, "evil").is_none(),
"named extra/skills must not scan nested SKILL.md: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load: {:?}",
report.skills
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"must watch extra-path root: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &extra.path().join("skills")),
"named extra/skills is not a discover walk: {dirs:?}"
);
}
#[test]
fn extra_path_skills_leftover_skill_md_does_not_hide_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("skills"), "loose", "LEFTOVER");
write_skill(&extra.path().join("wanted"), "wanted", "from-sibling");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"leftover extra/skills/SKILL.md must not hide leftover sibling: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
find_skill_by_name(&report.skills, "loose").is_none(),
"leftover extra/skills/SKILL.md must not load as a package: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::RootFile && s.name.as_deref() == Some("loose") }),
"leftover extra/skills/SKILL.md must stay a root_file skip: {:?}",
report.skips
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"must watch extra-path root: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &extra.path().join("skills")),
"leftover extra/skills/SKILL.md is not a discover walk: {dirs:?}"
);
}
#[cfg(unix)]
#[test]
fn extra_path_skills_fifo_skill_md_does_not_hide_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let fifo = extra.path().join("skills").join("SKILL.md");
fs::create_dir_all(fifo.parent().expect("parent")).expect("mkdir");
mkfifo(&fifo);
write_skill(&extra.path().join("wanted"), "wanted", "from-sibling");
let report = discover_extra_path_with_timeout(
extra.path().to_path_buf(),
&fifo,
"discover must not block on extra/skills/SKILL.md FIFO",
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"FIFO extra/skills/SKILL.md must not hide leftover sibling: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("SKILL.md")
&& s.detail.contains("regular file")
}),
"FIFO extra/skills/SKILL.md must stay unreadable: {:?}",
report.skips
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"must watch extra-path root: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &extra.path().join("skills")),
"FIFO extra/skills/SKILL.md is not a discover walk: {dirs:?}"
);
}
#[test]
fn extra_path_named_package_with_skills_subdir_does_not_scan_nested() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("skills").join("evil"), "evil", "NESTED_SECRET");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"matching extra-path package must not scan its skills/ tree: {:?}",
report
);
assert!(find_skill_by_name(&report.skills, "evil").is_none());
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load: {:?}",
report.skills
);
}
#[cfg(unix)]
#[test]
fn extra_path_escaped_root_skill_md_does_not_hide_skills_subdir() {
let cwd = tempfile::tempdir().expect("cwd");
let outside = tempfile::tempdir().expect("out");
fs::write(
outside.path().join("secret.md"),
"---\nname: stolen\ndescription: leaked\n---\nSECRET_BODY\n",
)
.expect("write");
let extra = tempfile::tempdir().expect("extra");
std::os::unix::fs::symlink(
outside.path().join("secret.md"),
extra.path().join("SKILL.md"),
)
.expect("symlink");
write_skill(&extra.path().join("skills").join("public"), "public", "ok");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load when extra-path SKILL.md escapes: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report.skills.iter().all(|s| s.name != "stolen"),
"must not load escaped extra-path SKILL.md: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("SECRET_BODY")),
"escaped SKILL.md body must not be loaded: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.detail.contains("escapes")
&& s.name.is_none()
&& s.path.ends_with("SKILL.md")
}),
"escaped extra-path SKILL.md must be unreadable, not peeked: {:?}",
report.skips
);
let stolen = unknown_or_skipped_skill_message("stolen", &report.skips);
assert!(
stolen.contains("unknown skill: stolen"),
"must not peek stolen from an escaped extra-path SKILL.md: {stolen}"
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1);
assert!(why.unknown_skill_message().is_none());
}
#[cfg(unix)]
#[test]
fn extra_path_escaped_root_skill_md_does_not_hide_sibling_package() {
let cwd = tempfile::tempdir().expect("cwd");
let outside = tempfile::tempdir().expect("out");
fs::write(
outside.path().join("secret.md"),
"---\nname: stolen\ndescription: leaked\n---\nSECRET_BODY\n",
)
.expect("write");
let extra = tempfile::tempdir().expect("extra");
std::os::unix::fs::symlink(
outside.path().join("secret.md"),
extra.path().join("SKILL.md"),
)
.expect("symlink");
write_skill(&extra.path().join("public"), "public", "ok");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling public must still load when extra-path SKILL.md escapes: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report.skills.iter().all(|s| s.name != "stolen"),
"must not load escaped extra-path SKILL.md: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("SECRET_BODY")),
"escaped SKILL.md body must not be loaded: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.detail.contains("escapes")
&& s.name.is_none()
&& s.path.ends_with("SKILL.md")
}),
"escaped extra-path SKILL.md must be unreadable, not peeked: {:?}",
report.skips
);
let stolen = unknown_or_skipped_skill_message("stolen", &report.skips);
assert!(
stolen.contains("unknown skill: stolen"),
"must not peek stolen from an escaped extra-path SKILL.md: {stolen}"
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1);
assert!(why.unknown_skill_message().is_none());
let loaded = find_skill_by_name(&report.skills, "public").expect("public");
assert_eq!(loaded.content.trim(), "ok");
}
#[test]
fn extra_path_dir_loose_file_does_not_hide_same_name_package() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
fs::write(
extra.path().join("SKILL.md"),
"---\nname: demo\ndescription: loose\n---\nloose\n",
)
.expect("write");
write_skill(&extra.path().join("demo"), "demo", "package");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["demo"],
"child package must not vanish behind a loose SKILL.md: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::RootFile && s.name.as_deref() == Some("demo")),
"loose extra-path SKILL.md must be root_file, not a package: {:?}",
report.skips
);
let why = crate::why(&report, Some("demo"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(
why.skips.iter().any(|s| s.kind == SkipKind::RootFile),
"why skips={:?}",
why.skips
);
assert!(why.unknown_skill_message().is_none());
assert!(find_skill_by_name(&report.skills, "demo").is_some());
}
#[test]
fn extra_path_single_package_dir_still_loads() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("demo");
write_skill(&pkg, "demo", "only package");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(report.skills.len(), 1, "skills={:?}", report.skills);
assert_eq!(report.skills[0].name, "demo");
assert!(report.skips.is_empty(), "skips={:?}", report.skips);
}
#[test]
fn extra_path_classify_named_package_prefetches_body() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("demo");
write_skill(&pkg, "demo", "PREFETCH_BODY");
let skill_file = pkg.join("SKILL.md");
match classify_extra_path_md(&pkg, &skill_file, false) {
ExtraPathMd::Parsed(skill) => {
assert_eq!(skill.name, "demo");
assert_eq!(skill.content.trim(), "PREFETCH_BODY");
}
other => panic!("named extra-path package must be Parsed, got {other:?}"),
}
assert!(
!extra_path_is_loose_collection(&pkg, &skill_file, false),
"named extra-path package is not a leftover collection"
);
}
#[test]
fn extra_path_load_reuses_classified_parse() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("demo");
write_skill(&pkg, "demo", "DISK_BODY");
let skill_file = pkg.join("SKILL.md");
let skill = crate::Skill::new("demo", "preparsed", "PREPARSED_BODY");
let mut skills = Vec::new();
let mut skips = Vec::new();
load_classified_extra_path_package(
&skill_file,
ExtraPathMd::Parsed(skill),
&SkillSource::ExtraPath,
&[],
&DiscoveryOptions::default(),
&mut skills,
&mut skips,
);
assert!(skips.is_empty(), "skips={skips:?}");
assert_eq!(skills.len(), 1, "skills={skills:?}");
assert_eq!(skills[0].name, "demo");
assert_eq!(
skills[0].content, "PREPARSED_BODY",
"load must keep the classified parse, not re-read or re-parse disk"
);
assert_eq!(skills[0].source, SkillSource::ExtraPath);
}
#[test]
fn extra_path_named_parse_fail_is_parsed_once() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
let skill_file = pkg.join("SKILL.md");
let body = "---\nname: demo\n---\nNO_DESC\n";
fs::write(&skill_file, body).expect("write");
let _ = crate::parse::take_parse_skill_contents();
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"missing description must not load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::ParseError
&& s.path == skill_file
&& s.name.as_deref() == Some("demo")
}),
"named extra-path parse fail must stay a parse_error skip: {:?}",
report.skips
);
let parses = crate::parse::take_parse_skill_contents();
let demo_parses = parses.iter().filter(|c| c.contains("NO_DESC")).count();
assert_eq!(
demo_parses, 1,
"classify parse_skill fail must not be parsed again in load: parses={parses:?}"
);
}
fn take_read_skill_md_paths() -> Vec<PathBuf> {
super::take_read_skill_md_paths()
}
#[test]
fn leftover_extra_path_root_skill_md_is_opened_once() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let leftover = extra.path().join("SKILL.md");
fs::write(
&leftover,
"---\ndescription: leftover without name\n---\nloose\n",
)
.expect("write leftover");
write_skill(&extra.path().join("skills").join("public"), "public", "ok");
let _ = take_read_skill_md_paths();
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load: {report:?}"
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::RootFile && s.path == leftover),
"leftover extra/SKILL.md must stay a skip: {:?}",
report.skips
);
let reads = take_read_skill_md_paths();
let leftover_opens = reads.iter().filter(|p| *p == &leftover).count();
assert_eq!(
leftover_opens, 1,
"classify leftover extra/SKILL.md must not be opened again for the root_file skip: {reads:?}"
);
}
#[test]
fn leftover_extra_path_sibling_skill_md_is_opened_once() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let leftover = extra.path().join("SKILL.md");
fs::write(
&leftover,
"---\nname: loose\ndescription: leftover root file\n---\nloose\n",
)
.expect("write leftover");
write_skill(&extra.path().join("public"), "public", "from-sibling");
let _ = take_read_skill_md_paths();
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling public must still load: {report:?}"
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::RootFile && s.path == leftover),
"leftover extra/SKILL.md must stay a skip: {:?}",
report.skips
);
let reads = take_read_skill_md_paths();
let leftover_opens = reads.iter().filter(|p| *p == &leftover).count();
assert_eq!(
leftover_opens, 1,
"classify leftover extra/SKILL.md plus sibling must not be opened again: {reads:?}"
);
}
#[test]
fn extra_path_classify_leftover_skill_md_dir_with_siblings_is_collection() {
let extra = tempfile::tempdir().expect("extra");
let leftover = extra.path().join("SKILL.md");
fs::create_dir_all(&leftover).expect("mkdir leftover SKILL.md dir");
write_skill(&extra.path().join("public"), "public", "ok");
match classify_extra_path_md(extra.path(), &leftover, false) {
ExtraPathMd::Collection { .. } => {}
other => {
panic!("leftover extra/SKILL.md dir + sibling must be Collection, got {other:?}")
}
}
assert!(
extra_path_is_loose_collection(extra.path(), &leftover, false),
"leftover extra/SKILL.md directory with siblings is a collection"
);
}
#[test]
fn extra_path_named_package_does_not_scan_nested_skill() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"named extra-path package must load; nested SKILL.md is not a sibling: {:?}",
report
);
assert_eq!(report.skills[0].content.trim(), "PACKAGE_BODY");
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load: {:?}",
report.skills
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"wanted/SKILL.md must not become a root_file skip: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "wanted").is_some());
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let why = crate::why(&report, Some("wanted"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn extra_path_nested_unknown_map_still_loads_top_level_name() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("SKILL.md"),
"---\nname: wanted\ndescription: docs\nhooks:\n name: pre-commit\n user-invocable: false\n---\nPACKAGE_BODY\n",
)
.expect("write");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"nested hooks.name must not become the skill name: {report:?}"
);
let skill = find_skill_by_name(&report.skills, "wanted").expect("load wanted");
assert!(
skill.user_invocable,
"nested user-invocable must not flip the omitted default"
);
assert!(skill.content.contains("PACKAGE_BODY"));
assert!(find_skill_by_name(&report.skills, "pre-commit").is_none());
let miss = unknown_or_skipped_skill("pre-commit", &report.skips);
assert_eq!(
miss.error_kind, "unknown_skill",
"nested name must not peel a skip: {miss:?}"
);
assert!(miss.path.is_none(), "unknown must omit path: {miss:?}");
}
#[test]
fn extra_path_unicode_indent_hooks_name_still_loads_top_level() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("SKILL.md"),
"---\nname: wanted\ndescription: docs\nhooks:\n\u{00a0}name: pre-commit\n\u{00a0}metadata:\n author: nest\n---\nPACKAGE_BODY\n",
)
.expect("write");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"unicode-indented hooks.name must not become the skill name: {report:?}"
);
let skill = find_skill_by_name(&report.skills, "wanted").expect("load wanted");
assert!(
skill.metadata.is_empty(),
"hooks.metadata is not top-level metadata: {:?}",
skill.metadata
);
assert!(skill.content.contains("PACKAGE_BODY"));
assert!(find_skill_by_name(&report.skills, "pre-commit").is_none());
let miss = unknown_or_skipped_skill("pre-commit", &report.skips);
assert_eq!(
miss.error_kind, "unknown_skill",
"nested name must not peel a skip: {miss:?}"
);
assert!(miss.path.is_none(), "unknown must omit path: {miss:?}");
}
#[test]
fn extra_path_parentdir_is_same_named_package() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let via_parent = pkg.join("evil").join("..");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![via_parent.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"wanted/evil/.. must be the wanted package, not a collection scan: {:?}",
report
);
assert_eq!(report.skills[0].content.trim(), "PACKAGE_BODY");
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load from wanted/evil/..: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.all(|s| s.kind != SkipKind::RootFile && s.kind != SkipKind::NameDirectoryMismatch),
"wanted/evil/.. must not become root_file or name_directory_mismatch: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "wanted").is_some());
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let why = crate::why(&report, Some("wanted"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn extra_path_dot_cwd_is_same_named_package() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let report = empty_home_discover(
&pkg,
&DiscoveryOptions {
paths: vec![".".to_owned()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"extra-path `.` with cwd in the package must be that package: {:?}",
report
);
assert_eq!(report.skills[0].content.trim(), "PACKAGE_BODY");
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load from extra-path `.`: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.all(|s| s.kind != SkipKind::RootFile && s.kind != SkipKind::NameDirectoryMismatch),
"extra-path `.` must not become root_file or name_directory_mismatch: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "wanted").is_some());
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let why = crate::why(&report, Some("wanted"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn extra_path_dotdot_cwd_is_same_named_package() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let report = empty_home_discover(
&pkg.join("evil"),
&DiscoveryOptions {
paths: vec!["..".to_owned()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"extra-path `..` from a child of the package must be that package: {:?}",
report
);
assert_eq!(report.skills[0].content.trim(), "PACKAGE_BODY");
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load from extra-path `..`: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.all(|s| s.kind != SkipKind::RootFile && s.kind != SkipKind::NameDirectoryMismatch),
"extra-path `..` must not become root_file or name_directory_mismatch: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "wanted").is_some());
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let why = crate::why(&report, Some("wanted"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn extra_path_parentdir_skill_md_file_is_same_named_package() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "FILE_BODY");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let via_parent_file = pkg.join("evil").join("..").join("SKILL.md");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![via_parent_file.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"wanted/evil/../SKILL.md must load as wanted: {:?}",
report
);
assert_eq!(report.skills[0].content.trim(), "FILE_BODY");
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"file extra-path must not scan nested SKILL.md: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.all(|s| s.kind != SkipKind::NameDirectoryMismatch),
"wanted/evil/../SKILL.md must match directory wanted: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "evil").is_none());
}
#[test]
fn extra_path_nameless_parse_error_does_not_scan_nested_skill() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("SKILL.md"),
"---\ndescription: no name\n---\nPACKAGE_BODY\n",
)
.expect("write");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"nameless extra-path SKILL.md must not load nested SKILL.md: {:?}",
report
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::ParseError && s.path.ends_with("wanted/SKILL.md")),
"nameless SKILL.md must stay a parse_error package skip: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"nameless wanted/SKILL.md must not become a root_file skip: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let msg = unknown_or_skipped_skill_message("wanted", &report.skips);
assert!(
msg.contains("skipped skill: wanted"),
"nameless extra-path package must not look missing: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
let why = crate::why(&report, Some("wanted"), None, None);
assert!(why.loaded.is_empty(), "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn cursor_denylist_is_silent() {
let root = tempfile::tempdir().expect("tmp");
write_skill(
&root.path().join(".cursor").join("skills").join("shell"),
"shell",
"denied",
);
write_skill(
&root.path().join(".cursor").join("skills").join("ok-one"),
"ok-one",
"keep",
);
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
vendor_roots: vec!["cursor".to_owned()],
..DiscoveryOptions::default()
},
);
assert_eq!(report.skills.len(), 1);
assert_eq!(report.skills[0].name, "ok-one");
assert!(report.skips.is_empty());
}
#[test]
fn disabled_name_is_silent() {
let root = tempfile::tempdir().expect("tmp");
write_skill(
&root.path().join(".agents").join("skills").join("hidden"),
"hidden",
"x",
);
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
disabled: vec!["hidden".to_owned()],
..DiscoveryOptions::default()
},
);
assert!(report.skills.is_empty());
assert!(report.skips.is_empty());
}
#[test]
fn disabled_name_uses_nfkc_identity_like_why_and_load() {
let root = tempfile::tempdir().expect("tmp");
write_skill(
&root.path().join(".agents").join("skills").join("перевод"),
"перевод",
"docs",
);
write_skill(
&root.path().join(".agents").join("skills").join("café"),
"café",
"coffee",
);
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
disabled: vec!["ПЕРЕВОД".to_owned(), "cafe\u{0301}".to_owned()],
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"disabled must NFKC-fold like why/load, not == : {:?}",
report.skills
);
assert!(report.skips.is_empty());
let why = crate::why(&report, Some("ПЕРЕВОД"), None, None);
assert!(
why.loaded.is_empty(),
"why must not list a skill disabled under a folded name: {:?}",
why.loaded
);
assert!(why.unknown_skill_message().is_some());
}
#[test]
fn user_skills_dir_is_user_source() {
let cwd = tempfile::tempdir().expect("cwd");
let user = tempfile::tempdir().expect("user");
write_skill(&user.path().join("mine"), "mine", "u");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
user_skills_dir: Some(user.path().to_path_buf()),
..DiscoveryOptions::default()
},
);
assert_eq!(report.skills.len(), 1);
assert_eq!(report.skills[0].source, SkillSource::User);
}
#[test]
fn user_skills_dir_expands_tilde_like_extra_path() {
let cwd = tempfile::tempdir().expect("cwd");
let home = tempfile::tempdir().expect("home");
write_skill(
&home.path().join("myskills").join("mine"),
"mine",
"from-home",
);
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(
cwd.path(),
&DiscoveryOptions {
user_skills_dir: Some(PathBuf::from("~/myskills")),
..DiscoveryOptions::default()
},
)
});
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["mine"],
"user_dir ~/myskills must expand like extra-path ~/: {:?}",
report
);
assert_eq!(report.skills[0].source, SkillSource::User);
assert_eq!(report.skills[0].content.trim(), "from-home");
}
#[test]
fn tilde_host_tokens_do_not_load_cwd_lookalikes_without_home() {
let cwd = tempfile::tempdir().expect("cwd");
write_skill(
&cwd.path().join("myskills").join("fromcwd"),
"fromcwd",
"from-cwd",
);
write_skill(
&cwd.path().join("~").join("myskills").join("fromlitjoin"),
"fromlitjoin",
"from-lit",
);
write_skill(
&cwd.path().join("~").join("frombare"),
"frombare",
"from-bare",
);
write_skill(
&cwd.path().join("secret").join("hidden"),
"hidden",
"cwd-secret",
);
write_skill(
&cwd.path().join("~").join("secret").join("litsecret"),
"litsecret",
"lit-secret",
);
let secret = cwd.path().join("secret");
let lit_secret = cwd.path().join("~").join("secret");
let user_opts = DiscoveryOptions {
user_skills_dir: Some(PathBuf::from("~/myskills")),
implicit_roots: false,
..DiscoveryOptions::default()
};
let bare_opts = DiscoveryOptions {
user_skills_dir: Some(PathBuf::from("~")),
implicit_roots: false,
..DiscoveryOptions::default()
};
let path_opts = DiscoveryOptions {
paths: vec!["~/myskills".to_owned()],
implicit_roots: false,
..DiscoveryOptions::default()
};
let ignore_opts = DiscoveryOptions {
paths: vec![
secret.display().to_string(),
lit_secret.display().to_string(),
],
ignore: vec!["~/secret".to_owned()],
implicit_roots: false,
..DiscoveryOptions::default()
};
let assert_refused = |report: &crate::skip::DiscoveryReport, flag: &str, raw: &str| {
let names: Vec<&str> = report.skills.iter().map(|s| s.name.as_str()).collect();
assert!(
!names.contains(&"fromcwd")
&& !names.contains(&"fromlitjoin")
&& !names.contains(&"frombare"),
"tilde must not load a cwd lookalike ({flag}): {names:?} {report:?}"
);
assert!(
report.skips.iter().any(|skip| {
skip.detail.contains("HOME is unset")
&& skip.detail.contains(flag)
&& skip.path == PathBuf::from(raw)
}),
"skip must name {flag} and keep raw {raw}: {:?}",
report.skips
);
};
for (label, run) in [
("unset", RunHome::Unset),
("empty", RunHome::Empty),
("blank", RunHome::Blank),
] {
let user = run.discover(cwd.path(), &user_opts);
assert_refused(&user, "user-dir", "~/myskills");
let bare = run.discover(cwd.path(), &bare_opts);
assert_refused(&bare, "user-dir", "~");
let extra = run.discover(cwd.path(), &path_opts);
assert_refused(&extra, "--path", "~/myskills");
let ignored = run.discover(cwd.path(), &ignore_opts);
let ignored_names: Vec<&str> = ignored.skills.iter().map(|s| s.name.as_str()).collect();
assert!(
ignored_names.contains(&"hidden") && ignored_names.contains(&"litsecret"),
"{label} ignore must not hide cwd lookalikes: {ignored_names:?} {ignored:?}"
);
assert!(
ignored.skips.iter().any(|skip| {
skip.detail.contains("HOME is unset")
&& skip.detail.contains("--ignore")
&& skip.path == PathBuf::from("~/secret")
}),
"{label} ignore skip missing: {:?}",
ignored.skips
);
let why = crate::why(&ignored, Some("nosuch-skill"), None, None);
let message = why.unknown_skill_message().unwrap_or_default();
assert!(
message.contains("refused --ignore") && message.contains("HOME is unset"),
"{label} load/why must name the ignore flag: {message}"
);
let watched = run.watch(cwd.path(), &user_opts);
assert!(
watched.iter().all(|dir| !dir.ends_with("myskills")),
"{label} watch must omit the tilde user dir: {watched:?}"
);
let watched_path = run.watch(cwd.path(), &path_opts);
assert!(
watched_path.iter().all(|dir| !dir.ends_with("myskills")),
"{label} watch must omit the tilde extra path: {watched_path:?}"
);
}
let home = tempfile::tempdir().expect("home");
write_skill(
&home.path().join("myskills").join("fromhome"),
"fromhome",
"from-home",
);
write_skill(
&home.path().join("secret").join("homesecret"),
"homesecret",
"from-home-secret",
);
let loaded = with_home_override(Some(home.path().to_path_buf()), || {
discover(cwd.path(), &user_opts)
});
assert_eq!(
loaded
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["fromhome"]
);
assert!(loaded.skips.is_empty(), "{loaded:?}");
let ignored_home = with_home_override(Some(home.path().to_path_buf()), || {
discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![
"~/secret".to_owned(),
secret.display().to_string(),
lit_secret.display().to_string(),
],
ignore: vec!["~/secret".to_owned()],
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
let ignored_home_names: Vec<&str> = ignored_home
.skills
.iter()
.map(|s| s.name.as_str())
.collect();
assert!(
ignored_home_names.contains(&"hidden") && ignored_home_names.contains(&"litsecret"),
"real HOME ignore must not hide cwd packages: {ignored_home_names:?}"
);
assert!(
!ignored_home_names.contains(&"homesecret"),
"real HOME ~/secret must still ignore the home tree: {ignored_home_names:?} {ignored_home:?}"
);
assert!(
ignored_home
.skips
.iter()
.all(|skip| !skip.detail.contains("HOME is unset")),
"{ignored_home:?}"
);
}
#[derive(Clone, Copy)]
enum RunHome {
Unset,
Empty,
Blank,
}
impl RunHome {
fn discover(
self,
cwd: &std::path::Path,
opts: &DiscoveryOptions,
) -> crate::skip::DiscoveryReport {
match self {
Self::Unset => with_home_unset(|| discover(cwd, opts)),
Self::Empty => with_home_override(Some(PathBuf::new()), || discover(cwd, opts)),
Self::Blank => with_home_override(Some(PathBuf::from(" ")), || discover(cwd, opts)),
}
}
fn watch(self, cwd: &std::path::Path, opts: &DiscoveryOptions) -> Vec<PathBuf> {
match self {
Self::Unset => with_home_unset(|| watch_dirs(cwd, opts)),
Self::Empty => with_home_override(Some(PathBuf::new()), || watch_dirs(cwd, opts)),
Self::Blank => with_home_override(Some(PathBuf::from(" ")), || watch_dirs(cwd, opts)),
}
}
}
#[test]
fn find_skill_by_name_is_case_insensitive() {
let mut skill = crate::skill::Skill::new("Demo", "d", "");
skill.name = "Demo".to_owned();
let skills = vec![skill];
assert!(find_skill_by_name(&skills, "demo").is_some());
assert!(find_skill_by_name(&skills, "").is_none());
}
#[test]
fn extra_path_unicode_package_why_matches_folded_name() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("перевод"), "перевод", "docs");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["перевод"],
"extra-path Unicode dir must load: {:?}",
report
);
assert!(find_skill_by_name(&report.skills, "ПЕРЕВОД").is_some());
let why = crate::why(&report, Some("ПЕРЕВОД"), None, None);
assert_eq!(
why.loaded.len(),
1,
"why must find the Unicode package under the folded query: {:?}",
why
);
assert_eq!(why.loaded[0].name, "перевод");
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn extra_path_unicode_loads_by_default_and_skips_when_ascii_names() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("café"), "café", "coffee");
let on = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
on.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["café"]
);
assert!(on.skips.is_empty());
let off = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
ascii_names: true,
..DiscoveryOptions::default()
},
);
assert!(
off.skills.is_empty(),
"ascii_names must not load café: {:?}",
off.skills
);
assert!(
off.skips.iter().any(|s| {
s.kind == SkipKind::ParseError
&& s.name.as_deref() == Some("café")
&& s.detail.contains("lowercase alphanumeric and hyphens only")
&& s.detail.contains("--ascii-names")
&& s.detail.contains("ascii_names")
&& !s.detail.contains("invalid YAML")
}),
"ascii_names skip must name --ascii-names / ascii_names, not claim invalid YAML: {:?}",
off.skips
);
let ascii_ok = tempfile::tempdir().expect("ascii");
write_skill(&ascii_ok.path().join("ok-name"), "ok-name", "ascii");
let keep = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![ascii_ok.path().display().to_string()],
ascii_names: true,
..DiscoveryOptions::default()
},
);
assert_eq!(
keep.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["ok-name"]
);
}
#[test]
fn walk_cwd_to_git_root_stops_at_nested_git() {
let outer = tempfile::tempdir().expect("outer");
fs::create_dir_all(outer.path().join(".git")).expect("outer git");
let inner = outer.path().join("inner");
fs::create_dir_all(inner.join(".git")).expect("inner git");
let walked = walk_cwd_to_git_root(&inner);
assert_eq!(walked, vec![inner.clone()]);
let no_git = tempfile::tempdir().expect("plain");
let nested = no_git.path().join("a").join("b");
fs::create_dir_all(&nested).expect("mkdir");
assert_eq!(walk_cwd_to_git_root(&nested), vec![nested]);
}
#[test]
fn walk_without_git_keeps_the_project_that_holds_agents() {
let proj = tempfile::tempdir().expect("proj");
let pkg = proj.path().join(".agents").join("skills").join("notes");
fs::create_dir_all(&pkg).expect("mkdir");
let walked = walk_cwd_to_git_root(&pkg);
assert!(
walked.iter().any(|p| p == proj.path()),
"inside .agents without git must see the project: {walked:?}"
);
assert!(
walked.iter().all(|p| p.starts_with(proj.path())),
"must not climb past that project: {walked:?}"
);
let claude = proj.path().join(".claude").join("skills").join("note");
fs::create_dir_all(&claude).expect("claude");
let from_claude = walk_cwd_to_git_root(&claude);
assert!(
from_claude.iter().any(|p| p == proj.path()),
"inside .claude without git must see the project: {from_claude:?}"
);
assert!(
from_claude.iter().all(|p| p.starts_with(proj.path())),
"vendor walk must not climb past that project: {from_claude:?}"
);
let src = proj.path().join("src");
fs::create_dir_all(&src).expect("src");
assert_eq!(
walk_cwd_to_git_root(&src),
vec![src.clone()],
"a sibling folder must not climb to an unrelated parent"
);
write_skill(&pkg, "notes", "body");
let found = empty_home_discover(&pkg, &DiscoveryOptions::default());
assert!(
found.skills.iter().any(|s| s.name == "notes"),
"list from the skill dir must load notes: {:?}",
found.skills
);
let missed = empty_home_discover(&src, &DiscoveryOptions::default());
assert!(
missed.skills.iter().all(|s| s.name != "notes"),
"src must not see the project skill: {:?}",
missed.skills
);
}
#[test]
fn watch_dirs_lists_cwd_vendor_user_and_extra() {
let root = tempfile::tempdir().expect("root");
fs::create_dir_all(root.path().join(".git")).expect("git");
let agents = root.path().join(".agents").join("skills");
let bline = root.path().join(".bline").join("skills");
fs::create_dir_all(&agents).expect("agents");
fs::create_dir_all(&bline).expect("bline");
let user = tempfile::tempdir().expect("user");
let pkg = tempfile::tempdir().expect("pkg");
write_skill(&pkg.path().join("wanted"), "wanted", "pkg");
let collection = tempfile::tempdir().expect("col");
write_skill(&collection.path().join("skills").join("hint"), "hint", "c");
let same = |dirs: &[PathBuf], want: &std::path::Path| {
dirs.iter().any(|p| {
p == want
|| p.canonicalize()
.ok()
.zip(want.canonicalize().ok())
.is_some_and(|(a, b)| a == b)
})
};
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
root.path(),
&DiscoveryOptions {
vendor_roots: vec!["bline".to_owned()],
user_skills_dir: Some(user.path().to_path_buf()),
paths: vec![
pkg.path().join("wanted").display().to_string(),
collection.path().display().to_string(),
],
..DiscoveryOptions::default()
},
)
});
assert!(same(&dirs, &agents), "cwd .agents/skills: {dirs:?}");
assert!(same(&dirs, &bline), "vendor .bline/skills: {dirs:?}");
assert!(
!dirs.iter().any(|p| p.ends_with(".claude/skills")),
"disabled vendor must not appear: {dirs:?}"
);
assert!(same(&dirs, user.path()), "user dir: {dirs:?}");
assert!(
same(&dirs, &pkg.path().join("wanted")),
"extra package: {dirs:?}"
);
assert!(
same(&dirs, collection.path()),
"extra collection root: {dirs:?}"
);
assert!(
same(&dirs, &collection.path().join("skills")),
"extra collection skills/: {dirs:?}"
);
let empty_user = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
root.path(),
&DiscoveryOptions {
user_skills_dir: Some(PathBuf::from(" ")),
..DiscoveryOptions::default()
},
)
});
assert!(
empty_user.iter().all(|p| p != root.path()),
"empty user_dir must not become cwd: {empty_user:?}"
);
assert!(
!empty_user
.iter()
.any(|p| p.file_name().and_then(|n| n.to_str()) == Some(" ")),
"empty user_dir omitted: {empty_user:?}"
);
}
fn watch_paths_contain(dirs: &[PathBuf], want: &std::path::Path) -> bool {
dirs.iter().any(|p| {
p == want
|| p.canonicalize()
.ok()
.zip(want.canonicalize().ok())
.is_some_and(|(a, b)| a == b)
})
}
#[test]
fn watch_dirs_omits_skills_subdir_on_named_extra_path_package() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("skills").join("evil"), "evil", "NESTED_SECRET");
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, &pkg),
"named extra-path package must be watched: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &pkg.join("skills")),
"named extra-path package must not watch nested skills/: {dirs:?}"
);
}
#[test]
fn watch_dirs_lists_skills_subdir_for_extra_path_collection() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
fs::write(
extra.path().join("SKILL.md"),
"---\nname: loose\ndescription: leftover\n---\nloose\n",
)
.expect("write");
write_skill(&extra.path().join("skills").join("public"), "public", "ok");
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"collection extra-path root must be watched: {dirs:?}"
);
assert!(
watch_paths_contain(&dirs, &extra.path().join("skills")),
"collection extra/skills must be watched: {dirs:?}"
);
}
#[cfg(unix)]
#[test]
fn watch_dirs_omits_escaped_extra_path_skills_subdir() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let outside = tempfile::tempdir().expect("out");
fs::create_dir_all(outside.path().join("stolen")).expect("mkdir");
fs::write(
outside.path().join("stolen").join("SKILL.md"),
"---\nname: stolen\ndescription: leaked\n---\nSECRET_BODY\n",
)
.expect("write");
std::os::unix::fs::symlink(outside.path(), extra.path().join("skills")).expect("symlink");
write_skill(&extra.path().join("public"), "public", "ok");
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"escaped extra/skills must still watch extra/: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &extra.path().join("skills")),
"escaped extra/skills must not be a watch root: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, outside.path()),
"watch_dirs must not list the escaped skills target: {dirs:?}"
);
}
#[cfg(unix)]
#[test]
fn watch_dirs_omits_escaped_project_and_home_skills_roots() {
let cwd = tempfile::tempdir().expect("cwd");
let outside = tempfile::tempdir().expect("out");
write_skill(&outside.path().join("stolen"), "stolen", "SECRET_BODY");
let agents = cwd.path().join(".agents");
fs::create_dir_all(&agents).expect("mkdir .agents");
let agents_skills = agents.join("skills");
std::os::unix::fs::symlink(outside.path(), &agents_skills).expect("symlink agents");
let claude = cwd.path().join(".claude");
fs::create_dir_all(&claude).expect("mkdir .claude");
let claude_skills = claude.join("skills");
std::os::unix::fs::symlink(outside.path(), &claude_skills).expect("symlink vendor");
let home = tempfile::tempdir().expect("home");
let home_agents = home.path().join(".agents");
fs::create_dir_all(&home_agents).expect("mkdir HOME/.agents");
let home_skills = home_agents.join("skills");
std::os::unix::fs::symlink(outside.path(), &home_skills).expect("symlink home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
vendor_roots: vec!["claude".to_owned()],
..DiscoveryOptions::default()
},
)
});
assert!(
!watch_paths_contain(&dirs, &agents_skills),
"watch_dirs must not list escaped .agents/skills (discover does not walk it): {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &claude_skills),
"watch_dirs must not list escaped .claude/skills: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &home_skills),
"watch_dirs must not list escaped HOME/.agents/skills: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, outside.path()),
"watch_dirs must not list the escaped skills target: {dirs:?}"
);
}
#[cfg(unix)]
#[test]
fn watch_dirs_omits_unreadable_extra_path_skills_subdir() {
use std::os::unix::fs::PermissionsExt;
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let skills = extra.path().join("skills");
fs::create_dir_all(skills.join("hidden")).expect("mkdir");
write_skill(&skills.join("hidden"), "hidden", "locked");
write_skill(&extra.path().join("public"), "public", "ok");
let original = fs::metadata(&skills).expect("meta").permissions();
struct Restore<'a>(&'a std::path::Path, fs::Permissions);
impl Drop for Restore<'_> {
fn drop(&mut self) {
let _ = fs::set_permissions(self.0, self.1.clone());
}
}
let _restore = Restore(&skills, original.clone());
let mut locked = original.clone();
locked.set_mode(0o000);
fs::set_permissions(&skills, locked).expect("chmod");
if fs::read_dir(&skills).is_ok() {
return;
}
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"unreadable extra/skills must still watch extra/: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &skills),
"unreadable extra/skills must not be a watch root: {dirs:?}"
);
}
#[cfg(unix)]
#[test]
fn unreadable_skills_dir_is_skip_not_silent() {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().expect("tmp");
let skills = root.path().join(".agents").join("skills");
fs::create_dir_all(&skills).expect("mkdir");
write_skill(&skills.join("hidden"), "hidden", "x");
let original = fs::metadata(&skills).expect("meta").permissions();
struct Restore<'a>(&'a std::path::Path, fs::Permissions);
impl Drop for Restore<'_> {
fn drop(&mut self) {
let _ = fs::set_permissions(self.0, self.1.clone());
}
}
let _restore = Restore(&skills, original.clone());
let mut locked = original.clone();
locked.set_mode(0o000);
fs::set_permissions(&skills, locked).expect("chmod");
if fs::read_dir(&skills).is_ok() {
return;
}
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(
report.skips.iter().any(|s| s.kind == SkipKind::Unreadable),
"unreadable skills dir must be a skip row, not silent: {:?}",
report.skips
);
assert!(report.skills.iter().all(|s| s.name != "hidden"));
}
#[cfg(unix)]
#[test]
fn unreadable_child_package_dir_is_skip_not_silent() {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().expect("tmp");
let skills = root.path().join(".agents").join("skills");
write_skill(&skills.join("visible"), "visible", "shown");
let hidden = skills.join("hidden");
write_skill(&hidden, "hidden", "locked");
let original = fs::metadata(&hidden).expect("meta").permissions();
struct Restore<'a>(&'a std::path::Path, fs::Permissions);
impl Drop for Restore<'_> {
fn drop(&mut self) {
let _ = fs::set_permissions(self.0, self.1.clone());
}
}
let _restore = Restore(&hidden, original.clone());
let mut locked = original.clone();
locked.set_mode(0o000);
fs::set_permissions(&hidden, locked).expect("chmod");
if fs::symlink_metadata(hidden.join("SKILL.md")).is_ok() {
return;
}
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(
find_skill_by_name(&report.skills, "visible").is_some(),
"readable sibling must stay: {:?}",
report.skills
);
assert!(
find_skill_by_name(&report.skills, "hidden").is_none(),
"locked package must not load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("hidden")
&& s.detail.contains("Permission denied")
}),
"locked package directory must be a skip: {:?}",
report.skips
);
let extra = tempfile::tempdir().expect("extra");
let only = extra.path().join("skills").join("hidden");
write_skill(&only, "hidden", "locked");
let original_only = fs::metadata(&only).expect("meta").permissions();
let _restore_only = Restore(&only, original_only.clone());
let mut locked_only = original_only;
locked_only.set_mode(0o000);
fs::set_permissions(&only, locked_only).expect("chmod");
if fs::symlink_metadata(only.join("SKILL.md")).is_ok() {
return;
}
let cwd = tempfile::tempdir().expect("cwd");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(
find_skill_by_name(&report.skills, "hidden").is_none(),
"locked extra child must not load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("hidden")
&& s.detail.contains("Permission denied")
}),
"extra root must skip the locked child package: {:?}",
report.skips
);
}
#[cfg(unix)]
#[test]
fn package_path_with_newline_component_is_unreadable_not_loaded() {
let cwd = tempfile::tempdir().expect("cwd");
let home = tempfile::tempdir().expect("home");
let parent = tempfile::tempdir().expect("parent");
let extra = parent.path().join("evil\nroot");
fs::create_dir_all(extra.join("demo")).expect("mkdir");
write_skill(&extra.join("demo"), "demo", "SECRET_BODY");
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
assert!(
report.skills.iter().all(|s| s.name != "demo"),
"must not load a package under a newline path component: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("SECRET_BODY")),
"body must not load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("SKILL.md")
&& (s.detail.contains("path")
|| s.detail.contains("control")
|| s.detail.contains("line"))
}),
"newline path component must be unreadable with a path reason: skills={:?} skips={:?}",
report.skills,
report.skips
);
let msg = unknown_or_skipped_skill_message("demo", &report.skips);
assert_eq!(msg.lines().count(), 1, "miss must stay one line: {msg:?}");
assert!(
!msg.contains('\n') && !msg.contains('\u{2028}'),
"miss must not echo raw separators: {msg:?}"
);
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
assert!(
dirs.iter()
.all(|p| !path_has_line_separator(p) && !p.display().to_string().contains('\n')),
"watch_dirs must not list a newline path: {dirs:?}"
);
}
#[cfg(unix)]
#[test]
fn user_dir_path_with_cr_component_is_unreadable_not_loaded() {
let cwd = tempfile::tempdir().expect("cwd");
let home = tempfile::tempdir().expect("home");
let parent = tempfile::tempdir().expect("parent");
let user = parent.path().join("evil\rroot");
fs::create_dir_all(user.join("demo")).expect("mkdir");
write_skill(&user.join("demo"), "demo", "SECRET_BODY");
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(
cwd.path(),
&DiscoveryOptions {
user_skills_dir: Some(user.clone()),
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
assert!(
report.skills.iter().all(|s| s.name != "demo"),
"must not load a package under a CR user_dir component: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("SECRET_BODY")),
"body must not load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("SKILL.md")
&& (s.detail.contains("path")
|| s.detail.contains("control")
|| s.detail.contains("line"))
}),
"CR user_dir must be unreadable with a path reason: skills={:?} skips={:?}",
report.skills,
report.skips
);
let msg = unknown_or_skipped_skill_message("demo", &report.skips);
assert_eq!(msg.lines().count(), 1, "miss must stay one line: {msg:?}");
assert!(
!msg.contains('\r') && !msg.contains('\n') && !msg.contains('\u{2028}'),
"miss must not echo raw separators: {msg:?}"
);
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
user_skills_dir: Some(user.clone()),
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
assert!(
dirs.iter()
.all(|p| !path_has_line_separator(p) && !p.display().to_string().contains('\r')),
"watch_dirs must not list a CR user_dir: {dirs:?}"
);
}
#[cfg(unix)]
#[test]
fn symlink_escape_is_unreadable() {
let root = tempfile::tempdir().expect("tmp");
let outside = tempfile::tempdir().expect("out");
fs::write(outside.path().join("SKILL.md"), "secret\n").expect("write");
let pkg = root.path().join(".agents").join("skills").join("escape");
fs::create_dir_all(&pkg).expect("mkdir");
std::os::unix::fs::symlink(outside.path().join("SKILL.md"), pkg.join("SKILL.md"))
.expect("symlink");
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(report.skills.is_empty());
assert_eq!(report.skips.len(), 1);
assert_eq!(report.skips[0].kind, SkipKind::Unreadable);
assert!(report.skips[0].detail.contains("escapes"));
assert!(report.skips[0].name.is_none());
let msg = unknown_or_skipped_skill_message("escape", &report.skips);
assert!(
msg.contains("skipped skill: escape"),
"symlink-escape skip must name the package, not unknown: {msg}"
);
assert!(msg.contains("unreadable"), "msg={msg}");
assert!(!msg.contains("unknown skill"), "msg={msg}");
}
fn corpus_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/corpus")
}
#[test]
fn corpus_minimal_valid_discovers_as_extra_path() {
let cwd = tempfile::tempdir().expect("cwd");
let pkg = corpus_dir().join("agentskills/minimal-valid");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(report.skills.len(), 1);
assert_eq!(report.skills[0].name, "minimal-valid");
assert_eq!(report.skills[0].source, SkillSource::ExtraPath);
assert!(report.skips.is_empty());
}
#[test]
fn corpus_name_mismatch_is_skipped() {
let cwd = tempfile::tempdir().expect("cwd");
let parent = corpus_dir().join("agentskills/name-mismatch");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![parent.display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(report.skills.is_empty());
assert_eq!(report.skips.len(), 1);
assert_eq!(report.skips[0].kind, SkipKind::NameDirectoryMismatch);
assert_eq!(report.skips[0].name.as_deref(), Some("good-name"));
}
#[test]
fn corpus_invalid_name_dir_is_parse_skip() {
let cwd = tempfile::tempdir().expect("cwd");
let parent = corpus_dir().join("agentskills/invalid-name");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![parent.display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(report.skills.is_empty());
assert_eq!(report.skips.len(), 1);
assert_eq!(report.skips[0].kind, SkipKind::ParseError);
assert_eq!(report.skips[0].name.as_deref(), Some("Bad_Name"));
let why = crate::why(&report, Some("Bad_Name"), None, None);
assert_eq!(
why.skips.len(),
1,
"why JSON must find the parse skip by name"
);
assert_eq!(why.skips[0].name.as_deref(), Some("Bad_Name"));
}
#[test]
fn corpus_collision_first_wins() {
let cwd = tempfile::tempdir().expect("cwd");
let a = corpus_dir().join("collision/a");
let b = corpus_dir().join("collision/b");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![a.display().to_string(), b.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(report.skills.len(), 1);
assert_eq!(report.skills[0].name, "foo");
assert!(report.skills[0].content.contains("First path wins"));
assert_eq!(report.skips.len(), 1);
assert_eq!(report.skips[0].kind, SkipKind::NameCollision);
assert!(report.skips[0].winner_path.is_some());
}
#[test]
fn corpus_root_file_in_agents_tree() {
let cwd = tempfile::tempdir().expect("cwd");
let dest = cwd.path().join(".agents").join("skills");
fs::create_dir_all(&dest).expect("mkdir");
fs::copy(
corpus_dir().join("root-file/SKILL.md"),
dest.join("SKILL.md"),
)
.expect("copy");
let report = empty_home_discover(cwd.path(), &DiscoveryOptions::default());
assert!(report.skills.is_empty());
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::RootFile && s.name.as_deref() == Some("loose") }),
"skips={:?}",
report.skips
);
}
#[test]
fn lowercase_skill_md_in_agents_package_loads() {
let cwd = corpus_dir().join("lowercase-skill-md");
let report = empty_home_discover(cwd.as_path(), &DiscoveryOptions::default());
let skill = report
.skills
.iter()
.find(|s| s.name == "lc-pack")
.expect("lc-pack");
assert_eq!(skill.source, SkillSource::Agents);
}
#[test]
fn incumbent_claude_vendor_layout_loads() {
let cwd = corpus_dir().join("incumbent/claude-project");
let off = empty_home_discover(cwd.as_path(), &DiscoveryOptions::default());
assert!(
off.skills.iter().all(|s| s.name != "pdf-helper"),
"claude vendor is opt-in"
);
let on = empty_home_discover(
cwd.as_path(),
&DiscoveryOptions {
vendor_roots: vec!["claude".to_owned()],
..DiscoveryOptions::default()
},
);
let skill = on
.skills
.iter()
.find(|s| s.name == "pdf-helper")
.expect("pdf-helper");
assert_eq!(
skill.source,
SkillSource::Vendor {
name: "claude".to_owned()
}
);
}
#[test]
fn incumbent_cursor_vendor_layout_loads() {
let cwd = corpus_dir().join("incumbent/cursor-project");
let off = empty_home_discover(cwd.as_path(), &DiscoveryOptions::default());
assert!(
off.skills.iter().all(|s| s.name != "create-rule"),
"cursor vendor is opt-in"
);
let on = empty_home_discover(
cwd.as_path(),
&DiscoveryOptions {
vendor_roots: vec!["cursor".to_owned()],
..DiscoveryOptions::default()
},
);
let skill = on
.skills
.iter()
.find(|s| s.name == "create-rule")
.expect("create-rule");
assert_eq!(
skill.source,
SkillSource::Vendor {
name: "cursor".to_owned()
}
);
let why = crate::why(&on, Some("create-rule"), None, None);
assert_eq!(why.loaded.len(), 1, "why={why:?}");
assert_eq!(why.loaded[0].name, "create-rule");
assert_eq!(
why.loaded[0].source,
SkillSource::Vendor {
name: "cursor".to_owned()
}
);
let want = cwd
.join(".cursor")
.join("skills")
.join("create-rule")
.join("SKILL.md");
let got = why.loaded[0].path.as_deref().expect("why path");
let same = got == want.as_path()
|| got
.canonicalize()
.ok()
.zip(want.canonicalize().ok())
.is_some_and(|(a, b)| a == b);
assert!(
same,
"why path must be the fixture SKILL.md: got={got:?} want={want:?}"
);
assert!(
why.skips.is_empty(),
"create-rule is loaded, not a skip: {:?}",
why.skips
);
let off_why = crate::why(&off, Some("create-rule"), None, None);
assert!(off_why.loaded.is_empty(), "off why={off_why:?}");
assert!(off_why.skips.is_empty(), "off skips={:?}", off_why.skips);
assert!(
off_why.unknown_skill_message().is_some(),
"why without vendor cursor must treat create-rule as unknown"
);
}
#[test]
fn incumbent_grok_vendor_layout_loads() {
let cwd = corpus_dir().join("incumbent/grok-project");
let off = empty_home_discover(cwd.as_path(), &DiscoveryOptions::default());
assert!(
off.skills.iter().all(|s| s.name != "project-grok"),
"grok vendor is opt-in"
);
let on = empty_home_discover(
cwd.as_path(),
&DiscoveryOptions {
vendor_roots: vec!["grok".to_owned()],
..DiscoveryOptions::default()
},
);
let skill = on
.skills
.iter()
.find(|s| s.name == "project-grok")
.expect("project-grok");
assert_eq!(
skill.source,
SkillSource::Vendor {
name: "grok".to_owned()
}
);
}
#[test]
fn incumbent_bline_vendor_layout_loads_project_bline() {
let cwd = corpus_dir().join("incumbent/bline-project");
let off = empty_home_discover(cwd.as_path(), &DiscoveryOptions::default());
assert!(
off.skills.iter().all(|s| s.name != "project-bline"),
"bline vendor is opt-in"
);
let on = empty_home_discover(
cwd.as_path(),
&DiscoveryOptions {
vendor_roots: vec!["bline".to_owned()],
..DiscoveryOptions::default()
},
);
let skill = on
.skills
.iter()
.find(|s| s.name == "project-bline")
.expect("project-bline");
assert_eq!(
skill.source,
SkillSource::Vendor {
name: "bline".to_owned()
}
);
let why = crate::why(&on, Some("project-bline"), None, None);
assert_eq!(why.loaded.len(), 1, "why={why:?}");
assert_eq!(why.loaded[0].name, "project-bline");
assert_eq!(
why.loaded[0].source,
SkillSource::Vendor {
name: "bline".to_owned()
}
);
let want = cwd
.join(".bline")
.join("skills")
.join("project-bline")
.join("SKILL.md");
let got = why.loaded[0].path.as_deref().expect("why path");
let same = got == want.as_path()
|| got
.canonicalize()
.ok()
.zip(want.canonicalize().ok())
.is_some_and(|(a, b)| a == b);
assert!(
same,
"why path must be the fixture SKILL.md: got={got:?} want={want:?}"
);
assert!(
why.skips.is_empty(),
"project-bline is loaded, not a skip: {:?}",
why.skips
);
let off_why = crate::why(&off, Some("project-bline"), None, None);
assert!(off_why.loaded.is_empty(), "off why={off_why:?}");
assert!(off_why.skips.is_empty(), "off skips={:?}", off_why.skips);
assert!(
off_why.unknown_skill_message().is_some(),
"why without vendor bline must treat project-bline as unknown"
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.as_path(),
&DiscoveryOptions {
vendor_roots: vec!["bline".to_owned()],
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, &cwd.join(".bline").join("skills")),
"watch_dirs must list cwd/.bline/skills when vendor bline is on: {dirs:?}"
);
}
#[test]
fn incumbent_claude_user_home_vendor_layout_loads() {
let cwd = tempfile::tempdir().expect("cwd");
let home = corpus_dir().join("incumbent/claude-user");
let off = with_home_override(Some(home.clone()), || {
discover(cwd.path(), &DiscoveryOptions::default())
});
assert!(
off.skills.iter().all(|s| s.name != "home-note"),
"claude vendor is opt-in at HOME too"
);
let on = with_home_override(Some(home.clone()), || {
discover(
cwd.path(),
&DiscoveryOptions {
vendor_roots: vec!["claude".to_owned()],
..DiscoveryOptions::default()
},
)
});
let skill = on
.skills
.iter()
.find(|s| s.name == "home-note")
.expect("home-note");
assert_eq!(
skill.source,
SkillSource::Vendor {
name: "claude".to_owned()
}
);
let why = crate::why(&on, Some("home-note"), None, None);
assert_eq!(why.loaded.len(), 1, "why={why:?}");
assert_eq!(why.loaded[0].name, "home-note");
assert_eq!(
why.loaded[0].source,
SkillSource::Vendor {
name: "claude".to_owned()
}
);
let want = home
.join(".claude")
.join("skills")
.join("home-note")
.join("SKILL.md");
let got = why.loaded[0].path.as_deref().expect("why path");
let same = got == want.as_path()
|| got
.canonicalize()
.ok()
.zip(want.canonicalize().ok())
.is_some_and(|(a, b)| a == b);
assert!(
same,
"why path must be the fixture SKILL.md: got={got:?} want={want:?}"
);
assert!(
why.skips.is_empty(),
"home-note is loaded, not a skip: {:?}",
why.skips
);
let off_why = crate::why(&off, Some("home-note"), None, None);
assert!(off_why.loaded.is_empty(), "off why={off_why:?}");
assert!(off_why.skips.is_empty(), "off skips={:?}", off_why.skips);
assert!(
off_why.unknown_skill_message().is_some(),
"why without vendor claude must treat home-note as unknown"
);
let dirs = with_home_override(Some(home.clone()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
vendor_roots: vec!["claude".to_owned()],
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, &home.join(".claude").join("skills")),
"watch_dirs must list HOME/.claude/skills when vendor claude is on: {dirs:?}"
);
}
#[test]
fn incumbent_cursor_user_home_vendor_layout_loads() {
let cwd = tempfile::tempdir().expect("cwd");
let home = corpus_dir().join("incumbent/cursor-user");
let off = with_home_override(Some(home.clone()), || {
discover(cwd.path(), &DiscoveryOptions::default())
});
assert!(
off.skills.iter().all(|s| s.name != "home-rule"),
"cursor vendor is opt-in at HOME too"
);
let on = with_home_override(Some(home.clone()), || {
discover(
cwd.path(),
&DiscoveryOptions {
vendor_roots: vec!["cursor".to_owned()],
..DiscoveryOptions::default()
},
)
});
let skill = on
.skills
.iter()
.find(|s| s.name == "home-rule")
.expect("home-rule");
assert_eq!(
skill.source,
SkillSource::Vendor {
name: "cursor".to_owned()
}
);
let dirs = with_home_override(Some(home.clone()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
vendor_roots: vec!["cursor".to_owned()],
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, &home.join(".cursor").join("skills")),
"watch_dirs must list HOME/.cursor/skills when vendor cursor is on: {dirs:?}"
);
}
#[test]
fn incumbent_grok_user_home_vendor_layout_loads() {
let cwd = tempfile::tempdir().expect("cwd");
let home = corpus_dir().join("incumbent/grok-user");
let off = with_home_override(Some(home.clone()), || {
discover(cwd.path(), &DiscoveryOptions::default())
});
assert!(
off.skills.iter().all(|s| s.name != "home-grok"),
"grok vendor is opt-in at HOME too"
);
let on = with_home_override(Some(home.clone()), || {
discover(
cwd.path(),
&DiscoveryOptions {
vendor_roots: vec!["grok".to_owned()],
..DiscoveryOptions::default()
},
)
});
let skill = on
.skills
.iter()
.find(|s| s.name == "home-grok")
.expect("home-grok");
assert_eq!(
skill.source,
SkillSource::Vendor {
name: "grok".to_owned()
}
);
let dirs = with_home_override(Some(home.clone()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
vendor_roots: vec!["grok".to_owned()],
..DiscoveryOptions::default()
},
)
});
assert!(
watch_paths_contain(&dirs, &home.join(".grok").join("skills")),
"watch_dirs must list HOME/.grok/skills when vendor grok is on: {dirs:?}"
);
}
#[test]
fn incumbent_vercel_skills_dir_as_extra_path() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = corpus_dir().join("incumbent/vercel-npx");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
..DiscoveryOptions::default()
},
);
let skill = report
.skills
.iter()
.find(|s| s.name == "deploy-hint")
.expect("deploy-hint");
assert_eq!(skill.source, SkillSource::ExtraPath);
}
#[test]
fn incumbent_vercel_skills_collection_as_extra_path() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = corpus_dir().join("incumbent/vercel-npx/skills");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
..DiscoveryOptions::default()
},
);
let skill = report
.skills
.iter()
.find(|s| s.name == "deploy-hint")
.expect("deploy-hint");
assert_eq!(skill.source, SkillSource::ExtraPath);
}
#[test]
fn extra_path_file_loads() {
let cwd = tempfile::tempdir().expect("cwd");
let path = cwd.path().join("one").join("SKILL.md");
fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
fs::write(
&path,
"---\nname: one\ndescription: extra file\n---\nbody\n",
)
.expect("write");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![path.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(report.skills.len(), 1);
assert_eq!(report.skills[0].source, SkillSource::ExtraPath);
assert_eq!(report.skills[0].name, "one");
let opts = DiscoveryOptions {
paths: vec![path.display().to_string()],
..DiscoveryOptions::default()
};
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, &path),
"watch_dirs must still list a regular extra-path SKILL.md file: {dirs:?}"
);
}
fn env_home() -> Option<PathBuf> {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
}
#[test]
fn home_override_clears_after_panic() {
let home = tempfile::tempdir().expect("home");
let leaked = home.path().to_path_buf();
write_skill(
&leaked.join(".agents").join("skills").join("leaked"),
"leaked",
"x",
);
let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
with_home_override(Some(leaked.clone()), || {
panic!("boom");
});
}));
assert!(panicked.is_err());
assert_eq!(
super::home_dir(),
env_home(),
"HOME override must not leak after panic"
);
let cwd = tempfile::tempdir().expect("cwd");
let report = discover(cwd.path(), &DiscoveryOptions::default());
assert!(
report.skills.iter().all(|s| s.name != "leaked"),
"leaked home skill visible after panic: {:?}",
report.skills
);
}
#[test]
fn home_override_restores_outer_after_nested() {
let outer = tempfile::tempdir().expect("outer");
let inner = tempfile::tempdir().expect("inner");
write_skill(
&outer.path().join(".agents").join("skills").join("outer"),
"outer",
"x",
);
write_skill(
&inner.path().join(".agents").join("skills").join("inner"),
"inner",
"x",
);
let cwd = tempfile::tempdir().expect("cwd");
with_home_override(Some(outer.path().to_path_buf()), || {
with_home_override(Some(inner.path().to_path_buf()), || {
let report = discover(cwd.path(), &DiscoveryOptions::default());
assert_eq!(report.skills.len(), 1);
assert_eq!(report.skills[0].name, "inner");
});
assert_eq!(
super::home_dir().as_deref(),
Some(outer.path()),
"nested override must restore outer home"
);
let report = discover(cwd.path(), &DiscoveryOptions::default());
assert_eq!(report.skills.len(), 1, "skills={:?}", report.skills);
assert_eq!(report.skills[0].name, "outer");
});
assert_eq!(super::home_dir(), env_home());
}
#[cfg(unix)]
#[test]
fn ignore_prefix_matches_canonical_walked_path() {
let root = tempfile::tempdir().expect("tmp");
write_skill(
&root.path().join(".agents").join("skills").join("secret"),
"secret",
"x",
);
write_skill(
&root.path().join(".agents").join("skills").join("public"),
"public",
"x",
);
let links = tempfile::tempdir().expect("links");
let cwd_link = links.path().join("cwd-link");
std::os::unix::fs::symlink(root.path(), &cwd_link).expect("symlink cwd");
let ignore = cwd_link.join(".agents").join("skills").join("secret");
let report = empty_home_discover(
&cwd_link,
&DiscoveryOptions {
ignore: vec![ignore.display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(
report.skills.iter().all(|s| s.name != "secret"),
"ignore prefix must skip the skill after cwd canonicalize: {:?}",
report.skills
);
assert_eq!(report.skills.len(), 1, "skills={:?}", report.skills);
assert_eq!(report.skills[0].name, "public");
assert!(
report.skips.iter().all(|s| {
s.name.as_deref() != Some("secret") && !s.path.ends_with("secret/SKILL.md")
}),
"ignore is silent (no Ignored skip row): {:?}",
report.skips
);
}
fn project_with_secret_and_public() -> tempfile::TempDir {
let root = tempfile::tempdir().expect("tmp");
write_skill(
&root.path().join(".agents").join("skills").join("secret"),
"secret",
"x",
);
write_skill(
&root.path().join(".agents").join("skills").join("public"),
"public",
"x",
);
root
}
fn assert_secret_ignored(report: &crate::skip::DiscoveryReport) {
assert!(
report.skills.iter().all(|s| s.name != "secret"),
"secret skill must be ignored, skills={:?}",
report.skills
);
assert_eq!(report.skills.len(), 1, "skills={:?}", report.skills);
assert_eq!(report.skills[0].name, "public");
assert!(
report.skips.iter().all(|s| {
s.name.as_deref() != Some("secret") && !s.path.ends_with("secret/SKILL.md")
}),
"ignore is silent (no Ignored skip row): {:?}",
report.skips
);
}
#[test]
fn ignore_relative_prefix_matches_absolute_walked_path() {
let root = project_with_secret_and_public();
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
ignore: vec![".agents/skills/secret".to_owned()],
..DiscoveryOptions::default()
},
);
assert_secret_ignored(&report);
}
fn write_sized_skill(dir: &std::path::Path, name: &str, total_bytes: usize) {
fs::create_dir_all(dir).expect("mkdir");
let header = format!("---\nname: {name}\ndescription: {name} skill\n---\n");
assert!(
total_bytes >= header.len(),
"total_bytes={} smaller than header {}",
total_bytes,
header.len()
);
let mut body = header;
body.extend(std::iter::repeat_n('x', total_bytes - body.len()));
fs::write(dir.join("SKILL.md"), body).expect("write");
}
#[test]
fn oversized_skill_md_is_unreadable_skip() {
let root = tempfile::tempdir().expect("tmp");
write_sized_skill(
&root.path().join(".agents").join("skills").join("huge"),
"huge",
SKILL_MD_MAX_BYTES as usize + 1,
);
write_skill(
&root.path().join(".agents").join("skills").join("ok-one"),
"ok-one",
"keep",
);
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(
report.skills.iter().all(|s| s.name != "huge"),
"oversized SKILL.md must not load: {:?}",
report.skills
);
assert_eq!(report.skills.len(), 1, "skills={:?}", report.skills);
assert_eq!(report.skills[0].name, "ok-one");
let skip = report
.skips
.iter()
.find(|s| s.kind == SkipKind::Unreadable)
.expect("unreadable skip");
assert!(
skip.detail.contains(&SKILL_MD_MAX_BYTES.to_string()),
"skip detail must name the byte cap: {}",
skip.detail
);
let msg = unknown_or_skipped_skill_message("huge", &report.skips);
assert!(
msg.contains("skipped skill: huge"),
"oversized package must be named, not unknown: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
let why = crate::why(&report, Some("huge"), None, None);
assert_eq!(why.skips.len(), 1);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn skill_md_at_byte_cap_still_loads() {
let root = tempfile::tempdir().expect("tmp");
write_sized_skill(
&root.path().join(".agents").join("skills").join("padded"),
"padded",
SKILL_MD_MAX_BYTES as usize,
);
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert_eq!(report.skills.len(), 1, "skills={:?}", report.skills);
assert_eq!(report.skills[0].name, "padded");
assert!(report.skips.is_empty(), "skips={:?}", report.skips);
}
#[test]
fn validate_path_package_dir_joins_skill_md() {
let root = tempfile::tempdir().expect("tmp");
let pkg = root.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
let skill = pkg.join("SKILL.md");
let report = validate_path(&pkg);
assert!(
report.ok,
"package dir must join SKILL.md like extra-path: {:?}",
report
);
assert_eq!(report.name.as_deref(), Some("wanted"));
assert_eq!(
report.path.as_path(),
skill.as_path(),
"report.path must be the joined SKILL.md, not the directory"
);
assert!(report.skip.is_none());
}
#[test]
fn validate_path_package_dir_joins_lowercase_skill_md() {
let root = tempfile::tempdir().expect("tmp");
let pkg = root.path().join("lc-pack");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("skill.md"),
"---\nname: lc-pack\ndescription: d\n---\nbody\n",
)
.expect("write");
let report = validate_path(&pkg);
assert!(
report.ok,
"package dir must join skill.md like extra-path: {:?}",
report
);
assert_eq!(report.name.as_deref(), Some("lc-pack"));
assert_eq!(
report.path.parent(),
Some(pkg.as_path()),
"joined file must stay in the package dir: {:?}",
report.path
);
let file_name = report.path.file_name().and_then(|n| n.to_str());
assert!(
file_name == Some("skill.md") || file_name == Some("SKILL.md"),
"APFS may resolve skill.md as SKILL.md: {:?}",
report.path
);
}
#[test]
fn validate_missing_path_names_next_step_not_os_error() {
let root = tempfile::tempdir().expect("tmp");
let missing = root.path().join("no-such-skill");
let report = validate_path(&missing);
assert!(!report.ok, "missing path must fail: {report:?}");
let miss = report.miss().expect("peel");
assert_eq!(miss.error_kind, "unreadable");
assert!(
miss.error.contains("path does not exist:"),
"must name the hole: {}",
miss.error
);
assert!(
miss.error.contains("SKILL.md") && miss.error.contains("package directory"),
"must name the next step: {}",
miss.error
);
assert!(
!miss.error.contains("os error 2") && !miss.error.contains("No such file"),
"must not leak the raw OS string: {}",
miss.error
);
assert_eq!(miss.error.lines().count(), 1, "{}", miss.error);
let shown = crate::sanitize_error_token(&missing.display().to_string());
assert!(
miss.error.contains(&shown),
"must echo the sanitized path: {}",
miss.error
);
}
#[test]
fn validate_path_through_file_parent_names_missing_not_os_error() {
let root = tempfile::tempdir().expect("tmp");
fs::write(root.path().join("not-a-dir"), "file").expect("write");
let missing = root
.path()
.join("not-a-dir")
.join("skills")
.join("foo")
.join("SKILL.md");
let report = validate_path(&missing);
assert!(!report.ok, "path through a file must fail: {report:?}");
let miss = report.miss().expect("peel");
assert_eq!(miss.error_kind, "unreadable");
assert!(
miss.error.contains("path does not exist:"),
"a file parent is a missing path: {}",
miss.error
);
assert!(
!miss.error.contains("os error") && !miss.error.contains("Not a directory"),
"must not leak the raw OS string: {}",
miss.error
);
let shown = crate::sanitize_error_token(&missing.display().to_string());
assert!(
miss.error.contains(&shown),
"must echo the sanitized path: {}",
miss.error
);
}
#[test]
fn validate_path_dir_without_skill_md_is_unreadable() {
let root = tempfile::tempdir().expect("tmp");
let dir = root.path().join("collection");
fs::create_dir_all(dir.join("wanted")).expect("mkdir");
write_skill(&dir.join("wanted"), "wanted", "child");
let report = validate_path(&dir);
assert!(!report.ok, "collection root is not a package: {:?}", report);
assert!(report.name.is_none());
assert!(
report
.errors
.iter()
.any(|e| e.contains("not a skill package")),
"must not say SKILL.md is not a regular file: {:?}",
report.errors
);
let skip = report.skip.expect("skip");
assert_eq!(skip.kind, SkipKind::Unreadable);
assert_eq!(
skip.path.as_path(),
dir.as_path(),
"miss path stays the directory the user passed"
);
assert!(
!skip.detail.contains("regular file"),
"directory miss must name the package hole: {}",
skip.detail
);
}
#[test]
fn validate_non_skill_filename_is_not_a_package_file() {
let root = tempfile::tempdir().expect("tmp");
let pkg = root.path().join("falseok");
fs::create_dir_all(&pkg).expect("mkdir");
let readme = pkg.join("README.md");
fs::write(&readme, "---\nname: falseok\ndescription: d\n---\nbody\n").expect("write");
let report = validate_path(&readme);
assert!(
!report.ok,
"a matching name in README.md must not validate: {report:?}"
);
let err = report.errors.join(" ");
assert!(
err.contains("SKILL.md") && err.contains("README.md"),
"must name the required filename and the file we got: {err}"
);
assert_eq!(err.lines().count(), 1, "{err}");
write_skill(&pkg, "falseok", "body");
let ok = validate_path(&pkg.join("SKILL.md"));
assert!(
ok.ok,
"SKILL.md in the same directory still validates: {ok:?}"
);
}
#[test]
fn validate_path_rejects_oversized_skill_md() {
let root = tempfile::tempdir().expect("tmp");
let pkg = root.path().join("huge");
write_sized_skill(&pkg, "huge", SKILL_MD_MAX_BYTES as usize + 1);
let report = validate_path(&pkg.join("SKILL.md"));
assert!(!report.ok);
assert!(report.name.is_none());
assert!(
report
.errors
.iter()
.any(|e| e.contains(&SKILL_MD_MAX_BYTES.to_string())),
"errors={:?}",
report.errors
);
let skip = report.skip.expect("skip");
assert_eq!(skip.kind, SkipKind::Unreadable);
assert!(skip.detail.contains(&SKILL_MD_MAX_BYTES.to_string()));
}
fn write_unknown_key_skill(dir: &std::path::Path) {
fs::create_dir_all(dir).expect("mkdir");
fs::write(
dir.join("SKILL.md"),
"---\nname: demo\ndescription: d\nmade_up_field: x\n---\nbody\n",
)
.expect("write");
}
#[test]
fn validate_path_nfkc_dotdot_component_is_package_like_extra_path() {
let root = tempfile::tempdir().expect("tmp");
let pkg = root.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let via = pkg.join("evil").join("‥").join("SKILL.md");
let report = validate_path(&via);
assert!(
report.ok,
"validate must rewrite NFKC `..` like extra-path and read wanted/SKILL.md: {:?}",
report
);
assert_eq!(report.name.as_deref(), Some("wanted"));
assert!(report.skip.is_none());
}
#[test]
fn validate_path_ignores_unknown_frontmatter_key() {
let root = tempfile::tempdir().expect("tmp");
let pkg = root.path().join("demo");
write_unknown_key_skill(&pkg);
let report = validate_path(&pkg.join("SKILL.md"));
assert!(
report.ok,
"default validate ignores unknown keys: {:?}",
report.errors
);
assert_eq!(report.name.as_deref(), Some("demo"));
}
#[test]
fn validate_path_strict_rejects_unknown_frontmatter_key() {
let root = tempfile::tempdir().expect("tmp");
let pkg = root.path().join("demo");
write_unknown_key_skill(&pkg);
let report = validate_path_with_options(&pkg.join("SKILL.md"), true);
assert!(!report.ok, "strict must reject made_up_field");
assert!(
report.errors.iter().any(|e| e.contains("made_up_field")),
"errors={:?}",
report.errors
);
assert!(
report
.errors
.iter()
.any(|e| e.contains("unknown frontmatter key: made_up_field")),
"errors={:?}",
report.errors
);
assert!(
report
.errors
.iter()
.any(|e| e.contains("omit --strict / strict")),
"errors={:?}",
report.errors
);
assert!(
report.errors.iter().all(|e| !e.contains("invalid YAML")),
"errors={:?}",
report.errors
);
let skip = report.skip.as_ref().expect("skip");
assert_eq!(skip.kind, SkipKind::ParseError);
assert_eq!(skip.code(), "parse_error");
let miss = report.miss().expect("failed validate has a peel");
assert_eq!(miss.error_kind, "parse_error");
assert_eq!(miss.error, report.errors[0]);
assert!(!miss.is_not_found());
let json = serde_json::to_string(&miss).expect("ser");
let v: serde_json::Value = serde_json::from_str(&json).expect("json");
assert_eq!(v["error_kind"], "parse_error", "json={json}");
assert_eq!(v["error"], miss.error, "json={json}");
assert!(
v.get("errorKind").is_none(),
"error_kind must stay snake_case: {json}"
);
}
#[test]
fn validate_path_strict_rejects_two_unknown_frontmatter_keys() {
let root = tempfile::tempdir().expect("tmp");
let pkg = root.path().join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("SKILL.md"),
"---\nname: demo\ndescription: d\nmade_up_field: x\nalso_extra: y\n---\nbody\n",
)
.expect("write");
let report = validate_path_with_options(&pkg.join("SKILL.md"), true);
assert!(!report.ok, "strict must reject two unknown keys");
assert!(
report.errors.iter().any(|e| {
e.contains("unknown frontmatter keys: made_up_field, also_extra")
&& e.contains("made_up_field")
&& e.contains("also_extra")
}),
"errors={:?}",
report.errors
);
assert!(
report
.errors
.iter()
.any(|e| e.contains("omit --strict / strict")),
"errors={:?}",
report.errors
);
assert!(
report.errors.iter().all(|e| !e.contains("invalid YAML")),
"errors={:?}",
report.errors
);
let skip = report.skip.as_ref().expect("skip");
assert_eq!(skip.kind, SkipKind::ParseError);
assert_eq!(skip.code(), "parse_error");
let miss = report.miss().expect("failed validate has a peel");
assert_eq!(miss.error_kind, "parse_error");
assert_eq!(miss.error, report.errors[0]);
}
#[test]
fn validate_miss_exposes_error_kind() {
let root = tempfile::tempdir().expect("tmp");
let ok_pkg = root.path().join("demo");
write_unknown_key_skill(&ok_pkg);
let ok = validate_path(&ok_pkg.join("SKILL.md"));
assert!(ok.ok, "ok={ok:?}");
assert!(ok.miss().is_none(), "ok validate must not peel a miss");
let mismatch = corpus_dir().join("agentskills/name-mismatch/wrong-dir/SKILL.md");
let report = validate_path(&mismatch);
assert!(!report.ok, "name mismatch must fail: {report:?}");
let miss = report.miss().expect("peel");
assert_eq!(miss.error_kind, "name_directory_mismatch");
assert!(
miss.error.contains("good-name"),
"mismatch peel must name the frontmatter: {}",
miss.error
);
assert!(!miss.is_not_found());
let missing = root.path().join("no-such").join("SKILL.md");
let unread = validate_path(&missing);
assert!(!unread.ok, "missing path must fail: {unread:?}");
let miss = unread.miss().expect("peel");
assert_eq!(miss.error_kind, "unreadable");
assert!(!miss.is_not_found());
let json = serde_json::to_string(&miss).expect("ser");
let v: serde_json::Value = serde_json::from_str(&json).expect("json");
assert_eq!(v["error_kind"], "unreadable", "json={json}");
assert!(
v.get("errorKind").is_none(),
"error_kind must stay snake_case: {json}"
);
}
#[test]
fn validate_miss_exposes_path() {
let mismatch = corpus_dir().join("agentskills/name-mismatch/wrong-dir/SKILL.md");
let report = validate_path(&mismatch);
let miss = report.miss().expect("peel");
assert_eq!(
miss.path.as_deref(),
Some(report.path.as_path()),
"validate peel must keep the SKILL.md path: {miss:?}"
);
let json = serde_json::to_string(&miss).expect("ser");
let v: serde_json::Value = serde_json::from_str(&json).expect("json");
assert_eq!(
v["path"].as_str().map(std::path::Path::new),
Some(report.path.as_path()),
"json={json}"
);
let root = tempfile::tempdir().expect("tmp");
let missing = root.path().join("no-such").join("SKILL.md");
let unread = validate_path(&missing);
let miss = unread.miss().expect("peel");
assert_eq!(
miss.path.as_deref(),
Some(unread.path.as_path()),
"unreadable peel must keep the asked path: {miss:?}"
);
assert!(
miss.winner_path.is_none(),
"unreadable validate has no collision winner: {miss:?}"
);
}
#[test]
fn validate_hostile_unknown_key_keeps_error_kind_stable() {
let root = tempfile::tempdir().expect("tmp");
let pkg = root.path().join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("SKILL.md"),
"---\nname: demo\ndescription: d\nevil\u{2028}key: x\n---\nbody\n",
)
.expect("write");
let report = validate_path_with_options(&pkg.join("SKILL.md"), true);
assert!(!report.ok, "strict must reject U+2028 key: {report:?}");
let miss = report.miss().expect("peel");
assert_eq!(miss.error_kind, "parse_error");
assert_eq!(
miss.error.lines().count(),
1,
"validate peel must stay one line: {:?}",
miss.error
);
assert!(
!miss.error.contains('\u{2028}'),
"U+2028 must not leak into error: {}",
miss.error
);
assert!(
miss.error.contains("evil?key"),
"hostile key must be sanitized: {}",
miss.error
);
let json = serde_json::to_string(&miss).expect("ser");
let v: serde_json::Value = serde_json::from_str(&json).expect("json");
assert_eq!(v["error_kind"], "parse_error", "json={json}");
assert!(
v.get("errorKind").is_none(),
"error_kind must stay snake_case: {json}"
);
}
#[test]
fn validate_path_strict_accepts_corpus_and_host_extensions() {
let corpus = corpus_dir().join("agentskills/minimal-valid/SKILL.md");
let report = validate_path_with_options(&corpus, true);
assert!(report.ok, "corpus must pass --strict: {:?}", report.errors);
let root = tempfile::tempdir().expect("tmp");
let pkg = root.path().join("hosty");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("SKILL.md"),
concat!(
"---\n",
"name: hosty\n",
"description: host extensions\n",
"license: MIT\n",
"compatibility: rust\n",
"allowed-tools: Read\n",
"triggers:\n",
" - hosty\n",
"user_invocable: true\n",
"disable_model_invocation: false\n",
"argument-hint: name\n",
"when-to-use: when testing\n",
"metadata:\n",
" author: craftbag\n",
"---\nbody\n",
),
)
.expect("write");
let report = validate_path_with_options(&pkg.join("SKILL.md"), true);
assert!(
report.ok,
"known fields and host extensions must pass --strict: {:?}",
report.errors
);
}
#[test]
fn ignore_dotdot_through_missing_component_matches() {
let root = project_with_secret_and_public();
let ignore = root
.path()
.join(".agents")
.join("skills")
.join("missing")
.join("..")
.join("secret");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
ignore: vec![ignore.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_secret_ignored(&report);
}
#[test]
fn ignore_nfkc_dot_argument_is_cwd_like_extra_path() {
for raw in [".", "․", "﹒", " . ", "\u{00A0}.\u{00A0}"] {
let root = project_with_secret_and_public();
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
ignore: vec![raw.to_owned()],
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"ignore `{raw}` must be cwd like extra-path `.`: {:?}",
report.skills
);
}
}
#[test]
fn ignore_nfkc_dotdot_component_collapses_like_ascii() {
let root = project_with_secret_and_public();
let via = root
.path()
.join(".agents")
.join("skills")
.join("secret")
.join("evil")
.join("‥");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
ignore: vec![via.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_secret_ignored(&report);
}
#[test]
fn ignore_mixed_ascii_and_nfkc_dots_collapse_like_ascii() {
let root = project_with_secret_and_public();
for suffix in ["..", "..", " ‥ ", ".."] {
let ignore = format!(".agents/skills/secret/evil/{suffix}");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
ignore: vec![ignore.clone()],
..DiscoveryOptions::default()
},
);
assert_secret_ignored(&report);
}
}
#[cfg(unix)]
#[test]
fn package_dir_symlink_escape_is_unreadable() {
let root = tempfile::tempdir().expect("tmp");
let outside = tempfile::tempdir().expect("out");
write_skill(&outside.path().join("stolen"), "stolen", "SECRET_BODY");
let skills = root.path().join(".agents").join("skills");
fs::create_dir_all(&skills).expect("mkdir");
std::os::unix::fs::symlink(outside.path().join("stolen"), skills.join("stolen"))
.expect("symlink package dir");
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(
report.skills.iter().all(|s| s.name != "stolen"),
"package dir symlink must not load the escaped skill: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("SECRET_BODY")),
"escaped skill body must not be loaded: {:?}",
report.skills
);
let skip = report
.skips
.iter()
.find(|s| s.kind == SkipKind::Unreadable)
.expect("unreadable skip");
assert!(
skip.detail.contains("escapes"),
"skip detail must name the escape: {}",
skip.detail
);
let msg = unknown_or_skipped_skill_message("stolen", &report.skips);
assert!(
msg.contains("skipped skill: stolen"),
"escaped package must be named, not unknown: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
}
#[cfg(unix)]
#[test]
fn dangling_package_symlink_is_unreadable() {
let root = tempfile::tempdir().expect("tmp");
let skills = root.path().join(".agents").join("skills");
fs::create_dir_all(&skills).expect("mkdir");
write_skill(&skills.join("ok"), "ok", "body");
fs::create_dir_all(skills.join("assets")).expect("mkdir assets");
fs::write(skills.join("README.md"), "notes").expect("readme");
std::os::unix::fs::symlink(skills.join("README.md"), skills.join("readme-link"))
.expect("file symlink");
let missing = root.path().join("missing-package");
std::os::unix::fs::symlink(&missing, skills.join("gone")).expect("dangling symlink");
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(
report.skills.iter().any(|s| s.name == "ok"),
"real package must still load: {:?}",
report.skills
);
let skip = report
.skips
.iter()
.find(|s| s.path.ends_with(".agents/skills/gone"))
.unwrap_or_else(|| {
panic!(
"dangling package must be a skip: skills={:?} skips={:?}",
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
report.skips
)
});
assert_eq!(skip.kind, SkipKind::Unreadable);
assert!(
skip.detail.contains("dangling symlink"),
"detail={}",
skip.detail
);
assert!(
skip.detail.contains("missing-package"),
"detail={}",
skip.detail
);
assert!(
report.skips.iter().all(|s| {
let name = s.path.file_name().and_then(|n| n.to_str());
name != Some("README.md") && name != Some("readme-link") && name != Some("assets")
}),
"files and empty dirs are not dangling packages: {:?}",
report.skips
);
let msg = unknown_or_skipped_skill_message("gone", &report.skips);
assert!(
msg.contains("dangling symlink"),
"load must name the broken package: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
}
#[cfg(unix)]
#[test]
fn dangling_skill_md_symlink_is_not_an_escape() {
let root = tempfile::tempdir().expect("tmp");
let skills = root.path().join(".agents").join("skills");
write_skill(&skills.join("visible"), "visible", "body");
let broken = skills.join("broken");
fs::create_dir_all(&broken).expect("mkdir broken");
std::os::unix::fs::symlink("gone.md", broken.join("SKILL.md")).expect("dangling skill");
let linked = skills.join("linked");
fs::create_dir_all(&linked).expect("mkdir linked");
fs::write(
linked.join("body.md"),
"---\nname: linked\ndescription: linked skill\n---\nbody\n",
)
.expect("body");
std::os::unix::fs::symlink("body.md", linked.join("SKILL.md")).expect("in-package link");
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(
report.skills.iter().any(|s| s.name == "visible"),
"{:?}",
report.skills
);
assert!(
report.skills.iter().any(|s| s.name == "linked"),
"in-package link must load: {:?}",
report.skills
);
let skip = report
.skips
.iter()
.find(|s| s.path.ends_with("broken/SKILL.md"))
.unwrap_or_else(|| panic!("broken package must skip: {:?}", report.skips));
assert_eq!(skip.kind, SkipKind::Unreadable);
assert!(
skip.detail.contains("dangling symlink") && skip.detail.contains("gone.md"),
"{}",
skip.detail
);
assert!(
!skip.detail.contains("escapes"),
"a missing target is not an escape: {}",
skip.detail
);
let validated = validate_path(&broken);
assert!(!validated.ok, "{validated:?}");
let detail = &validated.skip.expect("validate skip").detail;
assert!(
detail.contains("dangling symlink") && !detail.contains("escapes"),
"{detail}"
);
let extra = tempfile::tempdir().expect("extra");
let only = extra.path().join("only");
fs::create_dir_all(&only).expect("mkdir only");
std::os::unix::fs::symlink("missing.md", only.join("SKILL.md")).expect("extra link");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(report.skills.is_empty(), "{:?}", report.skills);
assert_eq!(report.skips.len(), 1, "{:?}", report.skips);
assert!(
report.skips[0].detail.contains("dangling symlink")
&& !report.skips[0].detail.contains("escapes"),
"{}",
report.skips[0].detail
);
let both = tempfile::tempdir().expect("both");
std::os::unix::fs::symlink("missing.md", both.path().join("SKILL.md")).expect("root link");
write_skill(&both.path().join("skills").join("kept"), "kept", "body");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
paths: vec![both.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(
report.skills.iter().any(|s| s.name == "kept"),
"{:?}",
report.skills
);
let skip = report
.skips
.iter()
.find(|s| s.path.ends_with("SKILL.md"))
.unwrap_or_else(|| panic!("root link must skip: {:?}", report.skips));
assert!(
skip.detail.contains("dangling symlink") && !skip.detail.contains("escapes"),
"{}",
skip.detail
);
}
#[cfg(unix)]
#[test]
fn symlink_loop_skill_md_is_not_an_escape() {
let root = tempfile::tempdir().expect("tmp");
let skills = root.path().join(".agents").join("skills");
write_skill(&skills.join("visible"), "visible", "body");
let self_loop = skills.join("selfloop");
fs::create_dir_all(&self_loop).expect("mkdir self");
std::os::unix::fs::symlink("SKILL.md", self_loop.join("SKILL.md")).expect("self loop");
let pair = skills.join("pair");
fs::create_dir_all(&pair).expect("mkdir pair");
std::os::unix::fs::symlink("b.md", pair.join("a.md")).expect("pair a");
std::os::unix::fs::symlink("a.md", pair.join("b.md")).expect("pair b");
std::os::unix::fs::symlink("a.md", pair.join("SKILL.md")).expect("pair skill");
let outside = tempfile::tempdir().expect("out");
fs::write(outside.path().join("secret.md"), "SECRET_BODY").expect("secret");
let leak = skills.join("leak");
fs::create_dir_all(&leak).expect("mkdir leak");
std::os::unix::fs::symlink(outside.path().join("secret.md"), leak.join("SKILL.md"))
.expect("outside link");
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(
report.skills.iter().any(|s| s.name == "visible"),
"{:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("SECRET_BODY")),
"outside link must not load: {:?}",
report.skills
);
let self_skip = report
.skips
.iter()
.find(|s| s.path.ends_with("selfloop/SKILL.md"))
.unwrap_or_else(|| panic!("self loop must skip: {:?}", report.skips));
assert_eq!(self_skip.kind, SkipKind::Unreadable);
assert!(
self_skip.detail.contains("symbolic link loop")
&& self_skip.detail.contains("SKILL.md")
&& !self_skip.detail.contains("escapes"),
"{}",
self_skip.detail
);
let pair_skip = report
.skips
.iter()
.find(|s| s.path.ends_with("pair/SKILL.md"))
.unwrap_or_else(|| panic!("pair loop must skip: {:?}", report.skips));
assert!(
pair_skip.detail.contains("symbolic link loop")
&& pair_skip.detail.contains("a.md")
&& !pair_skip.detail.contains("escapes"),
"{}",
pair_skip.detail
);
let leak_skip = report
.skips
.iter()
.find(|s| s.path.ends_with("leak/SKILL.md"))
.unwrap_or_else(|| panic!("outside link must skip: {:?}", report.skips));
assert!(
leak_skip.detail.contains("escapes") && !leak_skip.detail.contains("symbolic link loop"),
"{}",
leak_skip.detail
);
let validated = validate_path(&self_loop);
assert!(!validated.ok, "{validated:?}");
let detail = &validated.skip.expect("validate skip").detail;
assert!(
detail.contains("symbolic link loop") && !detail.contains("escapes"),
"{detail}"
);
let extra = tempfile::tempdir().expect("extra");
let only = extra.path().join("only");
fs::create_dir_all(&only).expect("mkdir only");
std::os::unix::fs::symlink("SKILL.md", only.join("SKILL.md")).expect("extra loop");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(report.skills.is_empty(), "{:?}", report.skills);
assert_eq!(report.skips.len(), 1, "{:?}", report.skips);
assert!(
report.skips[0].detail.contains("symbolic link loop")
&& !report.skips[0].detail.contains("escapes"),
"{}",
report.skips[0].detail
);
let both = tempfile::tempdir().expect("both");
std::os::unix::fs::symlink("SKILL.md", both.path().join("SKILL.md")).expect("root loop");
write_skill(&both.path().join("skills").join("kept"), "kept", "body");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
paths: vec![both.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(
report.skills.iter().any(|s| s.name == "kept"),
"{:?}",
report.skills
);
let skip = report
.skips
.iter()
.find(|s| s.path.ends_with("SKILL.md"))
.unwrap_or_else(|| panic!("root loop must skip: {:?}", report.skips));
assert!(
skip.detail.contains("symbolic link loop") && !skip.detail.contains("escapes"),
"{}",
skip.detail
);
}
#[cfg(unix)]
#[test]
fn symlink_loop_package_dir_is_a_skip() {
let root = tempfile::tempdir().expect("tmp");
let skills = root.path().join(".agents").join("skills");
write_skill(&skills.join("visible"), "visible", "body");
std::os::unix::fs::symlink("looppkg", skills.join("looppkg")).expect("package loop");
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(
report.skills.iter().any(|s| s.name == "visible"),
"{:?}",
report.skills
);
let skip = report
.skips
.iter()
.find(|s| s.path.ends_with("looppkg"))
.unwrap_or_else(|| panic!("package loop must skip: {:?}", report.skips));
assert_eq!(skip.kind, SkipKind::Unreadable);
assert_eq!(skip.name.as_deref(), Some("looppkg"));
assert!(
skip.detail.contains("symbolic link loop")
&& skip.detail.contains("looppkg")
&& !skip.detail.contains("escapes"),
"{}",
skip.detail
);
let validated = validate_path(&skills.join("looppkg"));
assert!(!validated.ok, "{validated:?}");
let detail = &validated.skip.expect("validate skip").detail;
assert!(
detail.contains("symbolic link loop") && !detail.contains("must be named"),
"{detail}"
);
let loop_file = skills.join("selfmd");
fs::create_dir_all(&loop_file).expect("mkdir selfmd");
std::os::unix::fs::symlink("SKILL.md", loop_file.join("SKILL.md")).expect("file loop");
let validated = validate_path(&loop_file.join("SKILL.md"));
assert!(!validated.ok, "{validated:?}");
let detail = &validated.skip.expect("file skip").detail;
assert!(
detail.contains("symbolic link loop")
&& detail.contains("SKILL.md")
&& !detail.contains("escapes"),
"{detail}"
);
}
#[cfg(unix)]
#[test]
fn symlink_loop_extra_path_file_matches_validate() {
let root = tempfile::tempdir().expect("tmp");
let file = root.path().join("SKILL.md");
std::os::unix::fs::symlink("SKILL.md", &file).expect("loop");
let report = empty_home_discover(
root.path(),
&DiscoveryOptions {
paths: vec![file.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(report.skills.is_empty(), "{:?}", report.skills);
assert_eq!(report.skips.len(), 1, "{:?}", report.skips);
assert!(
report.skips[0].detail.contains("symbolic link loop")
&& report.skips[0].detail.contains("SKILL.md")
&& !report.skips[0].detail.contains("Too many levels"),
"{}",
report.skips[0].detail
);
let validated = validate_path(&file);
let detail = &validated.skip.expect("validate skip").detail;
assert_eq!(detail, &report.skips[0].detail);
}
#[cfg(unix)]
#[test]
fn skills_root_skill_md_symlink_escape_is_unreadable_not_root_file() {
let root = tempfile::tempdir().expect("tmp");
let outside = tempfile::tempdir().expect("out");
fs::write(
outside.path().join("secret.md"),
"---\nname: stolen\ndescription: leaked\n---\nSECRET_BODY\n",
)
.expect("write");
let skills = root.path().join(".agents").join("skills");
fs::create_dir_all(&skills).expect("mkdir");
std::os::unix::fs::symlink(outside.path().join("secret.md"), skills.join("SKILL.md"))
.expect("symlink");
write_skill(&skills.join("public"), "public", "ok");
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(
report.skills.iter().all(|s| s.name != "stolen"),
"skills-root SKILL.md symlink must not load the escaped file: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("SECRET_BODY")),
"escaped SKILL.md body must not be loaded: {:?}",
report.skills
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling packages must still load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.detail.contains("escapes")
&& s.path.ends_with("SKILL.md")
}),
"escaped skills-root SKILL.md must be unreadable, not peeked: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"escaped SKILL.md must not become a root_file peek: {:?}",
report.skips
);
assert!(
report
.skips
.iter()
.all(|s| s.name.as_deref() != Some("stolen")),
"must not peek frontmatter from an escaped SKILL.md: {:?}",
report.skips
);
let msg = unknown_or_skipped_skill_message("stolen", &report.skips);
assert!(
msg.contains("unknown skill: stolen"),
"escaped root SKILL.md must not identify as stolen: {msg}"
);
}
#[cfg(unix)]
fn mkfifo(path: &std::path::Path) {
let status = std::process::Command::new("mkfifo")
.arg(path)
.status()
.expect("run mkfifo");
assert!(status.success(), "mkfifo {path:?} failed: {status}");
}
#[cfg(unix)]
fn unblock_fifo(path: &std::path::Path) {
let _ = std::fs::OpenOptions::new().write(true).open(path);
}
#[cfg(unix)]
#[test]
fn skills_root_fifo_skill_md_is_unreadable_not_hang() {
let root = tempfile::tempdir().expect("tmp");
let skills = root.path().join(".agents").join("skills");
fs::create_dir_all(&skills).expect("mkdir");
let fifo = skills.join("SKILL.md");
mkfifo(&fifo);
write_skill(&skills.join("public"), "public", "ok");
let home = tempfile::tempdir().expect("home");
let root_path = root.path().to_path_buf();
let home_path = home.path().to_path_buf();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let report = with_home_override(Some(home_path), || {
discover(&root_path, &DiscoveryOptions::default())
});
let _ = tx.send(report);
});
let report = match rx.recv_timeout(std::time::Duration::from_secs(2)) {
Ok(r) => r,
Err(_) => {
unblock_fifo(&fifo);
let _ = rx.recv_timeout(std::time::Duration::from_secs(2));
panic!("discover must not block on a FIFO SKILL.md");
}
};
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling packages must still load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("SKILL.md")
&& s.name.is_none()
&& s.detail.contains("regular file")
}),
"FIFO skills-root SKILL.md must be unreadable without peek: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"FIFO SKILL.md must not become a root_file peek: {:?}",
report.skips
);
}
#[cfg(unix)]
#[test]
fn validate_path_fifo_is_unreadable_not_hang() {
let tmp = tempfile::tempdir().expect("tmp");
let fifo = tmp.path().join("SKILL.md");
mkfifo(&fifo);
let path = fifo.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(validate_path(&path));
});
let report = match rx.recv_timeout(std::time::Duration::from_secs(2)) {
Ok(r) => r,
Err(_) => {
unblock_fifo(&fifo);
let _ = rx.recv_timeout(std::time::Duration::from_secs(2));
panic!("validate must not block on a FIFO SKILL.md");
}
};
assert!(!report.ok, "FIFO must not validate: {:?}", report);
let skip = report.skip.expect("skip");
assert_eq!(skip.kind, SkipKind::Unreadable, "skip={skip:?}");
assert!(skip.name.is_none(), "must not peek a FIFO: {skip:?}");
assert!(
skip.detail.contains("regular file"),
"detail must name the file type: {}",
skip.detail
);
}
#[cfg(unix)]
#[test]
fn validate_path_package_dir_skill_md_symlink_escape_is_unreadable() {
let tmp = tempfile::tempdir().expect("tmp");
let outside = tempfile::tempdir().expect("out");
fs::write(
outside.path().join("secret.md"),
"---\nname: wanted\ndescription: leaked\n---\nSECRET_BODY\n",
)
.expect("write");
let pkg = tmp.path().join("wanted");
fs::create_dir_all(&pkg).expect("mkdir");
std::os::unix::fs::symlink(outside.path().join("secret.md"), pkg.join("SKILL.md"))
.expect("symlink");
let report = validate_path(&pkg);
assert!(
!report.ok,
"package dir whose SKILL.md escapes the package must not validate: {:?}",
report
);
assert!(
report.name.is_none(),
"must not peek escaped SKILL.md: {:?}",
report
);
let skip = report.skip.expect("skip");
assert_eq!(skip.kind, SkipKind::Unreadable, "skip={skip:?}");
assert!(
skip.name.is_none(),
"must not peek a symlink escape: {skip:?}"
);
assert!(
skip.detail.contains("escapes"),
"detail must name the escape: {}",
skip.detail
);
}
fn assert_validate_token_does_not_retarget_root(raw: &str) {
let report = validate_path(std::path::Path::new(raw));
assert!(
!report.ok,
"validate token {raw:?} must not be ok (must not retarget to /): {report:?}"
);
assert!(
report.name.is_none(),
"must not peek a collapsed root token {raw:?}: {report:?}"
);
let path_s = report.path.display().to_string();
for banned in ["/", "/..", "/SKILL.md", "/skill.md", "/../SKILL.md"] {
assert_ne!(
path_s.as_str(),
banned,
"validate must not collapse {raw:?} to {banned}: {report:?}"
);
}
let skip = report.skip.as_ref().expect("skip");
assert_eq!(skip.kind, SkipKind::Unreadable, "skip={skip:?}");
let skip_s = skip.path.display().to_string();
for banned in ["/", "/..", "/SKILL.md", "/../SKILL.md"] {
assert_ne!(
skip_s.as_str(),
banned,
"skip.path must not be {banned} for {raw:?}: {skip:?}"
);
}
assert!(
skip.detail.contains("validate") && skip.detail.contains("skills_validate"),
"detail must name validate / skills_validate for {raw:?}: {}",
skip.detail
);
assert!(
skip.detail.contains("line separator") || skip.detail.contains("collapses"),
"detail must name the refuse for {raw:?}: {}",
skip.detail
);
assert!(
!path_s.contains('\n')
&& !path_s.contains('\r')
&& !path_s.contains('\u{2028}')
&& !path_s.contains('\u{2029}'),
"report.path must not echo a raw line separator for {raw:?}: {path_s:?}"
);
assert!(
!skip_s.contains('\n')
&& !skip_s.contains('\r')
&& !skip_s.contains('\u{2028}')
&& !skip_s.contains('\u{2029}'),
"skip.path must not echo a raw line separator for {raw:?}: {skip_s:?}"
);
}
#[test]
fn validate_path_line_separator_dotdot_does_not_retarget_to_fs_root() {
for raw in ["/..\n", "/..\r", "/..\u{2028}", "/..\u{2029}"] {
assert_validate_token_does_not_retarget_root(raw);
}
}
#[test]
fn validate_path_whitespace_collapse_dotdot_does_not_retarget_to_fs_root() {
for raw in [
" /..",
"\t/..",
"\u{85}/..",
"\u{00a0}/..",
"/ ..",
"/\t..",
"/\u{00a0}..",
] {
assert_validate_token_does_not_retarget_root(raw);
}
}
#[test]
fn validate_collapse_refuse_names_flag() {
let collapse = validate_path(std::path::Path::new(" /.."));
assert!(
!collapse.ok,
"collapse token must not validate: {collapse:?}"
);
let skip = collapse.skip.as_ref().expect("collapse skip");
assert_eq!(skip.kind, SkipKind::Unreadable, "skip={skip:?}");
assert!(
skip.detail.contains("validate") && skip.detail.contains("skills_validate"),
"validate collapse must name validate / skills_validate: {}",
skip.detail
);
let miss = collapse.miss().expect("collapse peel");
assert_eq!(miss.error_kind, "unreadable", "miss={miss:?}");
assert!(
miss.error.contains("validate") && miss.error.contains("skills_validate"),
"validate peel must name validate / skills_validate: {}",
miss.error
);
let line = validate_path(std::path::Path::new("/..\n"));
assert!(!line.ok, "line-separator token must not validate: {line:?}");
let skip = line.skip.as_ref().expect("line skip");
assert_eq!(skip.kind, SkipKind::Unreadable, "skip={skip:?}");
assert!(
skip.detail.contains("validate") && skip.detail.contains("skills_validate"),
"validate line-separator must name validate / skills_validate: {}",
skip.detail
);
}
#[cfg(unix)]
#[test]
fn validate_path_package_dir_newline_component_is_unreadable() {
let parent = tempfile::tempdir().expect("parent");
let pkg = parent.path().join("evil\nroot").join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("SKILL.md"),
"---\nname: demo\ndescription: d\n---\nSECRET_BODY\n",
)
.expect("write");
let report = validate_path(&pkg);
assert!(
!report.ok,
"package dir with a newline component must not validate: {report:?}"
);
assert!(
report.name.is_none(),
"must not peek newline-path SKILL.md: {report:?}"
);
let skip = report.skip.expect("skip");
assert_eq!(skip.kind, SkipKind::Unreadable, "skip={skip:?}");
let path_s = skip.path.display().to_string();
assert!(
!path_s.contains('\n') && !path_s.contains('\u{2028}') && !path_s.contains('\u{2029}'),
"skip.path must not echo a raw line separator: {path_s:?}"
);
assert!(
skip.detail.contains("line separator") || skip.detail.contains("path"),
"detail must name the refuse: {}",
skip.detail
);
}
#[cfg(unix)]
fn discover_extra_path_with_timeout(
extra: PathBuf,
fifo: &std::path::Path,
panic_msg: &'static str,
) -> crate::skip::DiscoveryReport {
let cwd = tempfile::tempdir().expect("cwd");
let home = tempfile::tempdir().expect("home");
let cwd_path = cwd.path().to_path_buf();
let home_path = home.path().to_path_buf();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let report = with_home_override(Some(home_path), || {
discover(
&cwd_path,
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
..DiscoveryOptions::default()
},
)
});
let _ = tx.send(report);
});
match rx.recv_timeout(std::time::Duration::from_secs(2)) {
Ok(r) => r,
Err(_) => {
unblock_fifo(fifo);
let _ = rx.recv_timeout(std::time::Duration::from_secs(2));
panic!("{panic_msg}");
}
}
}
#[cfg(unix)]
#[test]
fn extra_path_fifo_skill_md_is_unreadable_not_silent() {
let extra = tempfile::tempdir().expect("extra");
let fifo = extra.path().join("SKILL.md");
mkfifo(&fifo);
let report = discover_extra_path_with_timeout(
fifo.clone(),
&fifo,
"discover must not block on extra-path FIFO SKILL.md",
);
assert!(
report.skills.is_empty(),
"FIFO extra-path must not load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("SKILL.md")
&& s.name.is_none()
&& s.detail.contains("regular file")
}),
"extra-path FIFO SKILL.md must be unreadable, not a silent miss: {:?}",
report.skips
);
let validated = validate_path(&fifo);
assert!(!validated.ok, "validate must reject FIFO: {validated:?}");
let vskip = validated.skip.expect("validate skip");
assert_eq!(vskip.kind, SkipKind::Unreadable);
assert!(
vskip.detail.contains("regular file"),
"validate/discover must agree: {}",
vskip.detail
);
}
#[cfg(unix)]
#[test]
fn watch_dirs_omits_extra_path_fifo_skill_md() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let fifo = extra.path().join("SKILL.md");
mkfifo(&fifo);
let opts = DiscoveryOptions {
paths: vec![fifo.to_string_lossy().into_owned()],
..DiscoveryOptions::default()
};
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
!watch_paths_contain(&dirs, &fifo),
"watch_dirs must not list extra-path FIFO SKILL.md (discover does not load it): {dirs:?}"
);
}
#[test]
fn watch_dirs_omits_non_directory_project_and_home_roots() {
let cwd = tempfile::tempdir().expect("cwd");
let agents = cwd.path().join(".agents");
fs::create_dir_all(&agents).expect("mkdir .agents");
let agents_skills = agents.join("skills");
fs::write(&agents_skills, "not-a-dir").expect("file .agents/skills");
let home = tempfile::tempdir().expect("home");
let home_agents = home.path().join(".agents");
fs::create_dir_all(&home_agents).expect("mkdir HOME/.agents");
let home_skills = home_agents.join("skills");
fs::write(&home_skills, "not-a-dir").expect("file HOME/.agents/skills");
let user = tempfile::tempdir().expect("user parent");
let user_file = user.path().join("user-skills");
fs::write(&user_file, "not-a-dir").expect("file user_dir");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
user_skills_dir: Some(user_file.clone()),
..DiscoveryOptions::default()
},
)
});
assert!(
!watch_paths_contain(&dirs, &agents_skills),
"watch_dirs must not list a file at .agents/skills (notify cannot walk it): {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &home_skills),
"watch_dirs must not list a file at HOME/.agents/skills: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &user_file),
"watch_dirs must not list a file user_dir: {dirs:?}"
);
}
#[cfg(unix)]
#[test]
fn watch_dirs_omits_fifo_project_home_user_and_extra_roots() {
let cwd = tempfile::tempdir().expect("cwd");
let agents = cwd.path().join(".agents");
fs::create_dir_all(&agents).expect("mkdir .agents");
let agents_fifo = agents.join("skills");
mkfifo(&agents_fifo);
let claude = cwd.path().join(".claude");
fs::create_dir_all(&claude).expect("mkdir .claude");
let claude_fifo = claude.join("skills");
mkfifo(&claude_fifo);
let home = tempfile::tempdir().expect("home");
let home_agents = home.path().join(".agents");
fs::create_dir_all(&home_agents).expect("mkdir HOME/.agents");
let home_fifo = home_agents.join("skills");
mkfifo(&home_fifo);
let user_parent = tempfile::tempdir().expect("user parent");
let user_fifo = user_parent.path().join("user-skills");
mkfifo(&user_fifo);
let extra_fifo = user_parent.path().join("extra");
mkfifo(&extra_fifo);
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
vendor_roots: vec!["claude".to_owned()],
user_skills_dir: Some(user_fifo.clone()),
paths: vec![extra_fifo.to_string_lossy().into_owned()],
..DiscoveryOptions::default()
},
)
});
assert!(
!watch_paths_contain(&dirs, &agents_fifo),
"watch_dirs must not list FIFO .agents/skills (notify can hang): {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &claude_fifo),
"watch_dirs must not list FIFO .claude/skills: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &home_fifo),
"watch_dirs must not list FIFO HOME/.agents/skills: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &user_fifo),
"watch_dirs must not list FIFO user_dir: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &extra_fifo),
"watch_dirs must not list FIFO extra-path: {dirs:?}"
);
}
#[cfg(unix)]
#[test]
fn extra_path_package_fifo_skill_md_is_unreadable_not_silent() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
let fifo = pkg.join("SKILL.md");
mkfifo(&fifo);
write_skill(&extra.path().join("public"), "public", "ok");
let report = discover_extra_path_with_timeout(
extra.path().to_path_buf(),
&fifo,
"discover must not block on extra-path package FIFO SKILL.md",
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling packages must still load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("demo/SKILL.md")
&& s.name.is_none()
&& s.detail.contains("regular file")
}),
"package FIFO SKILL.md must be unreadable, not a silent miss: {:?}",
report.skips
);
let msg = unknown_or_skipped_skill_message("demo", &report.skips);
assert!(
msg.contains("skipped skill: demo"),
"FIFO package must not look unknown: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
let why = crate::why(&report, Some("demo"), None, None);
assert!(why.loaded.is_empty(), "why loaded={:?}", why.loaded);
assert_eq!(why.skips.len(), 1, "why skips={:?}", why.skips);
assert_eq!(why.skips[0].kind, SkipKind::Unreadable);
assert!(why.unknown_skill_message().is_none());
let validated = validate_path(&fifo);
assert!(!validated.ok, "validate must reject FIFO: {validated:?}");
let vskip = validated.skip.expect("validate skip");
assert_eq!(vskip.kind, SkipKind::Unreadable);
assert!(
vskip.detail.contains("regular file"),
"validate/discover must agree: {}",
vskip.detail
);
}
#[cfg(unix)]
#[test]
fn extra_path_named_fifo_file_load_why_validate_agree() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
fs::create_dir_all(&pkg).expect("mkdir");
let fifo = pkg.join("SKILL.md");
mkfifo(&fifo);
let report = discover_extra_path_with_timeout(
fifo.clone(),
&fifo,
"discover must not block on extra-path FIFO SKILL.md file",
);
assert!(
report.skills.is_empty(),
"FIFO extra-path file must not load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("wanted/SKILL.md")
&& s.name.is_none()
&& s.detail.contains("regular file")
}),
"named FIFO extra-path file must be unreadable: {:?}",
report.skips
);
let msg = unknown_or_skipped_skill_message("wanted", &report.skips);
assert!(
msg.contains("skipped skill: wanted"),
"named FIFO extra-path file must not look unknown: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
let why = crate::why(&report, Some("wanted"), None, None);
assert!(why.loaded.is_empty(), "why loaded={:?}", why.loaded);
assert_eq!(why.skips.len(), 1, "why skips={:?}", why.skips);
assert_eq!(why.skips[0].kind, SkipKind::Unreadable);
assert!(why.unknown_skill_message().is_none());
let validated = validate_path(&fifo);
assert!(!validated.ok, "validate must reject FIFO: {validated:?}");
let vskip = validated.skip.expect("validate skip");
assert_eq!(vskip.kind, SkipKind::Unreadable);
assert!(
vskip.detail.contains("regular file"),
"validate/discover must agree: {}",
vskip.detail
);
}
#[cfg(unix)]
#[test]
fn extra_path_package_dir_fifo_does_not_scan_nested_skill() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
fs::create_dir_all(&pkg).expect("mkdir");
let fifo = pkg.join("SKILL.md");
mkfifo(&fifo);
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let report = discover_extra_path_with_timeout(
pkg,
&fifo,
"discover must not block on extra-path package-dir FIFO SKILL.md",
);
assert!(
report.skills.is_empty(),
"FIFO extra-path package dir must not load nested SKILL.md: {:?}",
report
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("wanted/SKILL.md")
&& s.name.is_none()
&& s.detail.contains("regular file")
}),
"package-dir FIFO SKILL.md must be unreadable, not a silent miss: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"FIFO wanted/SKILL.md must not become a root_file skip: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let msg = unknown_or_skipped_skill_message("wanted", &report.skips);
assert!(
msg.contains("skipped skill: wanted"),
"FIFO extra-path package dir must not look missing: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
let why = crate::why(&report, Some("wanted"), None, None);
assert!(why.loaded.is_empty(), "why loaded={:?}", why.loaded);
assert_eq!(why.skips.len(), 1, "why skips={:?}", why.skips);
assert_eq!(why.skips[0].kind, SkipKind::Unreadable);
assert!(why.unknown_skill_message().is_none());
let validated = validate_path(&fifo);
assert!(!validated.ok, "validate must reject FIFO: {validated:?}");
let vskip = validated.skip.expect("validate skip");
assert_eq!(vskip.kind, SkipKind::Unreadable);
assert!(
vskip.detail.contains("regular file"),
"validate/discover must agree: {}",
vskip.detail
);
}
#[cfg(unix)]
#[test]
fn extra_path_symlink_to_fifo_is_unreadable_not_hang() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
let fifo = extra.path().join("blocked.fifo");
mkfifo(&fifo);
std::os::unix::fs::symlink(&fifo, pkg.join("SKILL.md")).expect("symlink");
let report = discover_extra_path_with_timeout(
extra.path().to_path_buf(),
&fifo,
"discover must not block on symlink-to-FIFO SKILL.md",
);
assert!(
report.skills.iter().all(|s| s.name != "demo"),
"symlink-to-FIFO must not load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable && s.path.ends_with("demo/SKILL.md") && s.name.is_none()
}),
"symlink-to-FIFO must be unreadable, not a silent miss: {:?}",
report.skips
);
let msg = unknown_or_skipped_skill_message("demo", &report.skips);
assert!(
msg.contains("skipped skill: demo"),
"symlink-to-FIFO package must not look unknown: {msg}"
);
}
#[cfg(unix)]
#[test]
fn extra_path_unix_socket_skill_md_is_unreadable_not_hang() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
let sock = pkg.join("SKILL.md");
let _listener = std::os::unix::net::UnixListener::bind(&sock).expect("bind unix socket");
let cwd = tempfile::tempdir().expect("cwd");
let home = tempfile::tempdir().expect("home");
let extra_path = extra.path().to_path_buf();
let cwd_path = cwd.path().to_path_buf();
let home_path = home.path().to_path_buf();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let report = with_home_override(Some(home_path), || {
discover(
&cwd_path,
&DiscoveryOptions {
paths: vec![extra_path.display().to_string()],
..DiscoveryOptions::default()
},
)
});
let _ = tx.send(report);
});
let report = match rx.recv_timeout(std::time::Duration::from_secs(2)) {
Ok(r) => r,
Err(_) => panic!("discover must not block on unix-socket SKILL.md"),
};
assert!(
report.skills.iter().all(|s| s.name != "demo"),
"socket SKILL.md must not load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("demo/SKILL.md")
&& s.name.is_none()
&& s.detail.contains("regular file")
}),
"unix-socket SKILL.md must be unreadable, not a silent miss: {:?}",
report.skips
);
let msg = unknown_or_skipped_skill_message("demo", &report.skips);
assert!(
msg.contains("skipped skill: demo"),
"socket package must not look unknown: {msg}"
);
}
#[cfg(unix)]
#[test]
fn extra_path_symlink_to_dev_zero_is_unreadable_not_unbounded() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("demo");
fs::create_dir_all(&pkg).expect("mkdir");
std::os::unix::fs::symlink("/dev/zero", pkg.join("SKILL.md")).expect("symlink");
let cwd = tempfile::tempdir().expect("cwd");
let home = tempfile::tempdir().expect("home");
let extra_path = extra.path().to_path_buf();
let cwd_path = cwd.path().to_path_buf();
let home_path = home.path().to_path_buf();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let report = with_home_override(Some(home_path), || {
discover(
&cwd_path,
&DiscoveryOptions {
paths: vec![extra_path.display().to_string()],
..DiscoveryOptions::default()
},
)
});
let _ = tx.send(report);
});
let report = match rx.recv_timeout(std::time::Duration::from_secs(2)) {
Ok(r) => r,
Err(_) => panic!("discover must not block on /dev/zero SKILL.md"),
};
assert!(
report.skills.iter().all(|s| s.name != "demo"),
"/dev/zero SKILL.md must not load: {:?}",
report.skills
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("demo/SKILL.md")
&& s.name.is_none()
&& (s.detail.contains("regular file") || s.detail.contains("escapes"))
}),
"symlink to /dev/zero must be unreadable, not an unbounded read: {:?}",
report.skips
);
let msg = unknown_or_skipped_skill_message("demo", &report.skips);
assert!(
msg.contains("skipped skill: demo"),
"/dev/zero package must not look unknown: {msg}"
);
}
#[cfg(unix)]
#[test]
fn skills_root_symlink_escape_is_unreadable() {
let root = tempfile::tempdir().expect("tmp");
let outside = tempfile::tempdir().expect("out");
write_skill(&outside.path().join("stolen"), "stolen", "SECRET_BODY");
let agents = root.path().join(".agents");
fs::create_dir_all(&agents).expect("mkdir");
std::os::unix::fs::symlink(outside.path(), agents.join("skills")).expect("symlink root");
let report = empty_home_discover(root.path(), &DiscoveryOptions::default());
assert!(
report.skills.iter().all(|s| s.name != "stolen"),
"skills-root symlink must not load the escaped tree: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::Unreadable && s.detail.contains("escapes") }),
"skills-root escape must be an unreadable skip: {:?}",
report.skips
);
}
#[cfg(unix)]
#[test]
fn extra_path_scan_does_not_follow_package_symlink() {
let cwd = tempfile::tempdir().expect("cwd");
let outside = tempfile::tempdir().expect("out");
write_skill(&outside.path().join("stolen"), "stolen", "SECRET_BODY");
let scan = tempfile::tempdir().expect("scan");
std::os::unix::fs::symlink(outside.path().join("stolen"), scan.path().join("stolen"))
.expect("symlink");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![scan.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(
report.skills.iter().all(|s| s.name != "stolen"),
"extra-path scan must not follow package dir symlink: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::Unreadable && s.detail.contains("escapes") }),
"skips={:?}",
report.skips
);
}
#[cfg(unix)]
#[test]
fn extra_path_explicit_package_symlink_still_loads() {
let cwd = tempfile::tempdir().expect("cwd");
let outside = tempfile::tempdir().expect("out");
write_skill(&outside.path().join("wanted"), "wanted", "host asked");
let link = tempfile::tempdir().expect("link");
let pkg = link.path().join("wanted");
std::os::unix::fs::symlink(outside.path().join("wanted"), &pkg).expect("symlink");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(report.skills.len(), 1, "skills={:?}", report.skills);
assert_eq!(report.skills[0].name, "wanted");
assert_eq!(report.skills[0].source, SkillSource::ExtraPath);
assert!(report.skips.is_empty(), "skips={:?}", report.skips);
}
#[cfg(unix)]
#[test]
fn extra_path_explicit_skill_md_file_symlink_still_loads() {
let cwd = tempfile::tempdir().expect("cwd");
let outside = tempfile::tempdir().expect("out");
write_skill(&outside.path().join("wanted"), "wanted", "host asked");
let link = tempfile::tempdir().expect("link");
let pkg = link.path().join("wanted");
fs::create_dir_all(&pkg).expect("mkdir");
let file = pkg.join("SKILL.md");
std::os::unix::fs::symlink(outside.path().join("wanted").join("SKILL.md"), &file)
.expect("symlink");
let opts = DiscoveryOptions {
paths: vec![file.display().to_string()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"explicit extra-path SKILL.md file symlink must load: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(report.skills[0].source, SkillSource::ExtraPath);
assert_eq!(report.skills[0].content.trim(), "host asked");
assert!(report.skips.is_empty(), "skips={:?}", report.skips);
let loaded = find_skill_by_name(&report.skills, "wanted").expect("wanted");
assert_eq!(loaded.content.trim(), "host asked");
let why = crate::why(&report, Some("wanted"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, &file),
"watch_dirs must list the extra-path SKILL.md file: {dirs:?}"
);
}
#[cfg(unix)]
#[test]
fn extra_path_skills_subdir_symlink_escape_is_unreadable() {
let cwd = tempfile::tempdir().expect("cwd");
let outside = tempfile::tempdir().expect("out");
write_skill(&outside.path().join("stolen"), "stolen", "SECRET_BODY");
let extra = tempfile::tempdir().expect("extra");
std::os::unix::fs::symlink(outside.path(), extra.path().join("skills")).expect("symlink");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(
report.skills.iter().all(|s| s.name != "stolen"),
"extra-path skills/ symlink must not load the escaped tree: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("SECRET_BODY")),
"escaped skill body must not be loaded: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::Unreadable && s.detail.contains("escapes") }),
"skills-subdir escape must be an unreadable skip: {:?}",
report.skips
);
}
#[cfg(unix)]
#[test]
fn extra_path_skills_subdir_symlink_escape_does_not_hide_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let outside = tempfile::tempdir().expect("out");
write_skill(&outside.path().join("stolen"), "stolen", "SECRET_BODY");
let extra = tempfile::tempdir().expect("extra");
std::os::unix::fs::symlink(outside.path(), extra.path().join("skills")).expect("symlink");
write_skill(&extra.path().join("public"), "public", "from-sibling");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling public must still load when extra/skills/ escapes: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report.skills.iter().all(|s| s.name != "stolen"),
"must not load the escaped skills/ tree: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("SECRET_BODY")),
"escaped skill body must not be loaded: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::Unreadable && s.detail.contains("escapes") }),
"skills-subdir escape must stay an unreadable skip: {:?}",
report.skips
);
let loaded = find_skill_by_name(&report.skills, "public").expect("public");
assert_eq!(loaded.content.trim(), "from-sibling");
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[cfg(unix)]
#[test]
fn extra_path_escaped_skills_and_leftover_skill_md_does_not_hide_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let outside = tempfile::tempdir().expect("out");
write_skill(&outside.path().join("stolen"), "stolen", "SECRET_BODY");
let extra = tempfile::tempdir().expect("extra");
fs::write(
extra.path().join("SKILL.md"),
"---\nname: loose\ndescription: leftover\n---\nloose\n",
)
.expect("write");
std::os::unix::fs::symlink(outside.path(), extra.path().join("skills")).expect("symlink");
write_skill(&extra.path().join("public"), "public", "from-sibling");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling public must still load when leftover SKILL.md and extra/skills/ escape: {:?}",
report
);
assert!(
report.skills.iter().all(|s| s.name != "stolen"),
"must not load the escaped skills/ tree: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::RootFile && s.name.as_deref() == Some("loose")),
"leftover extra-path SKILL.md must stay root_file: {:?}",
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::Unreadable && s.detail.contains("escapes") }),
"skills-subdir escape must stay unreadable: {:?}",
report.skips
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1);
assert!(why.unknown_skill_message().is_none());
let why_loose = crate::why(&report, Some("loose"), None, None);
assert!(why_loose.loaded.is_empty());
assert_eq!(why_loose.skips[0].kind, SkipKind::RootFile);
assert!(why_loose.unknown_skill_message().is_none());
}
#[cfg(unix)]
#[test]
fn extra_path_unreadable_skills_subdir_does_not_hide_sibling() {
use std::os::unix::fs::PermissionsExt;
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let skills = extra.path().join("skills");
fs::create_dir_all(&skills).expect("mkdir");
write_skill(&skills.join("hidden"), "hidden", "locked");
write_skill(&extra.path().join("public"), "public", "from-sibling");
let original = fs::metadata(&skills).expect("meta").permissions();
struct Restore<'a>(&'a std::path::Path, fs::Permissions);
impl Drop for Restore<'_> {
fn drop(&mut self) {
let _ = fs::set_permissions(self.0, self.1.clone());
}
}
let _restore = Restore(&skills, original.clone());
let mut locked = original.clone();
locked.set_mode(0o000);
fs::set_permissions(&skills, locked).expect("chmod");
if fs::read_dir(&skills).is_ok() {
return;
}
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling public must still load when extra/skills/ is unreadable: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report.skills.iter().all(|s| s.name != "hidden"),
"must not load packages inside unreadable extra/skills/: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::Unreadable && s.path == skills),
"unreadable extra/skills/ must be a skip row: {:?}",
report.skips
);
let loaded = find_skill_by_name(&report.skills, "public").expect("public");
assert_eq!(loaded.content.trim(), "from-sibling");
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn extra_path_oversized_skill_md_does_not_scan_nested_skill() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
fs::create_dir_all(&pkg).expect("mkdir");
let mut huge = String::from("---\nname: wanted\ndescription: huge\n---\n");
huge.push_str(&"x".repeat(SKILL_MD_MAX_BYTES as usize + 1));
fs::write(pkg.join("SKILL.md"), huge).expect("write");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"oversized extra-path package must not load nested SKILL.md: {:?}",
report
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::Unreadable && s.path.ends_with("wanted/SKILL.md")),
"oversized SKILL.md must stay an unreadable package skip: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"oversized wanted/SKILL.md must not become a root_file skip: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let msg = unknown_or_skipped_skill_message("wanted", &report.skips);
assert!(
msg.contains("skipped skill: wanted"),
"oversized extra-path package must not look missing: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
let why = crate::why(&report, Some("wanted"), None, None);
assert!(why.loaded.is_empty(), "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[cfg(unix)]
#[test]
fn extra_path_unreadable_skill_md_does_not_scan_nested_skill() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let skill_file = pkg.join("SKILL.md");
let mut perms = fs::metadata(&skill_file).expect("meta").permissions();
use std::os::unix::fs::PermissionsExt;
perms.set_mode(0o000);
fs::set_permissions(&skill_file, perms).expect("chmod");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
..DiscoveryOptions::default()
},
);
let _ = fs::set_permissions(&skill_file, fs::Permissions::from_mode(0o644));
assert!(
report.skills.is_empty(),
"unreadable extra-path package must not load nested SKILL.md: {:?}",
report
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::Unreadable && s.path.ends_with("wanted/SKILL.md")),
"unreadable SKILL.md must stay an unreadable package skip: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"unreadable wanted/SKILL.md must not become a root_file skip: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let msg = unknown_or_skipped_skill_message("wanted", &report.skips);
assert!(
msg.contains("skipped skill: wanted"),
"unreadable extra-path package must not look missing: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
let why = crate::why(&report, Some("wanted"), None, None);
assert!(why.loaded.is_empty(), "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn extra_path_dot_or_dotdot_frontmatter_name_does_not_scan_nested() {
for peeked in [".", "..", ".", "‥", "․", "﹒", "︰"] {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("SKILL.md"),
format!("---\nname: {peeked}\ndescription: path-like name\n---\nPACKAGE_BODY\n"),
)
.expect("write");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![pkg.display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"name `{peeked}` must not prove a loose collection: {:?}",
report
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load for name `{peeked}`: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::ParseError && s.path.ends_with("wanted/SKILL.md") }),
"name `{peeked}` must stay a parse_error package skip: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"wanted/SKILL.md with name `{peeked}` must not become a root_file skip: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let msg = unknown_or_skipped_skill_message("wanted", &report.skips);
assert!(
msg.contains("skipped skill: wanted"),
"peeked `{peeked}` is a path component, not the package name: {msg}"
);
assert!(
!msg.contains(&format!("skipped skill: {peeked}")),
"load must not present peeked `{peeked}` as the skill name: {msg}"
);
assert!(msg.contains("parse_error"), "msg={msg}");
let skip_path = report.skips[0].path.display().to_string();
assert!(
msg.contains(&skip_path),
"load must name the SKILL.md path: msg={msg} path={skip_path}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
let why = crate::why(&report, Some("wanted"), None, None);
assert!(why.loaded.is_empty(), "why loaded={:?}", why.loaded);
assert!(
why.unknown_skill_message().is_none(),
"why wanted must find the package skip, not unknown: {:?}",
why.unknown_skill_message()
);
}
}
#[test]
fn load_extra_path_dot_parse_skip_includes_joined_path() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("SKILL.md"),
"---\ndescription: no name\n---\nPACKAGE_BODY\n",
)
.expect("write");
let report = empty_home_discover(
&pkg,
&DiscoveryOptions {
paths: vec![".".to_owned()],
..DiscoveryOptions::default()
},
);
assert_eq!(report.skips.len(), 1, "skips={:?}", report.skips);
assert_eq!(report.skips[0].kind, SkipKind::ParseError);
let msg = unknown_or_skipped_skill_message("wanted", &report.skips);
assert!(
msg.contains("skipped skill: wanted"),
"extra-path `.` package must stay wanted: {msg}"
);
assert!(msg.contains("parse_error"), "msg={msg}");
let skip_path = report.skips[0].path.display().to_string();
assert!(
skip_path.contains("wanted"),
"skip path must be the joined discover cwd, not raw `.`: {skip_path}"
);
assert!(
msg.contains(&skip_path),
"load/MCP must name the joined SKILL.md after extra-path `.`: msg={msg} path={skip_path}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
let why = crate::why(&report, Some("wanted"), None, None);
assert_eq!(why.skips.len(), 1);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn extra_path_nfkc_dot_argument_joins_like_ascii_dot() {
for raw in [".", "․", "﹒"] {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let report = empty_home_discover(
&pkg,
&DiscoveryOptions {
paths: vec![raw.to_owned()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"extra-path `{raw}` must join cwd like ASCII `.`: {:?}",
report
);
assert_eq!(report.skills[0].content.trim(), "PACKAGE_BODY");
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load from extra-path `{raw}`: {:?}",
report.skills
);
assert!(find_skill_by_name(&report.skills, "wanted").is_some());
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let why = crate::why(&report, Some("wanted"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
}
#[test]
fn extra_path_nfkc_dotdot_argument_joins_like_ascii_dotdot() {
for raw in ["‥", "︰", "..", "․․"] {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let report = empty_home_discover(
&pkg.join("evil"),
&DiscoveryOptions {
paths: vec![raw.to_owned()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"extra-path `{raw}` must join like ASCII `..`: {:?}",
report
);
assert_eq!(report.skills[0].content.trim(), "PACKAGE_BODY");
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load from extra-path `{raw}`: {:?}",
report.skills
);
assert!(find_skill_by_name(&report.skills, "wanted").is_some());
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let why = crate::why(&report, Some("wanted"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
}
#[test]
fn extra_path_nfkc_dot_component_in_joined_path() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let via_fullwidth = pkg.join("evil").join("‥");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![via_fullwidth.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["wanted"],
"wanted/evil/‥ must be the wanted package: {:?}",
report
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load from wanted/evil/‥: {:?}",
report.skills
);
assert!(find_skill_by_name(&report.skills, "wanted").is_some());
assert!(find_skill_by_name(&report.skills, "evil").is_none());
}
#[test]
fn ignore_nfkc_dotdot_skips_extra_path_sibling_package() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
write_skill(&extra.path().join("public"), "public", "keep");
let extra_root = extra.path().display().to_string();
let via_fullwidth = pkg.join("evil").join("‥").display().to_string();
let loaded = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra_root.clone()],
..DiscoveryOptions::default()
},
);
assert!(
find_skill_by_name(&loaded.skills, "wanted").is_some(),
"collection extra-path must load wanted: {:?}",
loaded.skills
);
assert!(find_skill_by_name(&loaded.skills, "public").is_some());
let ignored = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra_root],
ignore: vec![via_fullwidth.clone()],
..DiscoveryOptions::default()
},
);
assert!(
ignored.skills.iter().all(|s| s.name != "wanted"),
"ignore `{via_fullwidth}` must collapse like extra-path and skip wanted: {:?}",
ignored.skills
);
assert!(
find_skill_by_name(&ignored.skills, "public").is_some(),
"sibling extra-path package must still load: {:?}",
ignored.skills
);
}
#[test]
fn extra_path_and_ignore_nfkc_dotdot_argument_agree() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
write_skill(&pkg, "wanted", "PACKAGE_BODY");
write_skill(&pkg.join("evil"), "evil", "NESTED_SECRET");
let child = pkg.join("evil");
let loaded = empty_home_discover(
&child,
&DiscoveryOptions {
paths: vec!["‥".to_owned()],
..DiscoveryOptions::default()
},
);
assert!(
find_skill_by_name(&loaded.skills, "wanted").is_some(),
"extra-path ‥ must load wanted: {:?}",
loaded.skills
);
let ignored = empty_home_discover(
&child,
&DiscoveryOptions {
paths: vec!["‥".to_owned()],
ignore: vec!["‥".to_owned()],
..DiscoveryOptions::default()
},
);
assert!(
ignored.skills.iter().all(|s| s.name != "wanted"),
"ignore ‥ must skip the extra-path ‥ package: {:?}",
ignored.skills
);
}
#[test]
fn load_extra_path_dotdot_parse_skip_includes_joined_path() {
let extra = tempfile::tempdir().expect("extra");
let pkg = extra.path().join("wanted");
fs::create_dir_all(&pkg).expect("mkdir");
fs::write(
pkg.join("SKILL.md"),
"---\ndescription: no name\n---\nPACKAGE_BODY\n",
)
.expect("write");
let child = pkg.join("evil");
fs::create_dir_all(&child).expect("mkdir");
let report = empty_home_discover(
&child,
&DiscoveryOptions {
paths: vec!["..".to_owned()],
..DiscoveryOptions::default()
},
);
assert_eq!(report.skips.len(), 1, "skips={:?}", report.skips);
assert_eq!(report.skips[0].kind, SkipKind::ParseError);
let msg = unknown_or_skipped_skill_message("wanted", &report.skips);
assert!(
msg.contains("skipped skill: wanted"),
"extra-path `..` package must stay wanted: {msg}"
);
assert!(msg.contains("parse_error"), "msg={msg}");
let skip_path = report.skips[0].path.display().to_string();
assert!(
skip_path.contains("wanted"),
"skip path must be the joined discover cwd, not raw `..`: {skip_path}"
);
assert!(
msg.contains(&skip_path),
"load/MCP must name the joined SKILL.md after extra-path `..`: msg={msg} path={skip_path}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
}
#[test]
fn empty_extra_path_does_not_scan_discover_cwd() {
let cwd = tempfile::tempdir().expect("cwd");
write_skill(&cwd.path().join("planted"), "planted", "FROM_CWD");
for raw in ["", " "] {
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![raw.to_owned()],
..DiscoveryOptions::default()
},
);
assert!(
find_skill_by_name(&report.skills, "planted").is_none(),
"extra-path {raw:?} must not scan discover cwd: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("FROM_CWD")),
"extra-path {raw:?} must not load cwd package body: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| s.source != SkillSource::ExtraPath),
"extra-path {raw:?} must not become ExtraPath: {:?}",
report.skills
);
}
}
#[test]
fn empty_ignore_does_not_hide_discover_cwd() {
let cwd = tempfile::tempdir().expect("cwd");
write_skill(
&cwd.path().join(".agents").join("skills").join("keep"),
"keep",
"KEEP_BODY",
);
for raw in ["", " "] {
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
ignore: vec![raw.to_owned()],
..DiscoveryOptions::default()
},
);
assert!(
find_skill_by_name(&report.skills, "keep").is_some(),
"ignore {raw:?} must not hide cwd .agents skills: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.any(|s| s.content.contains("KEEP_BODY")),
"ignore {raw:?} must not drop cwd skill body: {:?}",
report.skills
);
}
}
#[test]
fn ignore_line_separator_dotdot_does_not_hide_discover_cwd() {
let cwd = tempfile::tempdir().expect("cwd");
let agents = cwd.path().join(".agents").join("skills");
write_skill(&agents.join("keep"), "keep", "KEEP_BODY");
for raw in [
"evil\n/..",
"evil\nfoo/..",
"evil\r/..",
"evil\u{2028}/..",
"evil\u{2029}/..",
"\n/..",
] {
let opts = DiscoveryOptions {
ignore: vec![raw.to_owned()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert!(
find_skill_by_name(&report.skills, "keep").is_some(),
"ignore {raw:?} must not hide cwd .agents skills: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.any(|s| s.content.contains("KEEP_BODY")),
"ignore {raw:?} must not drop cwd skill body: {:?}",
report.skills
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, &agents),
"ignore {raw:?} must not omit cwd .agents/skills from watch: {dirs:?}"
);
}
}
#[test]
fn ignore_whitespace_dotdot_does_not_hide_discover_cwd() {
let cwd = tempfile::tempdir().expect("cwd");
let agents = cwd.path().join(".agents").join("skills");
write_skill(&agents.join("keep"), "keep", "KEEP_BODY");
for raw in [
"evil /..",
" /..",
"\t/..",
"\u{0b}/..",
"\u{0c}/..",
"\u{85}/..",
"\u{00a0}/..",
"evil\u{85}/..",
"evil\u{0b}/..",
"/ ..",
"/\t..",
"/\u{00a0}..",
] {
let opts = DiscoveryOptions {
ignore: vec![raw.to_owned()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert!(
find_skill_by_name(&report.skills, "keep").is_some(),
"ignore {raw:?} must not hide cwd .agents skills: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.any(|s| s.content.contains("KEEP_BODY")),
"ignore {raw:?} must not drop cwd skill body: {:?}",
report.skills
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, &agents),
"ignore {raw:?} must not omit cwd .agents/skills from watch: {dirs:?}"
);
}
}
#[test]
fn extra_path_whitespace_dotdot_does_not_scan_filesystem_root() {
let cwd = tempfile::tempdir().expect("cwd");
let home = tempfile::tempdir().expect("home");
for raw in [
" /..",
"\t/..",
"\u{85}/..",
"\u{00a0}/..",
"/ ..",
"/\t..",
"/\u{00a0}..",
] {
let opts = DiscoveryOptions {
paths: vec![raw.to_owned()],
implicit_roots: false,
..DiscoveryOptions::default()
};
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(cwd.path(), &opts)
});
assert!(
report.skills.is_empty(),
"extra-path {raw:?} must not load root packages: {:?}",
report.skills
);
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
dirs.is_empty(),
"extra-path {raw:?} must not watch a collapsed token (same as discover): {dirs:?}"
);
}
}
#[test]
fn user_dir_whitespace_dotdot_does_not_scan_filesystem_root() {
let cwd = tempfile::tempdir().expect("cwd");
let home = tempfile::tempdir().expect("home");
for raw in [
" /..",
"\t/..",
"\u{85}/..",
"\u{00a0}/..",
"/ ..",
"/\t..",
"/\u{00a0}..",
] {
let opts = DiscoveryOptions {
user_skills_dir: Some(PathBuf::from(raw)),
implicit_roots: false,
..DiscoveryOptions::default()
};
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(cwd.path(), &opts)
});
assert!(
report.skills.is_empty(),
"user_dir {raw:?} must not load root packages: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::Unreadable && s.detail.contains("collapses") }),
"user_dir {raw:?} must skip as collapse, not walk /: {:?}",
report.skips
);
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
dirs.is_empty(),
"user_dir {raw:?} must not watch a collapsed token (same as extra-path): {dirs:?}"
);
}
}
#[test]
fn why_load_whitespace_extra_path_does_not_scan_filesystem_root() {
let cwd = tempfile::tempdir().expect("cwd");
let home = tempfile::tempdir().expect("home");
for raw in [
" /..",
"\t/..",
"\u{85}/..",
"\u{00a0}/..",
"/ ..",
"/\t..",
"/\u{00a0}..",
] {
let opts = DiscoveryOptions {
paths: vec![raw.to_owned()],
implicit_roots: false,
..DiscoveryOptions::default()
};
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(cwd.path(), &opts)
});
assert!(
report.skills.is_empty(),
"extra-path {raw:?} must not load root packages: {:?}",
report.skills
);
assert!(
find_skill_by_name(&report.skills, "demo").is_none(),
"load must not find demo via extra-path {raw:?}: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::Unreadable && s.detail.contains("collapses") }),
"extra-path {raw:?} must skip as collapse, not walk /: {:?}",
report.skips
);
let why_named = crate::why(&report, Some("demo"), None, None);
assert!(
why_named.loaded.is_empty(),
"why demo must not load via extra-path {raw:?}: {:?}",
why_named.loaded
);
let why_all = crate::why(&report, None, None, None);
assert!(
why_all.loaded.is_empty(),
"why unfiltered must not load via extra-path {raw:?}: {:?}",
why_all.loaded
);
assert!(
why_all
.skips
.iter()
.any(|s| s.kind == SkipKind::Unreadable && s.detail.contains("collapses")),
"why must keep the collapse skip for extra-path {raw:?}: {:?}",
why_all.skips
);
let miss = unknown_or_skipped_skill("demo", &report.skips);
assert_eq!(
miss.error_kind, "unreadable",
"named load must peel the nameless collapse refuse: {miss:?}"
);
assert!(miss.path.is_some(), "named load must peel WHERE: {miss:?}");
assert!(
miss.error.contains("--path"),
"named load must name --path: {}",
miss.error
);
}
}
#[test]
fn collapse_refuse_names_flag_and_peels_named_miss() {
let cwd = tempfile::tempdir().expect("cwd");
let home = tempfile::tempdir().expect("home");
let extra = with_home_override(Some(home.path().to_path_buf()), || {
discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![" /..".to_owned()],
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
let extra_skip = extra
.skips
.iter()
.find(|s| s.kind == SkipKind::Unreadable && s.detail.contains("collapses"))
.expect("extra-path collapse skip");
assert!(
extra_skip.detail.contains("--path") && extra_skip.detail.contains("paths"),
"extra-path collapse must name --path / paths: {}",
extra_skip.detail
);
let extra_miss = unknown_or_skipped_skill("demo", &extra.skips);
assert_eq!(
extra_miss.error_kind, "unreadable",
"named load must peel the refuse, not unknown_skill: {extra_miss:?}"
);
assert!(
extra_miss.path.is_some(),
"named load must peel path so the host sees WHERE: {extra_miss:?}"
);
assert!(
extra_miss.error.contains("--path") && extra_miss.error.contains("paths"),
"named load error must name --path / paths: {}",
extra_miss.error
);
let user = with_home_override(Some(home.path().to_path_buf()), || {
discover(
cwd.path(),
&DiscoveryOptions {
user_skills_dir: Some(PathBuf::from(" /..")),
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
let user_skip = user
.skips
.iter()
.find(|s| s.kind == SkipKind::Unreadable && s.detail.contains("collapses"))
.expect("user_dir collapse skip");
assert!(
user_skip.detail.contains("--user-dir") && user_skip.detail.contains("user_dir"),
"user_dir collapse must name --user-dir / user_dir: {}",
user_skip.detail
);
let user_miss = unknown_or_skipped_skill("demo", &user.skips);
assert_eq!(
user_miss.error_kind, "unreadable",
"named load must peel the user_dir refuse: {user_miss:?}"
);
assert!(
user_miss.error.contains("--user-dir") && user_miss.error.contains("user_dir"),
"named load error must name --user-dir / user_dir: {}",
user_miss.error
);
let why_named = crate::why(&extra, Some("demo"), None, None);
let why_miss = why_named.unknown_skill_miss().expect("named why miss");
assert_eq!(
why_miss.error_kind, "unreadable",
"named why must peel the refuse, not drop the skip: {why_miss:?}"
);
assert!(
why_miss.path.is_some(),
"named why must peel path: {why_miss:?}"
);
assert!(
why_miss.error.contains("--path"),
"named why error must name --path: {}",
why_miss.error
);
}
fn assert_missing_host_path_skip(skip: &SkillSkip, shown: &str, flag_a: &str, flag_b: &str) {
assert_eq!(skip.kind, SkipKind::Unreadable, "{skip:?}");
assert!(skip.name.is_none(), "host miss is nameless: {skip:?}");
assert!(
skip.detail.contains("path does not exist:"),
"must name the hole: {}",
skip.detail
);
assert!(
skip.detail.contains(shown),
"must echo the sanitized path: {}",
skip.detail
);
assert!(
skip.detail.contains(flag_a) && skip.detail.contains(flag_b),
"must name the flag/field: {}",
skip.detail
);
assert!(
!skip.detail.contains("os error 2") && !skip.detail.contains("No such file"),
"must not leak the raw OS string: {}",
skip.detail
);
assert_eq!(skip.detail.lines().count(), 1, "{}", skip.detail);
}
#[test]
fn extra_path_missing_dir_is_unreadable_not_silent() {
let cwd = tempfile::tempdir().expect("cwd");
let root = tempfile::tempdir().expect("root");
let missing = root.path().join("no-such-extra");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![missing.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"missing extra-path must load zero skills: {:?}",
report.skills
);
assert_eq!(
report.skips.len(),
1,
"missing extra-path must be one skip: {:?}",
report.skips
);
let shown = crate::sanitize_error_token(&missing.display().to_string());
assert_missing_host_path_skip(&report.skips[0], &shown, "--path", "paths");
}
#[test]
fn user_dir_missing_is_unreadable_not_silent() {
let cwd = tempfile::tempdir().expect("cwd");
let root = tempfile::tempdir().expect("root");
let missing = root.path().join("no-such-user");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
user_skills_dir: Some(missing.clone()),
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"missing user_dir must load zero skills: {:?}",
report.skills
);
assert_eq!(
report.skips.len(),
1,
"missing user_dir must be one skip: {:?}",
report.skips
);
let shown = crate::sanitize_error_token(&missing.display().to_string());
assert_missing_host_path_skip(&report.skips[0], &shown, "--user-dir", "user_dir");
}
#[test]
fn extra_path_regular_file_not_skill_md_is_unreadable() {
let cwd = tempfile::tempdir().expect("cwd");
let root = tempfile::tempdir().expect("root");
let file = root.path().join("notes.txt");
fs::write(&file, "not a skill").expect("write");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![file.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"non-SKILL.md extra-path file must load zero skills: {:?}",
report.skills
);
let skip = report
.skips
.iter()
.find(|s| s.kind == SkipKind::Unreadable)
.expect("unreadable skip");
assert!(skip.name.is_none(), "file miss is nameless: {skip:?}");
assert!(
skip.detail.contains("not a directory") || skip.detail.contains("unreadable"),
"must say the extra-path is not a directory: {}",
skip.detail
);
let shown = crate::sanitize_error_token(&file.display().to_string());
assert!(
skip.detail.contains(&shown),
"must echo the sanitized path: {}",
skip.detail
);
assert_eq!(skip.detail.lines().count(), 1, "{}", skip.detail);
}
#[test]
fn host_path_through_a_file_says_missing_not_unreadable() {
let cwd = tempfile::tempdir().expect("cwd");
let parent = tempfile::tempdir().expect("parent");
let file = parent.path().join("notdir");
fs::write(&file, "x").expect("write");
let through = file.join("skills");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![through.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"a path through a file must load zero skills: {:?}",
report.skills
);
assert_eq!(report.skips.len(), 1, "{:?}", report.skips);
let shown = crate::sanitize_error_token(&through.display().to_string());
assert_missing_host_path_skip(&report.skips[0], &shown, "--path", "paths");
assert!(
!report.skips[0].detail.contains("unreadable")
&& !report.skips[0].detail.contains("os error 20")
&& !report.skips[0].detail.contains("Not a directory"),
"must not call a missing path unreadable: {}",
report.skips[0].detail
);
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
user_skills_dir: Some(through.clone()),
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(report.skills.is_empty(), "{:?}", report.skills);
assert_eq!(report.skips.len(), 1, "{:?}", report.skips);
assert_missing_host_path_skip(&report.skips[0], &shown, "--user-dir", "user_dir");
assert!(
!report.skips[0].detail.contains("unreadable"),
"user_dir through a file is missing, not unreadable: {}",
report.skips[0].detail
);
}
fn assert_implicit_nondirectory_is_silent(
report: &crate::skip::DiscoveryReport,
kept: Option<&str>,
) {
if let Some(name) = kept {
assert!(
report.skills.iter().any(|s| s.name == name),
"project skill must stay: skills={:?} skips={:?}",
report.skills,
report.skips
);
}
assert!(
report.skips.is_empty(),
"a file where an implicit skills directory belongs is not a tree: {:?}",
report.skips
);
}
#[test]
fn implicit_nondirectory_skill_roots_stay_silent() {
let cwd = tempfile::tempdir().expect("cwd");
fs::write(cwd.path().join(".agents"), "not a directory").expect("write");
assert_implicit_nondirectory_is_silent(
&empty_home_discover(cwd.path(), &DiscoveryOptions::default()),
None,
);
let cwd = tempfile::tempdir().expect("cwd");
fs::create_dir_all(cwd.path().join(".agents")).expect("mkdir");
fs::write(cwd.path().join(".agents").join("skills"), "not a directory").expect("write");
assert_implicit_nondirectory_is_silent(
&empty_home_discover(cwd.path(), &DiscoveryOptions::default()),
None,
);
let claude = DiscoveryOptions {
vendor_roots: vec!["claude".to_owned()],
..DiscoveryOptions::default()
};
let cwd = tempfile::tempdir().expect("cwd");
write_skill(
&cwd.path().join(".agents").join("skills").join("fromrel"),
"fromrel",
"body",
);
fs::write(cwd.path().join(".claude"), "not a directory").expect("write");
assert_implicit_nondirectory_is_silent(
&empty_home_discover(cwd.path(), &claude),
Some("fromrel"),
);
let cwd = tempfile::tempdir().expect("cwd");
write_skill(
&cwd.path().join(".agents").join("skills").join("fromrel"),
"fromrel",
"body",
);
fs::create_dir_all(cwd.path().join(".claude")).expect("mkdir");
fs::write(cwd.path().join(".claude").join("skills"), "not a directory").expect("write");
assert_implicit_nondirectory_is_silent(
&empty_home_discover(cwd.path(), &claude),
Some("fromrel"),
);
let cwd = tempfile::tempdir().expect("cwd");
write_skill(
&cwd.path().join(".agents").join("skills").join("fromrel"),
"fromrel",
"body",
);
let home = tempfile::tempdir().expect("home");
fs::write(home.path().join(".agents"), "not a directory").expect("write");
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(cwd.path(), &DiscoveryOptions::default())
});
assert_implicit_nondirectory_is_silent(&report, Some("fromrel"));
let cwd = tempfile::tempdir().expect("cwd");
write_skill(
&cwd.path().join(".agents").join("skills").join("fromrel"),
"fromrel",
"body",
);
let home = tempfile::tempdir().expect("home");
fs::write(home.path().join(".claude"), "not a directory").expect("write");
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(cwd.path(), &claude)
});
assert_implicit_nondirectory_is_silent(&report, Some("fromrel"));
}
#[test]
fn extra_path_missing_dir_named_load_why_peels_unreadable() {
let cwd = tempfile::tempdir().expect("cwd");
let root = tempfile::tempdir().expect("root");
let missing = root.path().join("no-such-extra");
let extra = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![missing.display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
let extra_miss = unknown_or_skipped_skill("demo", &extra.skips);
assert_eq!(
extra_miss.error_kind, "unreadable",
"named load must peel the missing extra-path, not unknown_skill: {extra_miss:?}"
);
assert!(
extra_miss.path.is_some(),
"named load must peel path so the host sees WHERE: {extra_miss:?}"
);
assert!(
extra_miss.error.contains("--path") && extra_miss.error.contains("paths"),
"named load error must name --path / paths: {}",
extra_miss.error
);
assert!(
extra_miss.error.contains("unknown skill: demo"),
"refuse must not claim demo was a skipped package: {}",
extra_miss.error
);
assert!(
!extra_miss.error.contains("skipped skill: demo"),
"refuse must not fuse the name into a skip row: {}",
extra_miss.error
);
let user_missing = root.path().join("no-such-user");
let user = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
user_skills_dir: Some(user_missing),
implicit_roots: false,
..DiscoveryOptions::default()
},
);
let user_miss = unknown_or_skipped_skill("demo", &user.skips);
assert_eq!(
user_miss.error_kind, "unreadable",
"named load must peel the missing user_dir, not unknown_skill: {user_miss:?}"
);
assert!(
user_miss.error.contains("--user-dir") && user_miss.error.contains("user_dir"),
"named load error must name --user-dir / user_dir: {}",
user_miss.error
);
let why_named = crate::why(&extra, Some("demo"), None, None);
let why_miss = why_named.unknown_skill_miss().expect("named why miss");
assert_eq!(
why_miss.error_kind, "unreadable",
"named why must peel the missing extra-path, not unknown_skill: {why_miss:?}"
);
}
#[test]
fn empty_user_dir_does_not_scan_discover_cwd() {
let cwd = tempfile::tempdir().expect("cwd");
write_skill(&cwd.path().join("planted"), "planted", "FROM_CWD");
for raw in ["", " "] {
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
user_skills_dir: Some(std::path::PathBuf::from(raw)),
..DiscoveryOptions::default()
},
);
assert!(
find_skill_by_name(&report.skills, "planted").is_none(),
"user_dir {raw:?} must not scan discover cwd as User: {:?}",
report.skills
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("FROM_CWD")),
"user_dir {raw:?} must not load cwd package body: {:?}",
report.skills
);
}
}
#[test]
fn user_dir_nfkc_dotdot_component_collapses_like_extra_path() {
let cwd = tempfile::tempdir().expect("cwd");
let user = tempfile::tempdir().expect("user");
write_skill(&user.path().join("mine"), "mine", "from-user");
write_skill(&user.path().join("evil"), "evil", "NESTED_SECRET");
let via_fullwidth = user.path().join("evil").join("‥");
let opts = DiscoveryOptions {
user_skills_dir: Some(via_fullwidth.clone()),
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert!(
find_skill_by_name(&report.skills, "mine").is_some(),
"user_dir wanted/evil/‥ must collapse like extra-path and load mine: {:?}",
report.skills
);
assert_eq!(
find_skill_by_name(&report.skills, "mine").map(|s| s.source.clone()),
Some(SkillSource::User),
"collapsed user_dir must stay User: {:?}",
report.skills
);
assert!(
find_skill_by_name(&report.skills, "evil").is_some(),
"collapsed user root must still list sibling evil: {:?}",
report.skills
);
let via_path = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![via_fullwidth.display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(
find_skill_by_name(&via_path.skills, "mine").is_some(),
"extra-path wanted/evil/‥ control must load mine: {:?}",
via_path.skills
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, user.path()),
"watch_dirs must list the collapsed user root, not wanted/evil/‥: {dirs:?}"
);
assert!(
!dirs
.iter()
.any(|p| p.as_os_str().to_string_lossy().contains('‥')),
"watch_dirs must not echo NFKC ‥ after collapse: {dirs:?}"
);
}
#[test]
fn user_dir_nfkc_dotdot_argument_joins_like_ascii_dotdot() {
for raw in ["‥", "︰", "..", "․․"] {
let user = tempfile::tempdir().expect("user");
write_skill(&user.path().join("mine"), "mine", "from-user");
write_skill(&user.path().join("evil"), "evil", "NESTED_SECRET");
let child = user.path().join("evil");
fs::create_dir_all(&child).expect("mkdir");
let opts = DiscoveryOptions {
user_skills_dir: Some(PathBuf::from(raw)),
..DiscoveryOptions::default()
};
let report = empty_home_discover(&child, &opts);
assert!(
find_skill_by_name(&report.skills, "mine").is_some(),
"user_dir `{raw}` must join like ASCII `..` and load mine: {:?}",
report.skills
);
assert_eq!(
find_skill_by_name(&report.skills, "mine").map(|s| s.source.clone()),
Some(SkillSource::User),
"user_dir `{raw}` must stay User: {:?}",
report.skills
);
assert!(
find_skill_by_name(&report.skills, "evil").is_some(),
"user_dir `{raw}` must still list sibling evil: {:?}",
report.skills
);
}
}
#[test]
fn user_dir_relative_joins_discover_cwd_like_extra_path() {
let cwd = tempfile::tempdir().expect("cwd");
write_skill(
&cwd.path().join("myskills").join("mine"),
"mine",
"from-cwd",
);
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
user_skills_dir: Some(std::path::PathBuf::from("myskills")),
..DiscoveryOptions::default()
},
);
let loaded = find_skill_by_name(&report.skills, "mine");
assert!(
loaded.is_some(),
"relative user_dir must join discover cwd like extra-path: {:?}",
report.skills
);
assert_eq!(
loaded.map(|s| s.source.clone()),
Some(SkillSource::User),
"joined user_dir must stay User: {:?}",
report.skills
);
let via_path = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec!["myskills".to_owned()],
..DiscoveryOptions::default()
},
);
assert!(
find_skill_by_name(&via_path.skills, "mine").is_some(),
"extra-path myskills control: {:?}",
via_path.skills
);
}
#[cfg(unix)]
#[test]
fn extra_path_fifo_leftover_skill_md_does_not_hide_skills_subdir() {
let extra = tempfile::tempdir().expect("extra");
let fifo = extra.path().join("SKILL.md");
mkfifo(&fifo);
write_skill(
&extra.path().join("skills").join("public"),
"public",
"from-skills",
);
let report = discover_extra_path_with_timeout(
extra.path().to_path_buf(),
&fifo,
"discover must not block on leftover extra-path FIFO SKILL.md",
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load when leftover extra/SKILL.md is a FIFO: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(
report.skills[0].content.trim(),
"from-skills",
"must load the skills/ package, not hang on leftover FIFO SKILL.md: {:?}",
report.skills[0]
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("SKILL.md")
&& s.name.is_none()
&& s.detail.contains("regular file")
}),
"leftover FIFO extra/SKILL.md must be unreadable, not peeked: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"FIFO leftover SKILL.md must not become a root_file peek: {:?}",
report.skips
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let validated = validate_path(&fifo);
assert!(
!validated.ok,
"validate must reject leftover FIFO: {validated:?}"
);
let vskip = validated.skip.expect("validate skip");
assert_eq!(vskip.kind, SkipKind::Unreadable);
assert!(
vskip.detail.contains("regular file"),
"validate/discover must agree leftover FIFO is not a regular file: {}",
vskip.detail
);
}
#[test]
fn extra_path_dir_leftover_skill_md_does_not_hide_sibling() {
let extra = tempfile::tempdir().expect("extra");
fs::create_dir_all(extra.path().join("SKILL.md")).expect("mkdir leftover dir");
write_skill(&extra.path().join("public"), "public", "from-sibling");
let cwd = tempfile::tempdir().expect("cwd");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling public must still load when leftover extra/SKILL.md is a directory: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(
report.skills[0].content.trim(),
"from-sibling",
"must load the sibling package, not hide it behind leftover SKILL.md dir: {:?}",
report.skills[0]
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[cfg(unix)]
#[test]
fn extra_path_fifo_leftover_and_unreadable_skills_does_not_hide_sibling() {
use std::os::unix::fs::PermissionsExt;
let extra = tempfile::tempdir().expect("extra");
let fifo = extra.path().join("SKILL.md");
mkfifo(&fifo);
let skills = extra.path().join("skills");
fs::create_dir_all(&skills).expect("mkdir");
write_skill(&skills.join("hidden"), "hidden", "locked");
write_skill(&extra.path().join("public"), "public", "from-sibling");
let original = fs::metadata(&skills).expect("meta").permissions();
struct Restore<'a>(&'a std::path::Path, fs::Permissions);
impl Drop for Restore<'_> {
fn drop(&mut self) {
let _ = fs::set_permissions(self.0, self.1.clone());
}
}
let _restore = Restore(&skills, original.clone());
let mut locked = original.clone();
locked.set_mode(0o000);
fs::set_permissions(&skills, locked).expect("chmod");
if fs::read_dir(&skills).is_ok() {
return;
}
let report = discover_extra_path_with_timeout(
extra.path().to_path_buf(),
&fifo,
"discover must not block on leftover FIFO SKILL.md plus unreadable extra/skills/",
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling public must still load when leftover extra/SKILL.md is a FIFO and extra/skills/ is unreadable: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report.skills.iter().all(|s| s.name != "hidden"),
"must not load packages inside unreadable extra/skills/: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::Unreadable && s.path == skills),
"unreadable extra/skills/ must stay a skip row: {:?}",
report.skips
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("SKILL.md")
&& s.name.is_none()
&& s.detail.contains("regular file")
}),
"leftover FIFO extra/SKILL.md must stay unreadable: {:?}",
report.skips
);
let loaded = find_skill_by_name(&report.skills, "public").expect("public");
assert_eq!(loaded.content.trim(), "from-sibling");
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[cfg(unix)]
#[test]
fn extra_path_unreadable_leftover_skill_md_does_not_hide_skills_subdir() {
use std::os::unix::fs::PermissionsExt;
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
let leftover = extra.path().join("SKILL.md");
fs::write(
&leftover,
"---\nname: loose\ndescription: leftover\n---\nloose\n",
)
.expect("write");
write_skill(
&extra.path().join("skills").join("public"),
"public",
"from-skills",
);
let original = fs::metadata(&leftover).expect("meta").permissions();
struct Restore<'a>(&'a std::path::Path, fs::Permissions);
impl Drop for Restore<'_> {
fn drop(&mut self) {
let _ = fs::set_permissions(self.0, self.1.clone());
}
}
let _restore = Restore(&leftover, original.clone());
let mut locked = original.clone();
locked.set_mode(0o000);
fs::set_permissions(&leftover, locked).expect("chmod");
if fs::read(&leftover).is_ok() {
return;
}
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load when leftover extra/SKILL.md is unreadable: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(
report.skills[0].content.trim(),
"from-skills",
"must load the skills/ package, not treat leftover as the extra-path package: {:?}",
report.skills[0]
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable && s.path.ends_with("SKILL.md") && s.name.is_none()
}),
"unreadable leftover extra/SKILL.md must be a skip: {:?}",
report.skips
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[cfg(unix)]
#[test]
fn extra_path_fifo_leftover_and_skills_file_does_not_hide_sibling() {
let extra = tempfile::tempdir().expect("extra");
let fifo = extra.path().join("SKILL.md");
mkfifo(&fifo);
fs::write(extra.path().join("skills"), "not-a-dir").expect("skills file");
write_skill(&extra.path().join("public"), "public", "from-sibling");
let report = discover_extra_path_with_timeout(
extra.path().to_path_buf(),
&fifo,
"discover must not block on leftover FIFO SKILL.md plus extra/skills file",
);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling public must still load when leftover extra/SKILL.md is a FIFO and extra/skills is a file: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(
report.skills[0].content.trim(),
"from-sibling",
"must load the sibling package, not treat leftover as the extra-path package: {:?}",
report.skills[0]
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("SKILL.md")
&& s.name.is_none()
&& s.detail.contains("regular file")
}),
"leftover FIFO extra/SKILL.md must stay unreadable: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"FIFO leftover SKILL.md must not become a root_file peek: {:?}",
report.skips
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let validated = validate_path(&fifo);
assert!(
!validated.ok,
"validate must reject leftover FIFO: {validated:?}"
);
let vskip = validated.skip.expect("validate skip");
assert_eq!(vskip.kind, SkipKind::Unreadable);
assert!(
vskip.detail.contains("regular file"),
"validate/discover must agree leftover FIFO is not a regular file: {}",
vskip.detail
);
let load_msg = unknown_or_skipped_skill_message("public", &report.skips);
assert!(
find_skill_by_name(&report.skills, "public").is_some(),
"load public must not be unknown: {load_msg}"
);
}
#[cfg(unix)]
#[test]
fn extra_path_fifo_leftover_and_skills_fifo_does_not_hide_sibling() {
let extra = tempfile::tempdir().expect("extra");
let leftover = extra.path().join("SKILL.md");
mkfifo(&leftover);
let skills = extra.path().join("skills");
mkfifo(&skills);
write_skill(&extra.path().join("public"), "public", "from-sibling");
let cwd = tempfile::tempdir().expect("cwd");
let home = tempfile::tempdir().expect("home");
let extra_path = extra.path().to_path_buf();
let cwd_path = cwd.path().to_path_buf();
let home_path = home.path().to_path_buf();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let report = with_home_override(Some(home_path), || {
discover(
&cwd_path,
&DiscoveryOptions {
paths: vec![extra_path.display().to_string()],
..DiscoveryOptions::default()
},
)
});
let _ = tx.send(report);
});
let report = match rx.recv_timeout(std::time::Duration::from_secs(2)) {
Ok(r) => r,
Err(_) => {
unblock_fifo(&leftover);
unblock_fifo(&skills);
let _ = rx.recv_timeout(std::time::Duration::from_secs(2));
panic!("discover must not block on leftover FIFO SKILL.md plus extra/skills FIFO");
}
};
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling public must still load when leftover extra/SKILL.md is a FIFO and extra/skills is a FIFO: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(
report.skills[0].content.trim(),
"from-sibling",
"must load the sibling, not hang on extra/skills FIFO: {:?}",
report.skills[0]
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path.ends_with("SKILL.md")
&& s.name.is_none()
&& s.detail.contains("regular file")
}),
"leftover FIFO extra/SKILL.md must stay unreadable: {:?}",
report.skips
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
}
#[test]
fn leftover_nameless_skill_md_and_skills_file_does_not_hide_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
fs::write(
extra.path().join("SKILL.md"),
"---\ndescription: leftover without name\n---\nloose\n",
)
.expect("write");
fs::write(extra.path().join("skills"), "not-a-dir").expect("skills file");
write_skill(&extra.path().join("public"), "public", "from-sibling");
let opts = DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling public must still load when leftover extra/SKILL.md has no name and extra/skills is a file: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(
report.skills[0].content.trim(),
"from-sibling",
"must load the sibling package, not the nameless leftover: {:?}",
report.skills[0]
);
assert!(
find_skill_by_name(&report.skills, "public").is_some(),
"load public must not be unknown: {:?}",
report
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"must watch extra/ when leftover has no name and extra/skills is a file: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &extra.path().join("skills")),
"watch_dirs must not list extra/skills when it is a file: {dirs:?}"
);
}
#[test]
fn leftover_nameless_skill_md_and_skills_dir_still_loads_collection() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
fs::write(
extra.path().join("SKILL.md"),
"---\ndescription: leftover without name\n---\nloose\n",
)
.expect("write");
write_skill(&extra.path().join("skills").join("public"), "public", "ok");
let opts = DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load when leftover extra/SKILL.md has no name: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::RootFile && s.path.ends_with("SKILL.md") }),
"nameless leftover extra/SKILL.md must stay a skip: {:?}",
report.skips
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"must watch extra/ for a nameless leftover collection: {dirs:?}"
);
assert!(
watch_paths_contain(&dirs, &extra.path().join("skills")),
"watch_dirs must list extra/skills when discover walks that collection: {dirs:?}"
);
}
#[test]
fn leftover_blank_peek_skill_md_and_skills_file_does_not_hide_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
fs::write(
extra.path().join("SKILL.md"),
"---\nname: \" \"\ndescription: leftover blank name\n---\nloose\n",
)
.expect("write");
fs::write(extra.path().join("skills"), "not-a-dir").expect("skills file");
write_skill(&extra.path().join("public"), "public", "from-sibling");
let opts = DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling public must still load when leftover extra/SKILL.md peeks a blank name and extra/skills is a file: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(
report.skills[0].content.trim(),
"from-sibling",
"must load the sibling package, not the blank-peek leftover: {:?}",
report.skills[0]
);
assert!(
find_skill_by_name(&report.skills, "public").is_some(),
"load public must not be unknown: {:?}",
report
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"must watch extra/ when leftover peeks a blank name and extra/skills is a file: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &extra.path().join("skills")),
"watch_dirs must not list extra/skills when it is a file: {dirs:?}"
);
}
#[test]
fn leftover_blank_peek_skill_md_and_skills_dir_still_loads_collection() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
fs::write(
extra.path().join("SKILL.md"),
"---\nname: \" \"\ndescription: leftover blank name\n---\nloose\n",
)
.expect("write");
write_skill(&extra.path().join("skills").join("public"), "public", "ok");
let opts = DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load when leftover extra/SKILL.md peeks a blank name: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::RootFile && s.path.ends_with("SKILL.md") }),
"blank-peek leftover extra/SKILL.md must stay a skip: {:?}",
report.skips
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"must watch extra/ for a blank-peek leftover collection: {dirs:?}"
);
assert!(
watch_paths_contain(&dirs, &extra.path().join("skills")),
"watch_dirs must list extra/skills when discover walks that collection: {dirs:?}"
);
}
#[test]
fn leftover_invalid_matching_peek_and_skills_dir_still_loads_collection() {
for peeked in ["DEMO", "\"demo \""] {
let cwd = tempfile::tempdir().expect("cwd");
let extra_root = tempfile::tempdir().expect("extra-root");
let extra = extra_root.path().join("demo");
fs::create_dir_all(&extra).expect("mkdir extra");
fs::write(
extra.join("SKILL.md"),
format!("---\nname: {peeked}\ndescription: leftover invalid name\n---\nloose\n"),
)
.expect("write");
write_skill(&extra.join("skills").join("public"), "public", "ok");
let opts = DiscoveryOptions {
paths: vec![extra.display().to_string()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load when leftover extra/SKILL.md peeks `{peeked}` matching extra dir: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::RootFile && s.path.ends_with("SKILL.md") }),
"invalid-matching leftover extra/SKILL.md must stay a skip for `{peeked}`: {:?}",
report.skips
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, &extra),
"must watch extra/ for an invalid-matching leftover collection (`{peeked}`): {dirs:?}"
);
assert!(
watch_paths_contain(&dirs, &extra.join("skills")),
"watch_dirs must list extra/skills when leftover peeks `{peeked}` matching extra dir: {dirs:?}"
);
}
}
#[test]
fn leftover_invalid_matching_peek_does_not_scan_nested_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let extra_root = tempfile::tempdir().expect("extra-root");
let extra = extra_root.path().join("demo");
fs::create_dir_all(&extra).expect("mkdir extra");
fs::write(
extra.join("SKILL.md"),
"---\nname: DEMO\ndescription: leftover invalid name\n---\nloose\n",
)
.expect("write");
write_skill(&extra.join("evil"), "evil", "NESTED_SECRET");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"invalid matching peek must not prove a sibling collection: {:?}",
report
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::ParseError && s.path.ends_with("demo/SKILL.md") }),
"leftover DEMO extra/SKILL.md must stay a parse_error package skip: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"demo/SKILL.md with name DEMO must not become a root_file skip: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let msg = unknown_or_skipped_skill_message("demo", &report.skips);
assert!(
msg.contains("skipped skill: demo") || msg.contains("skipped skill: DEMO"),
"package dir remains the identity: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
}
#[test]
fn leftover_ascii_matching_peek_and_skills_dir_still_loads_collection() {
let cwd = tempfile::tempdir().expect("cwd");
let extra_root = tempfile::tempdir().expect("extra-root");
let extra = extra_root.path().join("café");
fs::create_dir_all(&extra).expect("mkdir extra");
fs::write(
extra.join("SKILL.md"),
"---\nname: café\ndescription: leftover unicode name\n---\nloose\n",
)
.expect("write");
write_skill(&extra.join("skills").join("public"), "public", "ok");
let on = DiscoveryOptions {
paths: vec![extra.display().to_string()],
ascii_names: true,
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &on);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load when leftover extra/SKILL.md peeks café matching extra dir under ascii_names: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::RootFile && s.path.ends_with("SKILL.md") }),
"ascii-matching leftover extra/SKILL.md must stay a skip: {:?}",
report.skips
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &on)
});
assert!(
watch_paths_contain(&dirs, &extra),
"must watch extra/ for an ascii-matching leftover collection: {dirs:?}"
);
assert!(
watch_paths_contain(&dirs, &extra.join("skills")),
"watch_dirs must list extra/skills when leftover peeks café matching extra dir under ascii_names: {dirs:?}"
);
let off = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
..DiscoveryOptions::default()
},
);
assert_eq!(
off.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["café"],
"without ascii_names the matching Unicode leftover is the package: {:?}",
off
);
assert!(find_skill_by_name(&off.skills, "public").is_none());
}
#[test]
fn leftover_ascii_matching_peek_does_not_scan_nested_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let extra_root = tempfile::tempdir().expect("extra-root");
let extra = extra_root.path().join("café");
fs::create_dir_all(&extra).expect("mkdir extra");
fs::write(
extra.join("SKILL.md"),
"---\nname: café\ndescription: leftover unicode name\n---\nloose\n",
)
.expect("write");
write_skill(&extra.join("evil"), "evil", "NESTED_SECRET");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
ascii_names: true,
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"ascii matching peek must not prove a sibling collection: {:?}",
report
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::ParseError && s.path.ends_with("café/SKILL.md") }),
"leftover café extra/SKILL.md must stay a parse_error package skip: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"café/SKILL.md with name café must not become a root_file skip: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let msg = unknown_or_skipped_skill_message("café", &report.skips);
assert!(
msg.contains("skipped skill: café"),
"package dir remains the identity: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
}
#[test]
fn leftover_path_component_peek_and_skills_dir_still_loads_collection() {
for peeked in [".", "..", ".", "‥", "․", "﹒", "︰"] {
let cwd = tempfile::tempdir().expect("cwd");
let extra_root = tempfile::tempdir().expect("extra-root");
let extra = extra_root.path().join("wanted");
fs::create_dir_all(&extra).expect("mkdir extra");
fs::write(
extra.join("SKILL.md"),
format!("---\nname: {peeked}\ndescription: leftover path-like name\n---\nloose\n"),
)
.expect("write");
write_skill(&extra.join("skills").join("public"), "public", "ok");
let opts = DiscoveryOptions {
paths: vec![extra.display().to_string()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load when leftover extra/SKILL.md peeks `{peeked}`: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::RootFile && s.path.ends_with("SKILL.md") }),
"path-component leftover extra/SKILL.md must stay a skip for `{peeked}`: {:?}",
report.skips
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, &extra),
"must watch extra/ for a path-component leftover collection (`{peeked}`): {dirs:?}"
);
assert!(
watch_paths_contain(&dirs, &extra.join("skills")),
"watch_dirs must list extra/skills when leftover peeks `{peeked}`: {dirs:?}"
);
}
}
#[test]
fn leftover_path_component_peek_and_skills_file_does_not_hide_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let extra_root = tempfile::tempdir().expect("extra-root");
let extra = extra_root.path().join("wanted");
fs::create_dir_all(&extra).expect("mkdir extra");
fs::write(
extra.join("SKILL.md"),
"---\nname: .\ndescription: leftover path-like name\n---\nloose\n",
)
.expect("write");
fs::write(extra.join("skills"), "not-a-dir").expect("skills file");
write_skill(&extra.join("public"), "public", "from-sibling");
let opts = DiscoveryOptions {
paths: vec![extra.display().to_string()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"sibling public must still load when leftover extra/SKILL.md peeks `.` and extra/skills is a file: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(
report.skills[0].content.trim(),
"from-sibling",
"must load the sibling package, not the path-component leftover: {:?}",
report.skills[0]
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, &extra),
"must watch extra/ when leftover peeks `.` and extra/skills is a file: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &extra.join("skills")),
"watch_dirs must not list extra/skills when it is a file: {dirs:?}"
);
}
#[test]
fn leftover_unparseable_matching_peek_and_skills_dir_still_loads_collection() {
let long_desc = "x".repeat(crate::skill::SKILL_DESCRIPTION_MAX_CHARS + 1);
let leftovers = [
"---\nname: demo\n---\nloose\n".to_owned(),
format!("---\nname: demo\ndescription: {long_desc}\n---\nloose\n"),
];
for leftover in leftovers {
let cwd = tempfile::tempdir().expect("cwd");
let extra_root = tempfile::tempdir().expect("extra-root");
let extra = extra_root.path().join("demo");
fs::create_dir_all(&extra).expect("mkdir extra");
fs::write(extra.join("SKILL.md"), &leftover).expect("write");
write_skill(&extra.join("skills").join("public"), "public", "ok");
let opts = DiscoveryOptions {
paths: vec![extra.display().to_string()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load when leftover extra/SKILL.md peeks demo matching extra dir but cannot parse: leftover={leftover:?} skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::RootFile && s.path.ends_with("SKILL.md") }),
"unparseable matching leftover extra/SKILL.md must stay a skip: leftover={leftover:?} skips={:?}",
report.skips
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, &extra),
"must watch extra/ for an unparseable matching leftover collection: leftover={leftover:?} dirs={dirs:?}"
);
assert!(
watch_paths_contain(&dirs, &extra.join("skills")),
"watch_dirs must list extra/skills when leftover peeks demo matching extra dir but cannot parse: leftover={leftover:?} dirs={dirs:?}"
);
}
}
#[test]
fn leftover_unparseable_matching_peek_does_not_scan_nested_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let extra_root = tempfile::tempdir().expect("extra-root");
let extra = extra_root.path().join("demo");
fs::create_dir_all(&extra).expect("mkdir extra");
fs::write(extra.join("SKILL.md"), "---\nname: demo\n---\nloose\n").expect("write");
write_skill(&extra.join("evil"), "evil", "NESTED_SECRET");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.display().to_string()],
..DiscoveryOptions::default()
},
);
assert!(
report.skills.is_empty(),
"unparseable matching peek must not prove a sibling collection: {:?}",
report
);
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load: {:?}",
report.skills
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::ParseError && s.path.ends_with("demo/SKILL.md") }),
"leftover demo extra/SKILL.md must stay a parse_error package skip: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"demo/SKILL.md with name demo must not become a root_file skip: {:?}",
report.skips
);
assert!(find_skill_by_name(&report.skills, "evil").is_none());
let msg = unknown_or_skipped_skill_message("demo", &report.skips);
assert!(
msg.contains("skipped skill: demo"),
"package dir remains the identity: {msg}"
);
assert!(!msg.contains("unknown skill"), "msg={msg}");
}
#[test]
fn leftover_lowercase_skill_md_and_skills_dir_still_loads_collection() {
let cwd = tempfile::tempdir().expect("cwd");
let extra = tempfile::tempdir().expect("extra");
fs::write(
extra.path().join("skill.md"),
"---\nname: loose\ndescription: leftover lowercase\n---\nloose\n",
)
.expect("write");
write_skill(&extra.path().join("skills").join("public"), "public", "ok");
let opts = DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load when leftover extra/skill.md exists: skills={:?} skips={:?}",
report.skills,
report.skips
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, &extra.path().join("skills")),
"watch_dirs must list extra/skills when leftover is skill.md: {dirs:?}"
);
}
#[test]
fn leftover_lowercase_unparseable_matching_peek_still_loads_collection() {
let cwd = tempfile::tempdir().expect("cwd");
let extra_root = tempfile::tempdir().expect("extra-root");
let extra = extra_root.path().join("demo");
fs::create_dir_all(&extra).expect("mkdir extra");
fs::write(extra.join("skill.md"), "---\nname: demo\n---\nloose\n").expect("write");
write_skill(&extra.join("skills").join("public"), "public", "ok");
let opts = DiscoveryOptions {
paths: vec![extra.display().to_string()],
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"skills/public must still load when leftover extra/skill.md peeks demo matching extra dir but cannot parse: skills={:?} skips={:?}",
report.skills,
report.skips
);
}
#[test]
fn leftover_user_dir_skill_md_does_not_hide_skills_subdir() {
let cwd = tempfile::tempdir().expect("cwd");
let user = tempfile::tempdir().expect("user");
fs::write(
user.path().join("SKILL.md"),
"---\nname: loose\ndescription: leftover user root\n---\nloose\n",
)
.expect("write");
write_skill(&user.path().join("skills").join("public"), "public", "ok");
let opts = DiscoveryOptions {
user_skills_dir: Some(user.path().to_path_buf()),
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"user/skills/public must still load when leftover user_dir/SKILL.md exists: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(
report.skills[0].source,
SkillSource::User,
"user/skills collection must stay User: {:?}",
report.skills[0]
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::RootFile && s.name.as_deref() == Some("loose") }),
"leftover user_dir/SKILL.md must stay a skip: {:?}",
report.skips
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, user.path()),
"must watch user_dir when leftover exists: {dirs:?}"
);
assert!(
watch_paths_contain(&dirs, &user.path().join("skills")),
"watch_dirs must list user/skills when leftover user_dir/SKILL.md exists: {dirs:?}"
);
}
#[test]
fn leftover_user_dir_lowercase_skill_md_does_not_hide_skills_subdir() {
let cwd = tempfile::tempdir().expect("cwd");
let user = tempfile::tempdir().expect("user");
fs::write(
user.path().join("skill.md"),
"---\nname: loose\ndescription: leftover user lowercase\n---\nloose\n",
)
.expect("write");
write_skill(&user.path().join("skills").join("public"), "public", "ok");
let opts = DiscoveryOptions {
user_skills_dir: Some(user.path().to_path_buf()),
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"user/skills/public must still load when leftover user_dir/skill.md exists: skills={:?} skips={:?}",
report.skills,
report.skips
);
}
#[test]
fn leftover_user_dir_matching_peek_does_not_hide_skills_subdir() {
let cwd = tempfile::tempdir().expect("cwd");
let root = tempfile::tempdir().expect("root");
let user = root.path().join("skills");
fs::create_dir_all(&user).expect("mkdir user");
fs::write(
user.join("SKILL.md"),
"---\nname: skills\ndescription: leftover matching user dir\n---\nloose\n",
)
.expect("write");
write_skill(&user.join("skills").join("public"), "public", "ok");
write_skill(&user.join("sibling"), "sibling", "from-sibling");
let opts = DiscoveryOptions {
user_skills_dir: Some(user.clone()),
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
let names: Vec<_> = report.skills.iter().map(|s| s.name.as_str()).collect();
assert!(
names.contains(&"public"),
"user/skills/public must still load when leftover peeks skills matching user dir: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
names.contains(&"sibling"),
"user/sibling must still load next to leftover user_dir/SKILL.md: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| { s.kind == SkipKind::RootFile && s.name.as_deref() == Some("skills") }),
"matching leftover user_dir/SKILL.md must stay a skip: {:?}",
report.skips
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, &user.join("skills")),
"watch_dirs must list user/skills when leftover peeks skills matching user dir: {dirs:?}"
);
}
#[test]
fn user_dir_named_skills_package_does_not_scan_nested() {
let cwd = tempfile::tempdir().expect("cwd");
let user = tempfile::tempdir().expect("user");
write_skill(&user.path().join("skills"), "skills", "PACKAGE_BODY");
write_skill(
&user.path().join("skills").join("evil"),
"evil",
"NESTED_SECRET",
);
let opts = DiscoveryOptions {
user_skills_dir: Some(user.path().to_path_buf()),
..DiscoveryOptions::default()
};
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["skills"],
"user_dir/skills named package must not scan nested SKILL.md: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(find_skill_by_name(&report.skills, "evil").is_none());
assert!(
report
.skills
.iter()
.all(|s| !s.content.contains("NESTED_SECRET")),
"nested skill body must not load: {:?}",
report.skills
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, user.path()),
"must watch user_dir: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &user.path().join("skills")),
"watch_dirs must not list user/skills when it is a named package: {dirs:?}"
);
}
#[test]
fn leftover_user_dir_skills_loose_skill_md_is_root_file_not_name_mismatch() {
let cwd = tempfile::tempdir().expect("cwd");
let user = tempfile::tempdir().expect("user");
let leftover = user.path().join("skills").join("SKILL.md");
fs::create_dir_all(leftover.parent().expect("parent")).expect("mkdir");
fs::write(
&leftover,
"---\nname: loose\ndescription: leftover user skills root\n---\nloose\n",
)
.expect("write leftover");
write_skill(&user.path().join("skills").join("public"), "public", "ok");
let opts = DiscoveryOptions {
user_skills_dir: Some(user.path().to_path_buf()),
..DiscoveryOptions::default()
};
let _ = take_read_skill_md_paths();
let report = empty_home_discover(cwd.path(), &opts);
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"user/skills/public must still load when leftover user_dir/skills/SKILL.md exists: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(
report.skills[0].source,
SkillSource::User,
"user/skills collection must stay User: {:?}",
report.skills[0]
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::RootFile && s.path == leftover),
"leftover user_dir/skills/SKILL.md must stay root_file: {:?}",
report.skips
);
assert!(
report
.skips
.iter()
.all(|s| s.kind != SkipKind::NameDirectoryMismatch),
"leftover user_dir/skills/SKILL.md must not also be name_directory_mismatch: {:?}",
report.skips
);
let leftover_opens = take_read_skill_md_paths()
.iter()
.filter(|p| *p == &leftover)
.count();
assert_eq!(
leftover_opens, 1,
"classify leftover user_dir/skills/SKILL.md must not be opened again: leftover={leftover:?}"
);
}
#[test]
fn leftover_user_dir_skills_loose_skill_md_without_sibling_is_root_file_not_name_mismatch() {
let cwd = tempfile::tempdir().expect("cwd");
let user = tempfile::tempdir().expect("user");
let leftover = user.path().join("skills").join("SKILL.md");
fs::create_dir_all(leftover.parent().expect("parent")).expect("mkdir");
fs::write(
&leftover,
"---\nname: loose\ndescription: leftover user skills root\n---\nloose\n",
)
.expect("write leftover");
let opts = DiscoveryOptions {
user_skills_dir: Some(user.path().to_path_buf()),
..DiscoveryOptions::default()
};
let _ = take_read_skill_md_paths();
let report = empty_home_discover(cwd.path(), &opts);
assert!(
report.skills.is_empty(),
"leftover user_dir/skills/SKILL.md must not load as a package: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.any(|s| s.kind == SkipKind::RootFile && s.path == leftover),
"leftover user_dir/skills/SKILL.md must stay root_file: {:?}",
report.skips
);
assert!(
report
.skips
.iter()
.all(|s| s.kind != SkipKind::NameDirectoryMismatch),
"leftover user_dir/skills/SKILL.md must not also be name_directory_mismatch: {:?}",
report.skips
);
let leftover_opens = take_read_skill_md_paths()
.iter()
.filter(|p| *p == &leftover)
.count();
assert_eq!(
leftover_opens, 1,
"classify leftover user_dir/skills/SKILL.md must not be opened again: leftover={leftover:?}"
);
let miss = unknown_or_skipped_skill("skills", &report.skips);
assert_eq!(
miss.error_kind, "unknown_skill",
"load of skills must stay unknown_skill, not a skipped package: {miss:?}"
);
assert!(
miss.path.is_none(),
"unknown_skill has no SKILL.md to name: {miss:?}"
);
let why = crate::why(&report, Some("skills"), None, None);
assert!(
why.skips.is_empty(),
"why skills must not peel leftover as a skipped package: {:?}",
why.skips
);
assert_eq!(
why.unknown_skill_message().as_deref(),
Some("unknown skill: skills")
);
let home = tempfile::tempdir().expect("home");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, user.path()),
"must watch user_dir when leftover user_dir/skills/SKILL.md exists: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &user.path().join("skills")),
"leftover user_dir/skills/SKILL.md is not a discover walk: {dirs:?}"
);
}
#[cfg(unix)]
#[test]
fn leftover_user_dir_skills_fifo_skill_md_does_not_hide_sibling() {
let cwd = tempfile::tempdir().expect("cwd");
let user = tempfile::tempdir().expect("user");
let leftover = user.path().join("skills").join("SKILL.md");
fs::create_dir_all(leftover.parent().expect("parent")).expect("mkdir");
mkfifo(&leftover);
write_skill(&user.path().join("skills").join("public"), "public", "ok");
let opts = DiscoveryOptions {
user_skills_dir: Some(user.path().to_path_buf()),
..DiscoveryOptions::default()
};
let home = tempfile::tempdir().expect("home");
let cwd_path = cwd.path().to_path_buf();
let home_path = home.path().to_path_buf();
let opts_thread = opts.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let report = with_home_override(Some(home_path), || discover(&cwd_path, &opts_thread));
let _ = tx.send(report);
});
let report = match rx.recv_timeout(std::time::Duration::from_secs(2)) {
Ok(r) => r,
Err(_) => {
unblock_fifo(&leftover);
let _ = rx.recv_timeout(std::time::Duration::from_secs(2));
panic!("discover must not block on leftover user_dir/skills FIFO SKILL.md");
}
};
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["public"],
"user/skills/public must still load when leftover user_dir/skills/SKILL.md is a FIFO: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert_eq!(
report.skills[0].source,
SkillSource::User,
"user/skills collection must stay User: {:?}",
report.skills[0]
);
assert!(
report.skips.iter().any(|s| {
s.kind == SkipKind::Unreadable
&& s.path == leftover
&& s.name.is_none()
&& s.detail.contains("regular file")
}),
"leftover user_dir/skills FIFO SKILL.md must be unreadable, not peeked: {:?}",
report.skips
);
assert!(
report.skips.iter().all(|s| s.kind != SkipKind::RootFile),
"FIFO leftover SKILL.md must not become a root_file peek: {:?}",
report.skips
);
let why = crate::why(&report, Some("public"), None, None);
assert_eq!(why.loaded.len(), 1, "why loaded={:?}", why.loaded);
assert!(why.unknown_skill_message().is_none());
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, &user.path().join("skills")),
"watch_dirs must list user/skills when leftover user_dir/skills/SKILL.md is a FIFO: {dirs:?}"
);
}
fn names_in(report: &crate::skip::DiscoveryReport) -> Vec<&str> {
report.skills.iter().map(|s| s.name.as_str()).collect()
}
fn skip_names(report: &crate::skip::DiscoveryReport) -> Vec<String> {
report.skips.iter().filter_map(|s| s.name.clone()).collect()
}
#[test]
fn implicit_roots_defaults_on() {
assert!(
DiscoveryOptions::default().implicit_roots,
"implicit_roots must default true so existing discover stays additive"
);
}
#[test]
fn implicit_home_agents_skill_md_is_opened_once_when_cwd_is_home() {
let home = tempfile::tempdir().expect("home");
let skill_file = home
.path()
.join(".agents")
.join("skills")
.join("homeskill")
.join("SKILL.md");
write_skill(skill_file.parent().expect("pkg"), "homeskill", "FROM_HOME");
let leftover = home.path().join(".agents").join("skills").join("SKILL.md");
fs::write(
&leftover,
"---\nname: loose\ndescription: leftover home root\n---\nloose\n",
)
.expect("write leftover");
let _ = take_read_skill_md_paths();
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(home.path(), &DiscoveryOptions::default())
});
assert_eq!(
report
.skills
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>(),
["homeskill"],
"HOME .agents must load once: skills={:?} skips={:?}",
report.skills,
report.skips
);
assert!(
report
.skips
.iter()
.all(|s| s.kind != SkipKind::NameCollision),
"cwd==HOME must not collide HOME .agents with itself: {:?}",
report.skips
);
let leftover_canon = leftover.canonicalize().expect("leftover");
assert_eq!(
report
.skips
.iter()
.filter(|s| {
s.kind == SkipKind::RootFile && (s.path == leftover || s.path == leftover_canon)
})
.count(),
1,
"leftover HOME .agents SKILL.md must be one root_file: {:?}",
report.skips
);
let reads = take_read_skill_md_paths();
let skill_canon = skill_file.canonicalize().expect("skill");
let home_opens = reads
.iter()
.filter(|p| *p == &skill_file || *p == &skill_canon)
.count();
assert_eq!(
home_opens, 1,
"cwd==HOME must not open HOME .agents SKILL.md twice: {reads:?}"
);
let leftover_opens = reads
.iter()
.filter(|p| *p == &leftover || *p == &leftover_canon)
.count();
assert_eq!(
leftover_opens, 1,
"cwd==HOME must not open leftover HOME .agents SKILL.md twice: {reads:?}"
);
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(home.path(), &DiscoveryOptions::default())
});
let agents = home.path().join(".agents").join("skills");
let listed = dirs
.iter()
.filter(|p| watch_paths_contain(std::slice::from_ref(*p), &agents))
.count();
assert_eq!(
listed, 1,
"watch_dirs must list HOME .agents once when cwd is HOME: {dirs:?}"
);
}
#[test]
fn watch_dirs_docs_attach_agents_to_cwd_to_git_when_implicit_on() {
let src = include_str!("walk.rs");
let fn_at = src.find("pub fn watch_dirs(").expect("watch_dirs fn");
let docs: String = src[..fn_at]
.lines()
.rev()
.take_while(|line| line.starts_with("///"))
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("\n");
assert!(
docs.contains("cwd-to-git `.agents`"),
"watch_dirs rustdoc must attach .agents to the implicit cwd-to-git walk, not the whole tree: {docs}"
);
}
#[test]
fn implicit_roots_off_loads_only_extra_path_collection() {
let cwd = tempfile::tempdir().expect("cwd");
fs::create_dir_all(cwd.path().join(".git")).expect("git");
write_skill(
&cwd.path().join(".agents").join("skills").join("leaked"),
"leaked",
"FROM_CWD",
);
let home = tempfile::tempdir().expect("home");
write_skill(
&home.path().join(".agents").join("skills").join("homeskill"),
"homeskill",
"FROM_HOME",
);
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("wanted"), "wanted", "FROM_EXTRA");
let collection_only = DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
};
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(cwd.path(), &collection_only)
});
assert_eq!(
names_in(&report),
["wanted"],
"collection-only must load extra wanted, not cwd/HOME: skills={:?} skips={:?}",
report.skills,
report.skips
);
let skips = skip_names(&report);
assert!(
!skips.iter().any(|n| n == "leaked" || n == "homeskill"),
"cwd/HOME leaks must not appear as collision losers: skips={:?}",
report.skips
);
let defaulted = with_home_override(Some(home.path().to_path_buf()), || {
discover(cwd.path(), &DiscoveryOptions::default())
});
assert!(
find_skill_by_name(&defaulted.skills, "leaked").is_some(),
"default discover must still load cwd .agents: {:?}",
defaulted.skills
);
assert!(
find_skill_by_name(&defaulted.skills, "homeskill").is_some(),
"default discover must still load HOME .agents: {:?}",
defaulted.skills
);
}
#[test]
fn implicit_roots_off_empty_paths_is_empty_report() {
let cwd = tempfile::tempdir().expect("cwd");
fs::create_dir_all(cwd.path().join(".git")).expect("git");
write_skill(
&cwd.path().join(".agents").join("skills").join("leaked"),
"leaked",
"FROM_CWD",
);
let home = tempfile::tempdir().expect("home");
write_skill(
&home.path().join(".agents").join("skills").join("homeskill"),
"homeskill",
"FROM_HOME",
);
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(
cwd.path(),
&DiscoveryOptions {
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
assert!(
report.skills.is_empty() && report.skips.is_empty(),
"empty paths + no user dir + implicit_roots false must be empty: skills={:?} skips={:?}",
report.skills,
report.skips
);
}
#[test]
fn implicit_roots_off_still_loads_user_skills_dir() {
let cwd = tempfile::tempdir().expect("cwd");
write_skill(
&cwd.path().join(".agents").join("skills").join("leaked"),
"leaked",
"FROM_CWD",
);
let user = tempfile::tempdir().expect("user");
write_skill(&user.path().join("mine"), "mine", "FROM_USER");
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
user_skills_dir: Some(user.path().to_path_buf()),
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert_eq!(
names_in(&report),
["mine"],
"user_skills_dir must still load when implicit roots are off: {:?}",
report.skills
);
assert!(
find_skill_by_name(&report.skills, "leaked").is_none(),
"cwd .agents must stay off: {:?}",
report.skills
);
}
#[test]
fn watch_dirs_omits_implicit_roots_when_off() {
let cwd = tempfile::tempdir().expect("cwd");
fs::create_dir_all(cwd.path().join(".git")).expect("git");
let agents = cwd.path().join(".agents").join("skills");
fs::create_dir_all(&agents).expect("agents");
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("wanted"), "wanted", "FROM_EXTRA");
let home = tempfile::tempdir().expect("home");
let home_agents = home.path().join(".agents").join("skills");
fs::create_dir_all(&home_agents).expect("home agents");
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
)
});
assert!(
!watch_paths_contain(&dirs, &agents),
"watch_dirs must not list cwd .agents when implicit roots are off: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &home_agents),
"watch_dirs must not list HOME .agents when implicit roots are off: {dirs:?}"
);
assert!(
watch_paths_contain(&dirs, extra.path()),
"watch_dirs must still list the extra collection: {dirs:?}"
);
}
#[test]
fn implicit_roots_off_still_walks_leftover_extra_collection() {
let extra = tempfile::tempdir().expect("extra");
fs::write(
extra.path().join("SKILL.md"),
"---\nname: leftover\ndescription: leftover extra root\n---\nLEFTOVER\n",
)
.expect("leftover");
write_skill(
&extra.path().join("skills").join("public"),
"public",
"from-skills",
);
let cwd = tempfile::tempdir().expect("cwd");
write_skill(
&cwd.path().join(".agents").join("skills").join("leaked"),
"leaked",
"FROM_CWD",
);
let report = empty_home_discover(
cwd.path(),
&DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
},
);
assert!(
find_skill_by_name(&report.skills, "public").is_some(),
"leftover extra/SKILL.md + extra/skills must still load public: {:?}",
report.skills
);
assert!(
find_skill_by_name(&report.skills, "leaked").is_none(),
"implicit roots stay off for leftover extra collections: {:?}",
report.skills
);
}
#[test]
fn watch_dirs_omits_ignored_extra_path() {
let cwd = tempfile::tempdir().expect("cwd");
let wanted = tempfile::tempdir().expect("wanted");
write_skill(&wanted.path().join("wanted"), "wanted", "keep");
let secret = tempfile::tempdir().expect("secret");
write_skill(&secret.path().join("secret"), "secret", "hide");
let home = tempfile::tempdir().expect("home");
let opts = DiscoveryOptions {
paths: vec![
wanted.path().display().to_string(),
secret.path().display().to_string(),
],
ignore: vec![secret.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
};
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, wanted.path()),
"watch_dirs must still list the kept extra path: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, secret.path()),
"watch_dirs must omit an ignored extra path: {dirs:?}"
);
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(cwd.path(), &opts)
});
assert!(
find_skill_by_name(&report.skills, "wanted").is_some(),
"discover must still load wanted: {:?}",
report.skills
);
assert!(
find_skill_by_name(&report.skills, "secret").is_none(),
"discover must ignore secret: {:?}",
report.skills
);
}
#[test]
fn watch_dirs_omits_ignored_home_agents() {
let cwd = tempfile::tempdir().expect("cwd");
fs::create_dir_all(cwd.path().join(".git")).expect("git");
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("wanted"), "wanted", "keep");
let home = tempfile::tempdir().expect("home");
let home_agents = home.path().join(".agents").join("skills");
write_skill(&home_agents.join("homeskill"), "homeskill", "hide");
let opts = DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
ignore: vec![home.path().join(".agents").display().to_string()],
implicit_roots: true,
..DiscoveryOptions::default()
};
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"watch_dirs must still list the extra path: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &home_agents),
"watch_dirs must omit ignored HOME .agents/skills: {dirs:?}"
);
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(cwd.path(), &opts)
});
assert!(
find_skill_by_name(&report.skills, "wanted").is_some(),
"discover must still load wanted: {:?}",
report.skills
);
assert!(
find_skill_by_name(&report.skills, "homeskill").is_none(),
"discover must ignore HOME homeskill: {:?}",
report.skills
);
}
#[test]
fn watch_dirs_omits_ignored_cwd_agents() {
let cwd = tempfile::tempdir().expect("cwd");
fs::create_dir_all(cwd.path().join(".git")).expect("git");
let agents = cwd.path().join(".agents").join("skills");
write_skill(&agents.join("leaked"), "leaked", "hide");
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("wanted"), "wanted", "keep");
let home = tempfile::tempdir().expect("home");
let opts = DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
ignore: vec![cwd.path().join(".agents").display().to_string()],
implicit_roots: true,
..DiscoveryOptions::default()
};
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"watch_dirs must still list the extra path: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, &agents),
"watch_dirs must omit ignored cwd .agents/skills: {dirs:?}"
);
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(cwd.path(), &opts)
});
assert!(
find_skill_by_name(&report.skills, "wanted").is_some(),
"discover must still load wanted: {:?}",
report.skills
);
assert!(
find_skill_by_name(&report.skills, "leaked").is_none(),
"discover must ignore cwd leaked: {:?}",
report.skills
);
}
#[test]
fn watch_dirs_omits_ignored_user_dir() {
let cwd = tempfile::tempdir().expect("cwd");
fs::create_dir_all(cwd.path().join(".git")).expect("git");
let extra = tempfile::tempdir().expect("extra");
write_skill(&extra.path().join("wanted"), "wanted", "keep");
let user = tempfile::tempdir().expect("user");
write_skill(&user.path().join("userskill"), "userskill", "hide");
let home = tempfile::tempdir().expect("home");
let opts = DiscoveryOptions {
paths: vec![extra.path().display().to_string()],
user_skills_dir: Some(user.path().to_path_buf()),
ignore: vec![user.path().display().to_string()],
implicit_roots: false,
..DiscoveryOptions::default()
};
let dirs = with_home_override(Some(home.path().to_path_buf()), || {
watch_dirs(cwd.path(), &opts)
});
assert!(
watch_paths_contain(&dirs, extra.path()),
"watch_dirs must still list the extra path: {dirs:?}"
);
assert!(
!watch_paths_contain(&dirs, user.path()),
"watch_dirs must omit an ignored user_dir: {dirs:?}"
);
let report = with_home_override(Some(home.path().to_path_buf()), || {
discover(cwd.path(), &opts)
});
assert!(
find_skill_by_name(&report.skills, "wanted").is_some(),
"discover must still load wanted: {:?}",
report.skills
);
assert!(
find_skill_by_name(&report.skills, "userskill").is_none(),
"discover must ignore user_dir userskill: {:?}",
report.skills
);
}