mod common;
use std::fs;
use std::path::Path;
use common::{TempDir, installer, into_dir, skills};
use skill_embed::{Action, Error, InstallOptions, SKILL_FILE, State};
fn copy_tree(from: &Path, to: &Path) {
fs::create_dir_all(to).expect("the destination");
for entry in fs::read_dir(from).expect("the source") {
let entry = entry.expect("an entry");
let target = to.join(entry.file_name());
if entry.path().is_dir() {
copy_tree(&entry.path(), &target);
} else {
fs::copy(entry.path(), &target).expect("the copy");
}
}
}
fn row<'a>(
results: &'a [skill_embed::InstallResult],
name: &str,
) -> Option<&'a skill_embed::InstallResult> {
results.iter().find(|r| r.skill == name)
}
fn before() -> skill_embed::SkillSet {
skills(&["bare-skill", "demo-skill"])
}
fn after() -> skill_embed::SkillSet {
skills(&["demo-skill"])
}
#[test]
fn an_upgrade_removes_a_skill_the_binary_no_longer_carries() {
let tmp = TempDir::new("sweep-upgrade");
let dest = tmp.join("skills");
let options = into_dir(&dest);
installer(before()).install(&options).1.expect("the first install");
assert!(dest.join("bare-skill").exists(), "the first install did not land");
let next = installer(after());
let statuses = next.status(&options).expect("status");
let orphan = statuses.iter().find(|st| st.skill == "bare-skill").expect("a row");
assert_eq!(orphan.state, State::Orphaned);
assert_eq!(orphan.installed_by.as_deref(), Some("mytool"));
let named = InstallOptions { names: vec!["demo-skill".to_owned()], ..options.clone() };
next.install(&named).1.expect("the named run");
assert!(
dest.join("bare-skill").exists(),
"`install demo-skill` removed a skill it was not asked about"
);
let dry = InstallOptions { dry_run: true, ..options.clone() };
let (results, outcome) = next.install(&dry);
outcome.expect("the dry run");
assert_eq!(row(&results, "bare-skill").expect("a row").action, Action::Removed);
assert!(dest.join("bare-skill").exists(), "the dry run removed it");
let (results, outcome) = next.install(&options);
outcome.expect("the full install");
let swept = row(&results, "bare-skill").expect("a row");
assert_eq!(swept.action, Action::Removed);
assert_eq!(swept.reason.as_deref(), Some("no longer embedded in mytool"));
assert!(!dest.join("bare-skill").exists(), "the dropped skill survived");
assert!(dest.join("demo-skill").exists(), "the skill still carried was removed too");
}
#[test]
fn uninstall_takes_orphans_with_it() {
let tmp = TempDir::new("sweep-uninstall");
let dest = tmp.join("skills");
let options = into_dir(&dest);
installer(before()).install(&options).1.expect("the first install");
installer(after()).uninstall(&options).1.expect("the uninstall");
for name in ["demo-skill", "bare-skill"] {
assert!(!dest.join(name).exists(), "uninstall left {name} behind");
}
}
#[test]
fn the_sweep_claims_only_this_tools_own_work() {
let tmp = TempDir::new("sweep-claims");
let dest = tmp.join("skills");
let options = into_dir(&dest);
installer(before()).install(&options).1.expect("the first install");
fs::create_dir_all(dest.join("handwritten")).expect("a directory");
fs::write(dest.join("handwritten").join(SKILL_FILE), "---\nname: handwritten\n---\nmine\n")
.expect("a manifest");
fs::create_dir_all(dest.join("not-a-skill")).expect("a directory");
fs::write(dest.join("notes.md"), "mine\n").expect("a file");
let other_dir = tmp.join("othertool");
installer(skills(&["othertools-skill"]))
.with_tool_name("othertool")
.install(&into_dir(&other_dir))
.1
.expect("the other tool's install");
copy_tree(&other_dir.join("othertools-skill"), &dest.join("othertools-skill"));
let forced = InstallOptions { force: true, ..options };
installer(after()).install(&forced).1.expect("the forced install");
for name in ["handwritten", "othertools-skill", "not-a-skill", "notes.md"] {
assert!(dest.join(name).exists(), "the sweep took {name}, which is not this tool's");
}
}
#[test]
fn an_edited_fork_is_never_swept() {
let tmp = TempDir::new("sweep-fork");
let dest = tmp.join("skills");
let options = into_dir(&dest);
installer(before()).install(&options).1.expect("the first install");
let fork = dest.join("bare-skill-mine");
copy_tree(&dest.join("bare-skill"), &fork);
let body = fs::read_to_string(fork.join(SKILL_FILE)).expect("the fork's manifest");
fs::write(fork.join(SKILL_FILE), body + "\nMy own additions.\n").expect("the edit");
let next = installer(after());
for force in [false, true] {
let o = InstallOptions { force, ..options.clone() };
for (label, results) in
[("install", next.install(&o).0), ("uninstall", next.uninstall(&o).0)]
{
assert!(
!results.iter().any(|r| r.path == fork),
"{label} (force={force}) reported the fork"
);
assert!(fork.join(SKILL_FILE).exists(), "{label} (force={force}) took the fork");
}
}
}
#[test]
fn a_spelling_of_an_installed_skill_is_not_swept_away() {
let tmp = TempDir::new("sweep-case");
let probe = tmp.mkdir("CaseProbe");
let folds = tmp.join("caseprobe").exists();
fs::remove_dir_all(probe).expect("the probe");
if !folds {
eprintln!("this file system does not fold case, so the two names are two directories");
return;
}
let dest = tmp.join("skills");
let options = into_dir(&dest);
let skills = installer(before());
skills.install(&options).1.expect("the first install");
fs::rename(dest.join("bare-skill"), dest.join("Bare-Skill")).expect("the rename");
let (results, outcome) = skills.install(&options);
outcome.expect("the second install");
assert!(
!results.iter().any(|r| r.action == Action::Removed),
"the run removed the skill it had just installed: {results:?}"
);
assert!(dest.join("bare-skill").join(SKILL_FILE).exists(), "the installed skill is gone");
}
#[test]
fn the_sweep_leaves_a_rescued_installation_alone() {
let tmp = TempDir::new("sweep-rescue");
let dest = tmp.join("skills");
let options = into_dir(&dest);
installer(before()).install(&options).1.expect("the first install");
let rescued = dest.join(".bare-skill.old-1234");
fs::rename(dest.join("bare-skill"), &rescued).expect("the rename");
let (results, outcome) = installer(after()).install(&options);
outcome.expect("the install");
assert!(!results.iter().any(|r| r.path == rescued), "the sweep reported the rescue copy");
assert!(
rescued.join(SKILL_FILE).exists(),
"the rescue copy the error told the user to recover is gone"
);
}
#[test]
fn an_orphan_can_be_named() {
let tmp = TempDir::new("sweep-named");
let dest = tmp.join("skills");
let options = into_dir(&dest);
installer(before()).install(&options).1.expect("the first install");
let next = installer(after());
let named = InstallOptions { names: vec!["bare-skill".to_owned()], ..options.clone() };
let (results, outcome) = next.uninstall(&named);
outcome.expect("naming an orphan");
assert_eq!(row(&results, "bare-skill").expect("a row").action, Action::Removed);
assert!(!dest.join("bare-skill").exists(), "the named orphan survived");
assert!(dest.join("demo-skill").exists(), "a skill that was not named was removed");
let unknown = InstallOptions { names: vec!["never-existed".to_owned()], ..options };
assert!(matches!(next.status(&unknown), Err(Error::UnknownSkill { .. })));
}