use super::*;
use crate::core::skill_deployer::deploy_skills;
use crate::core::skill_drift::{deployed_path, skill_reference};
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::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 repair_rewrites_drifted_and_verifies() {
let tmp = TempDir::new().unwrap();
let backups = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _src = deploy_real(&dest, "tm-workflow", "v1", None);
let outcomes = repair_skills(
&reference_of(&[("tm-workflow", "v2")]),
&paths,
None,
false,
backups.path(),
);
let repaired: Vec<&RepairOutcome> = outcomes.iter().filter(|o| o.changed()).collect();
assert_eq!(repaired.len(), 1, "outcomes: {outcomes:?}");
assert_eq!(repaired[0].stem, "tm-workflow");
let on_disk = fs::read_to_string(dest.join("tm-workflow").join("SKILL.md")).unwrap();
assert_eq!(on_disk, "v2");
assert!(SkillManifest::load(&dest).checksum_matches("tm-workflow", "v2"));
}
#[test]
fn repair_writes_a_nested_reference_key_to_its_own_path() {
let tmp = TempDir::new().unwrap();
let backups = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _src = deploy_real(
&dest,
"documentation-style",
"entry v1",
Some(("spec.md", "reference v1")),
);
let outcomes = repair_skills(
&reference_of(&[
("documentation-style", "entry v1"),
("documentation-style/references/spec.md", "reference v2"),
]),
&paths,
None,
false,
backups.path(),
);
let repaired: Vec<&RepairOutcome> = outcomes.iter().filter(|o| o.changed()).collect();
assert_eq!(repaired.len(), 1, "outcomes: {outcomes:?}");
assert_eq!(repaired[0].stem, "documentation-style/references/spec.md");
assert_eq!(
fs::read_to_string(deployed_path(
&dest,
"documentation-style/references/spec.md"
))
.unwrap(),
"reference v2"
);
assert_eq!(
fs::read_to_string(deployed_path(&dest, "documentation-style")).unwrap(),
"entry v1"
);
}
#[test]
fn repair_refuses_a_tier_with_an_unreadable_manifest() {
let tmp = TempDir::new().unwrap();
let backups = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _src = deploy_real(&dest, "tm-workflow", "v1", None);
fs::write(
dest.join(crate::core::skill_manifest::SKILL_MANIFEST_FILE),
"{ not json",
)
.unwrap();
let outcomes = repair_skills(
&reference_of(&[("tm-workflow", "v2")]),
&paths,
None,
true,
backups.path(),
);
assert!(
outcomes.iter().all(|o| !o.changed()),
"nothing may be written into a tier with an unreadable ledger: {outcomes:?}"
);
assert_eq!(
fs::read_to_string(deployed_path(&dest, "tm-workflow")).unwrap(),
"v1",
"the deployed file must be untouched"
);
}
#[test]
fn repair_never_touches_a_frozen_skill_by_default() {
let tmp = TempDir::new().unwrap();
let backups = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _src = deploy_real(&dest, "tm-workflow", "what tm wrote", None);
hand_edit(&dest, "tm-workflow", "hand-edited by the operator");
let outcomes = repair_skills(
&reference_of(&[("tm-workflow", "v2")]),
&paths,
None,
false,
backups.path(),
);
assert_eq!(outcomes.len(), 1);
assert_eq!(outcomes[0].action, RepairAction::SkippedFrozen);
let on_disk = fs::read_to_string(dest.join("tm-workflow").join("SKILL.md")).unwrap();
assert_eq!(on_disk, "hand-edited by the operator");
}
#[test]
fn repair_backs_up_before_overwriting() {
let tmp = TempDir::new().unwrap();
let backups = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _src = deploy_real(&dest, "tm-workflow", "what tm wrote", None);
hand_edit(&dest, "tm-workflow", "hand-edited by the operator");
let outcomes = repair_skills(
&reference_of(&[("tm-workflow", "v2")]),
&paths,
None,
true,
backups.path(),
);
let RepairAction::Repaired { backup } = &outcomes[0].action else {
panic!("expected a repair, got {:?}", outcomes[0].action);
};
let backup = backup.as_ref().expect("an existing file must be backed up");
assert_eq!(
fs::read_to_string(backup).unwrap(),
"hand-edited by the operator",
"the backup must hold the operator's original content"
);
assert_eq!(
fs::read_to_string(dest.join("tm-workflow").join("SKILL.md")).unwrap(),
"v2"
);
}
#[cfg(unix)]
#[test]
fn repair_reports_verification_failure() {
use std::os::unix::fs::PermissionsExt;
let tmp = TempDir::new().unwrap();
let backups = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _src = deploy_real(&dest, "tm-workflow", "v1", None);
let skill_dir = dest.join("tm-workflow");
fs::set_permissions(&skill_dir, fs::Permissions::from_mode(0o500)).unwrap();
let deniable = fs::write(skill_dir.join(".probe"), "x").is_err();
if !deniable {
let _ = fs::remove_file(skill_dir.join(".probe"));
fs::set_permissions(&skill_dir, fs::Permissions::from_mode(0o755)).unwrap();
return;
}
let outcomes = repair_skills(
&reference_of(&[("tm-workflow", "v2")]),
&paths,
None,
true,
backups.path(),
);
fs::set_permissions(&skill_dir, fs::Permissions::from_mode(0o755)).unwrap();
assert!(
matches!(outcomes[0].action, RepairAction::Failed(_)),
"got {:?}",
outcomes[0].action
);
assert!(!outcomes[0].changed());
assert_eq!(
fs::read_to_string(skill_dir.join("SKILL.md")).unwrap(),
"v1"
);
}
#[test]
fn repair_skips_unverifiable_skills() {
let tmp = TempDir::new().unwrap();
let backups = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _src = deploy_real(&dest, "my-custom-skill", "operator content", None);
let outcomes = repair_skills(
&reference_of(&[("tm-workflow", "v2")]),
&paths,
None,
true,
backups.path(),
);
assert!(
matches!(outcomes[0].action, RepairAction::SkippedUnverifiable(_)),
"got {:?}",
outcomes[0].action
);
assert_eq!(
fs::read_to_string(dest.join("my-custom-skill").join("SKILL.md")).unwrap(),
"operator content"
);
}
#[test]
fn repair_is_a_noop_when_everything_matches() {
let tmp = TempDir::new().unwrap();
let backups = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let _src = deploy_real(&paths.claude_skills_dir(), "tm-workflow", "v2", None);
let outcomes = repair_skills(
&reference_of(&[("tm-workflow", "v2")]),
&paths,
None,
false,
backups.path(),
);
assert!(outcomes.is_empty(), "outcomes: {outcomes:?}");
assert!(
!backups.path().join("operator-home").exists(),
"a no-op run must not create backup directories"
);
}
#[test]
fn repair_deletes_nothing() {
let tmp = TempDir::new().unwrap();
let backups = TempDir::new().unwrap();
let paths = paths_under(&tmp);
let dest = paths.claude_skills_dir();
let _src = deploy_real(&dest, "tm-workflow", "v1", None);
fs::write(dest.join("README.md"), "operator note").unwrap();
fs::create_dir_all(dest.join("unrelated-dir")).unwrap();
fs::write(dest.join("unrelated-dir").join("keep.txt"), "keep").unwrap();
repair_skills(
&reference_of(&[("tm-workflow", "v2")]),
&paths,
None,
true,
backups.path(),
);
assert!(dest.join("README.md").exists());
assert!(dest.join("unrelated-dir").join("keep.txt").exists());
assert!(dest.join("tm-workflow").join("SKILL.md").exists());
}
#[test]
fn backup_root_follows_the_remediation_convention() {
let now = chrono::DateTime::parse_from_rfc3339("2026-08-02T13:45:06Z")
.unwrap()
.with_timezone(&chrono::Utc);
let root = backup_root_for(Path::new("/home/x"), now);
assert_eq!(
root,
Path::new("/home/x/.trusty-mpm/backup-doctor-remediation-20260802-134506")
);
}
#[test]
fn embedded_reference_is_usable_as_a_repair_source() {
let reference = skill_reference(None);
assert!(
reference.assets.contains_key("tm-workflow"),
"the binary must embed tm-workflow for --fix-skills to repair it"
);
assert!(!reference.assets["tm-workflow"].is_empty());
}