use super::*;
use crate::core::skill_deployer::deploy_skills;
use crate::core::skill_drift::{SkillReference, deployed_path};
use std::collections::BTreeMap;
use std::fs;
use tempfile::TempDir;
fn reference_of(pairs: &[(&str, &str)]) -> SkillReference {
SkillReference {
assets: pairs
.iter()
.map(|(s, c)| (s.to_string(), c.to_string()))
.collect::<BTreeMap<_, _>>(),
origin: "this binary's embedded bundled assets".to_string(),
}
}
fn deploy_real(dest: &Path, stem: &str, body: &str, reference: Option<(&str, &str)>) -> TempDir {
let src = TempDir::new().unwrap();
fs::create_dir_all(dest).unwrap();
fs::write(src.path().join(format!("{stem}.md")), body).unwrap();
if let Some((ref_name, ref_body)) = reference {
let refs = src.path().join(stem).join("references");
fs::create_dir_all(&refs).unwrap();
fs::write(refs.join(ref_name), ref_body).unwrap();
}
deploy_skills(src.path(), dest).unwrap();
src
}
fn hand_edit(dest: &Path, manifest_key: &str, new_content: &str) {
fs::write(deployed_path(dest, manifest_key), new_content).unwrap();
}
fn paths_under(tmp: &TempDir) -> FrameworkPaths {
let mut paths = FrameworkPaths::under(tmp.path());
paths.trusty_mpm_root = None;
paths
}
#[test]
fn staleness_ok_when_every_tier_matches() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let _src = deploy_real(&paths.claude_skills_dir(), "tm-doctor", "v2", None);
let check = report(&reference_of(&[("tm-doctor", "v2")]), &paths, None);
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}
#[test]
fn staleness_catches_drift_the_stale_cache_hid() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let _src = deploy_real(
&paths.claude_skills_dir(),
"tm-delegation-patterns",
"v1 mentions PM_INSTRUCTIONS.md",
None,
);
let check = report(
&reference_of(&[("tm-delegation-patterns", "v2 describes the JSON manifest")]),
&paths,
None,
);
assert_eq!(
check.status,
CheckStatus::Warn,
"message: {}",
check.message
);
assert!(
check.message.contains("tm-delegation-patterns"),
"{}",
check.message
);
assert!(check.message.contains("REPAIRABLE"), "{}", check.message);
assert!(
check.message.contains("operator home tier"),
"the remedy must name the tier it applies to: {}",
check.message
);
}
#[test]
fn drift_names_the_tm_install_remedy_only_for_the_operator_home_tier() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let _src = deploy_real(
&paths.claude_skills_dir(),
"tm-delegation-patterns",
"v1",
None,
);
let check = report(
&reference_of(&[("tm-delegation-patterns", "v2")]),
&paths,
None,
);
assert!(
check
.message
.contains("1 drifted at the operator home tier — REPAIRABLE by `tm install`"),
"{}",
check.message
);
}
#[test]
fn managed_config_drift_does_not_claim_tm_install_repairs_it() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let managed = paths.agent_deploy_dir().parent().unwrap().join("skills");
let _src = deploy_real(&managed, "tm-delegation-patterns", "v1", None);
let check = report(
&reference_of(&[("tm-delegation-patterns", "v2")]),
&paths,
None,
);
assert_eq!(check.status, CheckStatus::Warn, "{}", check.message);
assert!(
check.message.contains("managed config tier"),
"{}",
check.message
);
assert!(
check.message.contains("does NOT write this tier"),
"the remedy must say `tm install` cannot repair this tier: {}",
check.message
);
assert!(
!check.message.contains("REPAIRABLE by `tm install`"),
"this is the #5865 defect verbatim: {}",
check.message
);
assert!(
check.message.contains("tm doctor --fix-skills"),
"a tier `tm install` cannot reach must still name a remedy that can: {}",
check.message
);
}
#[test]
fn project_tier_drift_names_a_remedy_that_writes_the_project_tier() {
let tmp = TempDir::new().unwrap();
let project = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let _src = deploy_real(
&project.path().join(".claude").join("skills"),
"tm-delegation-patterns",
"v1",
None,
);
let check = report(
&reference_of(&[("tm-delegation-patterns", "v2")]),
&paths,
Some(project.path()),
);
assert!(check.message.contains("project tier"), "{}", check.message);
assert!(
!check.message.contains("REPAIRABLE by `tm install`"),
"{}",
check.message
);
}
#[test]
fn managed_config_conventions_drift_still_escalates_to_fail() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let managed = paths.agent_deploy_dir().parent().unwrap().join("skills");
let _src = deploy_real(&managed, "tm-workflow", "v1", None);
let check = report(&reference_of(&[("tm-workflow", "v2")]), &paths, None);
assert_eq!(
check.status,
CheckStatus::Fail,
"conventions drift must escalate at EVERY tier: {}",
check.message
);
assert!(
check.message.to_lowercase().contains("convention"),
"{}",
check.message
);
}
#[test]
fn an_unknown_tier_label_never_claims_tm_install_repairs_it() {
let remedy = drift_remedy("some future tier");
assert!(!remedy.contains("tm install"), "{remedy}");
assert!(remedy.contains("tm doctor --fix-skills"), "{remedy}");
}
#[test]
fn staleness_escalates_when_conventions_skill_drifts() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let _src = deploy_real(&paths.claude_skills_dir(), "tm-workflow", "v1", None);
let check = report(&reference_of(&[("tm-workflow", "v2")]), &paths, None);
assert_eq!(
check.status,
CheckStatus::Fail,
"message: {}",
check.message
);
assert!(check.message.contains("tm-workflow"), "{}", check.message);
assert!(
check.message.to_lowercase().contains("convention"),
"{}",
check.message
);
}
#[test]
fn staleness_warns_when_ordinary_skill_drifts() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _a = deploy_real(&dest, "tm-doctor", "v1", None);
let _b = deploy_real(&dest, "tm-workflow", "v2", None);
let check = report(
&reference_of(&[("tm-doctor", "v2"), ("tm-workflow", "v2")]),
&paths,
None,
);
assert_eq!(
check.status,
CheckStatus::Warn,
"message: {}",
check.message
);
assert!(check.message.contains("tm-doctor"), "{}", check.message);
}
#[test]
fn staleness_reports_frozen_separately_from_repairable_drift() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let _src = deploy_real(
&paths.claude_skills_dir(),
"tm-doctor",
"what tm wrote",
None,
);
hand_edit(&paths.claude_skills_dir(), "tm-doctor", "hand-edited");
let check = report(&reference_of(&[("tm-doctor", "v2")]), &paths, None);
assert_eq!(
check.status,
CheckStatus::Warn,
"message: {}",
check.message
);
assert!(check.message.contains("FROZEN"), "{}", check.message);
assert!(
check.message.contains("--fix-skills"),
"the frozen report must name the opt-in remedy: {}",
check.message
);
assert!(
!check.message.contains("REPAIRABLE"),
"a frozen-only finding must not claim `tm install` repairs it: {}",
check.message
);
assert!(
!check.message.contains("drifted at the"),
"a frozen-only finding has no repairable-drift clause: {}",
check.message
);
}
#[test]
fn staleness_is_unknown_when_a_skill_cannot_be_verified() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let _src = deploy_real(&paths.claude_skills_dir(), "my-custom-skill", "x", None);
let check = report(&reference_of(&[("tm-doctor", "v2")]), &paths, None);
assert_eq!(
check.status,
CheckStatus::Unknown,
"message: {}",
check.message
);
assert!(
check.message.contains("could NOT be verified"),
"{}",
check.message
);
}
#[test]
fn staleness_is_unknown_with_no_reference_assets() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let check = report(&reference_of(&[]), &paths, None);
assert_eq!(check.status, CheckStatus::Unknown);
}
#[test]
fn staleness_reports_a_missing_deployed_file() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _src = deploy_real(&dest, "tm-doctor", "v2", None);
fs::remove_file(deployed_path(&dest, "tm-doctor")).unwrap();
let check = report(&reference_of(&[("tm-doctor", "v2")]), &paths, None);
assert_eq!(
check.status,
CheckStatus::Warn,
"message: {}",
check.message
);
assert!(check.message.contains("ABSENT"), "{}", check.message);
}
#[test]
fn staleness_covers_the_managed_config_tier() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let managed = paths.agent_deploy_dir().parent().unwrap().join("skills");
let _src = deploy_real(&managed, "tm-delegation-patterns", "v1", None);
let check = report(
&reference_of(&[("tm-delegation-patterns", "v2")]),
&paths,
None,
);
assert_eq!(
check.status,
CheckStatus::Warn,
"message: {}",
check.message
);
assert!(
check
.message
.contains("managed config/tm-delegation-patterns"),
"the managed-config tier must be named: {}",
check.message
);
}
#[test]
fn staleness_message_is_bounded() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let stems: Vec<String> = (0..10).map(|i| format!("skill-{i}")).collect();
let _srcs: Vec<TempDir> = stems
.iter()
.map(|stem| deploy_real(&dest, stem, "v1", None))
.collect();
let pairs: Vec<(&str, &str)> = stems.iter().map(|s| (s.as_str(), "v2")).collect();
let check = report(&reference_of(&pairs), &paths, None);
assert!(check.message.contains("+5 more"), "{}", check.message);
}
#[test]
fn staleness_is_ok_for_a_pristine_multi_file_deploy() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let _src = deploy_real(
&paths.claude_skills_dir(),
"tm-doctor",
"entry v1",
Some(("guide.md", "reference v1")),
);
let check = report(
&reference_of(&[
("tm-doctor", "entry v1"),
("tm-doctor/references/guide.md", "reference v1"),
]),
&paths,
None,
);
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}
#[test]
fn staleness_escalates_when_a_reference_file_of_a_conventions_skill_drifts() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let _src = deploy_real(
&paths.claude_skills_dir(),
"documentation-style",
"entry v1",
Some(("method-function.md", "reference v1")),
);
let check = report(
&reference_of(&[
("documentation-style", "entry v1"),
(
"documentation-style/references/method-function.md",
"reference v2",
),
]),
&paths,
None,
);
assert_eq!(
check.status,
CheckStatus::Fail,
"message: {}",
check.message
);
assert!(
check.message.contains("method-function.md"),
"{}",
check.message
);
}
#[test]
fn staleness_is_unknown_when_a_tier_ledger_is_corrupt() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _src = deploy_real(&dest, "tm-doctor", "v1", None);
fs::write(
dest.join(crate::core::skill_manifest::SKILL_MANIFEST_FILE),
"{ not json",
)
.unwrap();
let check = report(&reference_of(&[("tm-doctor", "v1")]), &paths, None);
assert_eq!(
check.status,
CheckStatus::Unknown,
"message: {}",
check.message
);
assert!(
check.message.contains("could NOT be verified"),
"{}",
check.message
);
}
#[test]
fn staleness_is_unknown_when_a_tier_has_no_ledger() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _src = deploy_real(&dest, "tm-doctor", "v1", None);
fs::remove_file(dest.join(crate::core::skill_manifest::SKILL_MANIFEST_FILE)).unwrap();
let check = report(&reference_of(&[("tm-doctor", "v1")]), &paths, None);
assert_eq!(
check.status,
CheckStatus::Unknown,
"message: {}",
check.message
);
}
#[test]
fn a_retired_skill_no_longer_pins_staleness_to_unknown() {
let tmp = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _current = deploy_real(&dest, "tm-workflow", "v2", None);
let _retired = deploy_real(&dest, "tm-pr-workflow", "the retired workflow skill", None);
let reference = reference_of(&[("tm-workflow", "v2")]);
let before = report(&reference, &paths, None);
assert_eq!(
before.status,
CheckStatus::Unknown,
"expected the #5224 defect, got: {}",
before.message
);
assert!(
before.message.contains("tm-pr-workflow"),
"{}",
before.message
);
let live: std::collections::BTreeSet<String> = ["tm-workflow".to_string()].into();
let retired =
crate::core::skill_retire::retire_orphans_in("operator home", &dest, &live).unwrap();
assert_eq!(retired.len(), 1, "{retired:?}");
assert!(retired[0].removed, "{retired:?}");
let after = report(&reference, &paths, None);
assert_eq!(
after.status,
CheckStatus::Ok,
"the check is still degraded after the orphan was retired: {}",
after.message
);
}