use super::*;
use crate::core::skill_tiers::{SkillTier, list_project_custom_stems};
use tempfile::TempDir;
const V1: &str = "1.3.5";
const V2: &str = "1.3.6";
fn seed_framework(base: &Path, stem: &str, body: &str) -> FrameworkPaths {
let fw = FrameworkPaths::under(base);
std::fs::create_dir_all(&fw.skills).unwrap();
std::fs::write(fw.skills.join(format!("{stem}.md")), body).unwrap();
fw
}
fn reseed(fw: &FrameworkPaths, stem: &str, body: &str) {
std::fs::write(fw.user_skill_source_dir().join(format!("{stem}.md")), body).unwrap();
}
fn write_project_manifest(project_dir: &Path, body: &str) {
let dir = crate::core::harness_root::framework_dir(project_dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("manifest.toml"), body).unwrap();
}
fn plan_for(fw: &FrameworkPaths, project_dir: &Path) -> HarnessPlan {
let catalog_root = crate::content::catalog_root_for(&fw.root);
let manifest = resolve_manifest(&ManifestSources::resolve(project_dir, &catalog_root));
HarnessPlan::from_manifest(&manifest, fw, &catalog_root)
}
fn fixture(stem: &str, body: &str) -> (TempDir, FrameworkPaths, std::path::PathBuf) {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path(), stem, body);
std::fs::create_dir_all(fw.user_skill_source_dir()).unwrap();
reseed(&fw, stem, body);
let project = tmp.path().join("workspace");
std::fs::create_dir_all(&project).unwrap();
(tmp, fw, project)
}
#[test]
fn stamp_is_stable_across_calls() {
let (_tmp, fw, project) = fixture("probe-skill", "V1");
let plan = plan_for(&fw, &project);
assert_eq!(
project_tier_stamp(V1, &plan),
project_tier_stamp(V1, &plan),
"the stamp is a pure function of its inputs"
);
}
#[test]
fn stamp_changes_with_the_version() {
let (_tmp, fw, project) = fixture("probe-skill", "V1");
let plan = plan_for(&fw, &project);
assert_ne!(
project_tier_stamp(V1, &plan),
project_tier_stamp(V2, &plan),
"a version bump is the primary trigger"
);
}
#[test]
fn stamp_changes_with_the_skill_selection() {
let (_tmp, fw, project) = fixture("probe-skill", "V1");
let before = project_tier_stamp(V1, &plan_for(&fw, &project));
write_project_manifest(&project, "[skills]\nexclude = [\"something-else\"]\n");
let after = project_tier_stamp(V1, &plan_for(&fw, &project));
assert_ne!(
before, after,
"an operator editing `[skills]` in the project override must trigger, \
without waiting for a release"
);
}
#[test]
fn stamp_ignores_manifest_changes_outside_the_skill_selection() {
let (_tmp, fw, project) = fixture("probe-skill", "V1");
let before = project_tier_stamp(V1, &plan_for(&fw, &project));
write_project_manifest(&project, "[style]\nactive = \"probe-style\"\n");
let after = project_tier_stamp(V1, &plan_for(&fw, &project));
assert_eq!(
before, after,
"a style edit changes no skill, so it must not force a redeploy"
);
}
#[test]
fn version_bump_redeploys() {
let (_tmp, fw, project) = fixture("probe-skill", "V1");
let deployed = project_skill_dir(&project)
.join("probe-skill")
.join("SKILL.md");
let first = ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
assert!(first.deployed, "the first run has no stamp, so it deploys");
assert_eq!(std::fs::read_to_string(&deployed).unwrap(), "V1");
reseed(&fw, "probe-skill", "V2-REFRESHED");
let second = ensure_project_skill_tier_for_version(&fw, &project, V2).unwrap();
assert!(second.deployed, "a version bump must re-run deployment");
assert_eq!(
std::fs::read_to_string(&deployed).unwrap(),
"V2-REFRESHED",
"a managed, user-unmodified project-tier skill must be refreshed"
);
}
#[test]
fn skill_selection_change_redeploys() {
let (_tmp, fw, project) = fixture("probe-skill", "V1");
let deployed = project_skill_dir(&project)
.join("probe-skill")
.join("SKILL.md");
ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
reseed(&fw, "probe-skill", "V1-PLUS");
write_project_manifest(&project, "[skills]\nexclude = [\"something-else\"]\n");
let report = ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
assert!(
report.deployed,
"an edited `[skills]` selection must deploy at the same version"
);
assert_eq!(std::fs::read_to_string(&deployed).unwrap(), "V1-PLUS");
}
#[test]
fn unchanged_version_is_a_noop() {
let (_tmp, fw, project) = fixture("probe-skill", "V1");
let deployed = project_skill_dir(&project)
.join("probe-skill")
.join("SKILL.md");
ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
let before = std::fs::metadata(&deployed).unwrap().modified().unwrap();
let second = ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
assert!(
!second.deployed,
"a matching stamp must take the no-op path, not a redeploy"
);
assert_eq!(second.stats, DeployStats::default());
let after = std::fs::metadata(&deployed).unwrap().modified().unwrap();
assert_eq!(before, after, "a no-op must not rewrite a single file");
}
#[test]
fn project_custom_skill_is_never_overwritten() {
let (_tmp, fw, project) = fixture("probe-skill", "BUNDLED");
let custom = project_skill_dir(&project)
.join("probe-skill")
.join("SKILL.md");
std::fs::create_dir_all(custom.parent().unwrap()).unwrap();
let mine = "MY OWN SKILL — never overwrite this\n";
std::fs::write(&custom, mine).unwrap();
let first = ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
assert_eq!(std::fs::read_to_string(&custom).unwrap(), mine);
assert!(
first.shadowed.iter().any(|s| s.stem == "probe-skill"
&& s.winner == SkillTier::Project
&& s.loser == SkillTier::User),
"the hand-placed copy must win precedence outright: {:?}",
first.shadowed
);
assert!(
!first.stats.deployed.iter().any(|s| s == "probe-skill"),
"a shadowed stem must never be written: {:?}",
first.stats
);
reseed(&fw, "probe-skill", "BUNDLED-V2");
let second = ensure_project_skill_tier_for_version(&fw, &project, V2).unwrap();
assert!(second.deployed, "the version bumped, so a deploy ran");
assert_eq!(
std::fs::read_to_string(&custom).unwrap(),
mine,
"a version bump must not wipe the operator's local copy"
);
assert!(
second.shadowed.iter().any(|s| s.stem == "probe-skill"),
"…still shadowed on the version-triggered run: {:?}",
second.shadowed
);
}
#[test]
fn frozen_skill_is_still_skipped() {
let (_tmp, fw, project) = fixture("probe-skill", "V1");
let deployed = project_skill_dir(&project)
.join("probe-skill")
.join("SKILL.md");
ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
let hand_edit = "V1 plus my own notes\n";
std::fs::write(&deployed, hand_edit).unwrap();
reseed(&fw, "probe-skill", "V2");
let report = ensure_project_skill_tier_for_version(&fw, &project, V2).unwrap();
assert!(report.deployed, "the version bumped, so a deploy ran");
assert_eq!(
std::fs::read_to_string(&deployed).unwrap(),
hand_edit,
"a checksum-frozen hand edit must survive a version-triggered redeploy"
);
assert!(
report.stats.skipped.iter().any(|s| s == "probe-skill"),
"the frozen skill must be reported as skipped: {:?}",
report.stats
);
}
#[test]
fn missing_skill_source_does_not_stamp() {
let tmp = TempDir::new().unwrap();
let mut fw = FrameworkPaths::under(tmp.path());
fw.trusty_mpm_root = None;
let project = tmp.path().join("workspace");
std::fs::create_dir_all(&project).unwrap();
let report = ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
assert!(!report.deployed);
assert!(
!project_skill_dir(&project)
.join(PROJECT_TIER_STAMP_FILE)
.exists(),
"an empty deploy must not be recorded as current — the next run retries"
);
}
#[test]
fn deploys_into_the_workspace_not_the_framework_home() {
let (_tmp, fw, project) = fixture("probe-skill", "V1");
ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
assert!(
project_skill_dir(&project)
.join("probe-skill")
.join("SKILL.md")
.is_file()
);
assert!(
!fw.claude_skills_dir().join("probe-skill").exists(),
"a home-scoped `fw` must not redirect this deploy into ~/.claude/skills"
);
}
#[test]
fn stamp_file_is_ignored_by_the_tier_planner() {
let (_tmp, fw, project) = fixture("probe-skill", "V1");
ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
let dest = project_skill_dir(&project);
assert!(dest.join(PROJECT_TIER_STAMP_FILE).is_file());
let custom = list_project_custom_stems(&dest).unwrap();
assert!(
!custom.iter().any(|s| s.starts_with('.')),
"the stamp marker must never be mistaken for a project-custom skill: {custom:?}"
);
}
#[test]
fn empty_skill_source_does_not_stamp() {
let tmp = TempDir::new().unwrap();
let submodule_root = TempDir::new().unwrap();
let submodule_skills = submodule_root.path().join("agents").join("skills");
std::fs::create_dir_all(&submodule_skills).unwrap();
let mut fw = FrameworkPaths::under(tmp.path());
fw.trusty_mpm_root = Some(submodule_root.path().to_path_buf());
assert_eq!(
fw.skill_source_dir(),
submodule_skills,
"fixture guard: the empty submodule must be the resolved source"
);
let project = tmp.path().join("workspace");
std::fs::create_dir_all(&project).unwrap();
let report = ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
assert!(!report.deployed);
assert!(
!project_skill_dir(&project)
.join(PROJECT_TIER_STAMP_FILE)
.exists(),
"an empty source must not be recorded as a successful deploy — the stamp \
would freeze the project tier stale forever"
);
}
#[test]
fn project_tier_receives_no_bundled_skill() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path(), "probe-bundled", "BUNDLED ONLY");
let project = tmp.path().join("workspace");
std::fs::create_dir_all(&project).unwrap();
let report = ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
assert!(
!report.stats.deployed.iter().any(|s| s == "probe-bundled"),
"a bundled skill must not be deployed to the project tier: {:?}",
report.stats
);
assert!(
!project_skill_dir(&project).join("probe-bundled").exists(),
"and nothing may be written for it on disk"
);
}
#[test]
fn project_tier_still_receives_a_user_custom_skill() {
let (_tmp, fw, project) = fixture("probe-skill", "V1");
let report = ensure_project_skill_tier_for_version(&fw, &project, V1).unwrap();
assert!(report.deployed, "the deploy must still run: {report:?}");
assert_eq!(
std::fs::read_to_string(
project_skill_dir(&project)
.join("probe-skill")
.join("SKILL.md")
)
.unwrap(),
"V1",
"a user-custom skill still reaches the project tier"
);
}
#[test]
fn bundled_skill_reaches_the_user_tier_only() {
use crate::core::skill_tiers::plan_skill_tiers;
use std::collections::BTreeSet;
let bundled: BTreeSet<String> = ["tm-ticketing".to_string()].into_iter().collect();
let empty = BTreeSet::new();
let user_dest = plan_skill_tiers(&empty, &empty, &bundled);
assert!(
user_dest.bundled_deploy.contains("tm-ticketing"),
"the user tier deploys the whole bundled roster: {user_dest:?}"
);
let project_dest: BTreeSet<String> = bundled
.iter()
.filter(|s| super::bundled_excluded_from_project_tier(s))
.cloned()
.collect();
assert!(
project_dest.is_empty(),
"and the project tier's selection admits none of it: {project_dest:?}"
);
}