use std::fs;
use std::path::Path;
use std::time::{Duration, SystemTime};
use tempfile::TempDir;
use crate::channels::telegram::menu_refresh::skills_signature_from;
const VALID_SKILL: &str = "---\nname: demo\ndescription: demo skill\n---\nbody\n";
fn write_skill(root: &Path, name: &str, body: &str) {
let dir = root.join(name);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("SKILL.md"), body).unwrap();
}
fn set_mtime(path: &Path, offset_secs: u64) {
let file = fs::File::options().write(true).open(path).unwrap();
file.set_modified(SystemTime::now() + Duration::from_secs(offset_secs))
.unwrap();
}
#[test]
fn signature_is_stable_when_nothing_changes() {
let user = TempDir::new().unwrap();
let projects = TempDir::new().unwrap();
write_skill(user.path(), "alpha", VALID_SKILL);
let first = skills_signature_from(user.path(), projects.path());
let second = skills_signature_from(user.path(), projects.path());
assert_eq!(first, second, "unchanged dirs must hash identically");
}
#[test]
fn signature_changes_on_add_edit_remove() {
let user = TempDir::new().unwrap();
let projects = TempDir::new().unwrap();
write_skill(user.path(), "alpha", VALID_SKILL);
let base = skills_signature_from(user.path(), projects.path());
write_skill(user.path(), "beta", VALID_SKILL);
let with_beta = skills_signature_from(user.path(), projects.path());
assert_ne!(base, with_beta, "adding a skill must change the signature");
let beta_md = user.path().join("beta").join("SKILL.md");
fs::write(
&beta_md,
"---\nname: beta\ndescription: edited\n---\nnew body\n",
)
.unwrap();
set_mtime(&beta_md, 10);
let edited = skills_signature_from(user.path(), projects.path());
assert_ne!(
with_beta, edited,
"editing a SKILL.md must change the signature"
);
fs::remove_dir_all(user.path().join("beta")).unwrap();
let removed = skills_signature_from(user.path(), projects.path());
assert_eq!(
base, removed,
"removing the added skill must restore the base signature"
);
}
#[test]
fn signature_ignores_non_skill_files() {
let user = TempDir::new().unwrap();
let projects = TempDir::new().unwrap();
write_skill(user.path(), "alpha", VALID_SKILL);
let base = skills_signature_from(user.path(), projects.path());
fs::write(user.path().join("README.md"), "not a skill").unwrap();
fs::write(user.path().join("alpha").join("notes.txt"), "not SKILL.md").unwrap();
let polluted = skills_signature_from(user.path(), projects.path());
assert_eq!(
base, polluted,
"non-skill files must not change the signature"
);
}
#[test]
#[cfg(unix)]
fn signature_follows_symlinked_skill_dirs() {
let real = TempDir::new().unwrap();
write_skill(real.path(), "linked", VALID_SKILL);
let linked_md = real.path().join("linked").join("SKILL.md");
set_mtime(&linked_md, 20);
let user = TempDir::new().unwrap();
let projects = TempDir::new().unwrap();
let empty = skills_signature_from(user.path(), projects.path());
std::os::unix::fs::symlink(real.path().join("linked"), user.path().join("linked")).unwrap();
let with_link = skills_signature_from(user.path(), projects.path());
assert_ne!(
empty, with_link,
"a symlinked skill must be visible to the signature"
);
fs::write(
&linked_md,
"---\nname: linked\ndescription: edited\n---\nchanged\n",
)
.unwrap();
set_mtime(&linked_md, 30);
let after_edit = skills_signature_from(user.path(), projects.path());
assert_ne!(
with_link, after_edit,
"editing a symlinked skill's target must change the signature"
);
}
#[test]
fn signature_covers_project_overlays() {
let user = TempDir::new().unwrap();
let projects = TempDir::new().unwrap();
let empty = skills_signature_from(user.path(), projects.path());
let project_skills = projects.path().join("myproj").join("skills");
write_skill(&project_skills, "proj-skill", VALID_SKILL);
let with_project = skills_signature_from(user.path(), projects.path());
assert_ne!(
empty, with_project,
"a project-overlay skill must be visible to the signature"
);
write_skill(
&projects.path().join("other").join("skills"),
"proj-skill",
VALID_SKILL,
);
let two_projects = skills_signature_from(user.path(), projects.path());
assert_ne!(
with_project, two_projects,
"same-named skills in different projects must not collide"
);
}