use super::*;
fn fixture(base: &Path, stems: &[&str]) -> (FrameworkPaths, std::path::PathBuf) {
let paths = FrameworkPaths::under(base);
let source = paths.skill_source_dir();
std::fs::create_dir_all(&source).expect("fixture: bundled source dir");
for stem in stems {
std::fs::write(source.join(format!("{stem}.md")), "# bundled\n")
.expect("fixture: write bundled skill");
}
let project = base.join("project");
std::fs::create_dir_all(&project).expect("fixture: project dir");
(paths, project)
}
fn project_skill(project: &Path, stem: &str) {
let dir = project.join(".claude").join("skills").join(stem);
std::fs::create_dir_all(&dir).expect("fixture: project skill dir");
std::fs::write(dir.join("SKILL.md"), "# project copy\n").expect("fixture: write SKILL.md");
}
#[test]
fn project_tier_bundled_copy_warns() {
let tmp = tempfile::tempdir().expect("tempdir");
let (paths, project) = fixture(tmp.path(), &["tm-ticketing", "tm-workflow"]);
project_skill(&project, "tm-ticketing");
let check = check_skill_project_tier(&paths, Some(&project));
assert_eq!(check.status, CheckStatus::Warn, "{check:?}");
assert!(
check.message.contains("tm-ticketing"),
"the duplicate must be named: {}",
check.message
);
assert!(
check.message.contains("tm doctor --fix-skills"),
"and the remediation given: {}",
check.message
);
}
#[test]
fn the_probe_removes_nothing_it_reports() {
let tmp = tempfile::tempdir().expect("tempdir");
let (paths, project) = fixture(tmp.path(), &["tm-ticketing"]);
project_skill(&project, "tm-ticketing");
let file = project
.join(".claude")
.join("skills")
.join("tm-ticketing")
.join("SKILL.md");
let check = check_skill_project_tier(&paths, Some(&project));
assert_eq!(check.status, CheckStatus::Warn);
assert!(
file.exists(),
"the probe must leave the operator's file alone"
);
}
#[test]
fn project_custom_only_tier_is_ok() {
let tmp = tempfile::tempdir().expect("tempdir");
let (paths, project) = fixture(tmp.path(), &["tm-ticketing"]);
project_skill(&project, "our-house-style");
let check = check_skill_project_tier(&paths, Some(&project));
assert_eq!(check.status, CheckStatus::Ok, "{check:?}");
}
#[test]
fn project_tier_without_bundled_names_is_ok() {
let tmp = tempfile::tempdir().expect("tempdir");
let (paths, project) = fixture(tmp.path(), &["tm-ticketing"]);
let check = check_skill_project_tier(&paths, Some(&project));
assert_eq!(
check.status,
CheckStatus::Ok,
"an absent project tier holds no duplicate: {check:?}"
);
}
fn mark_managed(project: &Path, stems: &[&str]) {
use trusty_agents_common::agents::manifest::checksum;
use trusty_agents_common::skills::manifest::{SkillManifest, SkillManifestEntry};
let dir = project.join(".claude").join("skills");
let mut manifest = SkillManifest::load(&dir).expect("fixture: load ledger");
for stem in stems {
let content = std::fs::read_to_string(dir.join(stem).join("SKILL.md"))
.expect("fixture: read the deployed copy");
manifest.managed.insert(
(*stem).to_string(),
SkillManifestEntry {
checksum: checksum(&content),
deployed_at: "2026-09-01T00:00:00Z".to_string(),
},
);
}
manifest.save(&dir).expect("fixture: save ledger");
}
#[test]
fn project_tier_manifest_managed_copy_is_still_flagged() {
let tmp = tempfile::tempdir().expect("tempdir");
let (paths, project) = fixture(tmp.path(), &["tm-ticketing", "tm-workflow"]);
project_skill(&project, "tm-ticketing");
mark_managed(&project, &["tm-ticketing"]);
let check = check_skill_project_tier(&paths, Some(&project));
assert_eq!(
check.status,
CheckStatus::Warn,
"a manifest-managed stray is still a stray: {check:?}"
);
assert!(
check.message.contains("tm-ticketing"),
"the duplicate must be named: {}",
check.message
);
assert!(
check.message.contains("tm doctor --fix-skills"),
"and the remediation given: {}",
check.message
);
}
#[test]
fn a_user_custom_skill_is_not_flagged() {
let tmp = tempfile::tempdir().expect("tempdir");
let (paths, project) = fixture(tmp.path(), &["tm-ticketing"]);
project_skill(&project, "our-house-style");
mark_managed(&project, &[]);
let check = check_skill_project_tier(&paths, Some(&project));
assert_eq!(
check.status,
CheckStatus::Ok,
"a non-bundled name is the operator's own work: {check:?}"
);
}
#[test]
fn unverifiable_states_are_unknown_not_ok() {
let tmp = tempfile::tempdir().expect("tempdir");
let (paths, project) = fixture(tmp.path(), &["tm-ticketing"]);
assert_eq!(
check_skill_project_tier(&paths, None).status,
CheckStatus::Unknown,
"no project in scope classifies nothing"
);
let bare = tempfile::tempdir().expect("tempdir");
let empty_roster = FrameworkPaths::under(bare.path());
assert_eq!(
check_skill_project_tier(&empty_roster, Some(&project)).status,
CheckStatus::Unknown,
"an empty bundled roster classifies nothing"
);
}
#[test]
#[cfg(unix)]
fn an_unreadable_project_tier_is_unknown_not_ok() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().expect("tempdir");
let (paths, project) = fixture(tmp.path(), &["tm-ticketing"]);
project_skill(&project, "tm-ticketing");
let dir = project.join(".claude").join("skills");
let original = std::fs::metadata(&dir)
.expect("tier metadata")
.permissions();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o000)).expect("chmod 000");
let readable_anyway = std::fs::read_dir(&dir).is_ok();
let check = check_skill_project_tier(&paths, Some(&project));
std::fs::set_permissions(&dir, original).expect("restore tier permissions");
if readable_anyway {
return;
}
assert_eq!(
check.status,
CheckStatus::Unknown,
"an unreadable tier is undetermined, never a clean bill of health: {check:?}"
);
assert!(
check.message.contains("could not be read"),
"the message must say what it could not do: {}",
check.message
);
}
#[test]
fn an_unclassifiable_bundled_entry_is_counted_by_the_check() {
let tmp = tempfile::tempdir().expect("tempdir");
let (paths, project) = fixture(tmp.path(), &["tm-ticketing"]);
let dir = project.join(".claude").join("skills");
std::fs::create_dir_all(&dir).expect("fixture: project tier");
std::fs::write(dir.join("tm-ticketing"), "# not a skill dir\n").expect("fixture: plain file");
let check = check_skill_project_tier(&paths, Some(&project));
assert_eq!(
check.status,
CheckStatus::Warn,
"the check and the repair must count the same entries: {check:?}"
);
assert!(
check.message.contains("tm-ticketing"),
"and it must name the entry: {check:?}"
);
}