use super::*;
use crate::core::agent_manifest::{AgentManifest, ManifestEntry, Origin, checksum};
use crate::core::skill_manifest::{SkillManifest, SkillManifestEntry};
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn write_agent(dir: &Path, stem: &str, body: &str) {
fs::create_dir_all(dir).unwrap();
fs::write(
dir.join(format!("{stem}.md")),
format!("---\nname: {stem}\nrole: {stem}\n---\n\n# {stem}\n\n{body}\n"),
)
.unwrap();
}
fn write_skill(dir: &Path, stem: &str, body: &str) {
fs::create_dir_all(dir).unwrap();
fs::write(
dir.join(format!("{stem}.md")),
format!("# {stem}\n\n{body}\n"),
)
.unwrap();
}
fn deployed_agent_matching(catalog_agents: &Path, stem: &str) -> AgentManifest {
let composed = compose_agent_for_deploy(stem, catalog_agents, &std::env::temp_dir()).unwrap();
let mut m = AgentManifest::default();
m.managed.insert(
format!("{stem}.md"),
ManifestEntry {
source_chain: vec![stem.to_owned()],
checksum: checksum(&composed),
deployed_at: "2026-06-17T00:00:00Z".to_owned(),
origin: Origin::Bundled,
},
);
m
}
fn deployed_skill_matching(catalog_skills: &Path, stem: &str) -> SkillManifest {
let body = fs::read_to_string(catalog_skills.join(format!("{stem}.md"))).unwrap();
let mut m = SkillManifest::default();
m.managed.insert(
stem.to_owned(),
SkillManifestEntry {
checksum: checksum(&body),
deployed_at: "2026-06-17T00:00:00Z".to_owned(),
},
);
m
}
#[test]
fn detect_unknown_when_never_synced() {
let root = TempDir::new().unwrap();
let agents = root.path().join("repo/.claude/agents");
let skills = root.path().join("repo/.claude/skills");
let report = detect_staleness(
&agents,
&skills,
&AgentManifest::default(),
&SkillManifest::default(),
&root.path().join("dep-agents"),
&root.path().join("dep-skills"),
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert!(report.unknown, "never-synced must be unknown");
assert!(!report.stale, "never-synced must not be stale");
assert!(report.changes.is_empty());
}
#[test]
fn detect_not_stale_when_identical() {
let root = TempDir::new().unwrap();
let agents = root.path().join("agents");
let skills = root.path().join("skills");
write_agent(&agents, "rust-engineer", "ENGINEER BODY");
write_skill(&skills, "tm-doctor", "DOCTOR BODY");
let dep_agents = deployed_agent_matching(&agents, "rust-engineer");
let dep_skills = deployed_skill_matching(&skills, "tm-doctor");
let report = detect_staleness(
&agents,
&skills,
&dep_agents,
&dep_skills,
&root.path().join("dep-agents"),
&root.path().join("dep-skills"),
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert!(!report.unknown, "synced catalog is not unknown");
assert!(!report.stale, "identical content is not stale: {report:?}");
assert!(report.changes.is_empty());
}
#[test]
fn detect_flags_changed_agent() {
let root = TempDir::new().unwrap();
let agents = root.path().join("agents");
let skills = root.path().join("skills");
write_agent(&agents, "rust-engineer", "ORIGINAL BODY");
let dep_agents = deployed_agent_matching(&agents, "rust-engineer");
write_agent(&agents, "rust-engineer", "UPSTREAM EDIT — NEW BEHAVIOUR");
let report = detect_staleness(
&agents,
&skills,
&dep_agents,
&SkillManifest::default(),
&root.path().join("dep-agents"),
&root.path().join("dep-skills"),
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert!(report.stale, "changed agent must be stale");
assert!(!report.unknown);
assert_eq!(report.changes.len(), 1, "exactly one change: {report:?}");
assert_eq!(report.changes[0].artifact, "agent");
assert_eq!(report.changes[0].name, "rust-engineer");
assert_eq!(report.changes[0].kind, ChangeKind::Changed);
}
#[test]
fn detect_flags_new_agent() {
let root = TempDir::new().unwrap();
let agents = root.path().join("agents");
write_agent(&agents, "newcomer", "BRAND NEW");
let report = detect_staleness(
&agents,
&root.path().join("skills"),
&AgentManifest::default(),
&SkillManifest::default(),
&root.path().join("dep-agents"),
&root.path().join("dep-skills"),
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert!(report.stale);
assert_eq!(report.changes[0].kind, ChangeKind::New);
assert_eq!(report.changes[0].name, "newcomer");
}
#[test]
fn detect_flags_new_skill() {
let root = TempDir::new().unwrap();
let skills = root.path().join("skills");
write_skill(&skills, "fresh-skill", "NEW SKILL");
let report = detect_staleness(
&root.path().join("agents"),
&skills,
&AgentManifest::default(),
&SkillManifest::default(),
&root.path().join("dep-agents"),
&root.path().join("dep-skills"),
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert!(report.stale);
assert_eq!(report.changes.len(), 1);
assert_eq!(report.changes[0].artifact, "skill");
assert_eq!(report.changes[0].name, "fresh-skill");
assert_eq!(report.changes[0].kind, ChangeKind::New);
}
#[test]
fn detect_flags_changed_skill() {
let root = TempDir::new().unwrap();
let skills = root.path().join("skills");
write_skill(&skills, "tm-doctor", "ORIGINAL");
let dep_skills = deployed_skill_matching(&skills, "tm-doctor");
write_skill(&skills, "tm-doctor", "UPSTREAM CHANGED");
let report = detect_staleness(
&root.path().join("agents"),
&skills,
&AgentManifest::default(),
&dep_skills,
&root.path().join("dep-agents"),
&root.path().join("dep-skills"),
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert!(report.stale);
assert_eq!(report.changes[0].kind, ChangeKind::Changed);
assert_eq!(report.changes[0].artifact, "skill");
}
#[test]
fn detect_respects_selection() {
let root = TempDir::new().unwrap();
let agents = root.path().join("agents");
write_agent(&agents, "rust-engineer", "BODY");
write_agent(&agents, "php-engineer", "BODY");
let dep_agents = deployed_agent_matching(&agents, "rust-engineer");
let report = detect_staleness(
&agents,
&root.path().join("skills"),
&dep_agents,
&SkillManifest::default(),
&root.path().join("dep-agents"),
&root.path().join("dep-skills"),
&std::env::temp_dir(),
|name| name == "rust-engineer", |_| true,
|_| false,
);
assert!(
!report.stale,
"a deselected new agent must not make the harness stale: {report:?}"
);
}
#[test]
fn detect_respects_ignore_staleness() {
let root = TempDir::new().unwrap();
let agents = root.path().join("agents");
write_agent(&agents, "rust-engineer", "BODY");
write_agent(&agents, "engineer", "BODY");
let dep_agents = deployed_agent_matching(&agents, "rust-engineer");
let report = detect_staleness(
&agents,
&root.path().join("skills"),
&dep_agents,
&SkillManifest::default(),
&root.path().join("dep-agents"),
&root.path().join("dep-skills"),
&std::env::temp_dir(),
|_| true, |_| true,
|name| name == "engineer", );
assert!(
!report.stale,
"an ignore_staleness agent must not make the harness stale: {report:?}"
);
}
#[test]
fn detect_caps_change_list() {
let root = TempDir::new().unwrap();
let agents = root.path().join("agents");
for i in 0..(MAX_CHANGES + 8) {
write_agent(&agents, &format!("agent{i:02}"), "BODY");
}
let report = detect_staleness(
&agents,
&root.path().join("skills"),
&AgentManifest::default(),
&SkillManifest::default(),
&root.path().join("dep-agents"),
&root.path().join("dep-skills"),
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert!(report.stale);
assert_eq!(
report.changes.len(),
MAX_CHANGES,
"change list must be capped"
);
}
#[test]
fn summary_lines_render_kind_and_name() {
let report = StalenessReport {
stale: true,
unknown: false,
changes: vec![
CatalogChange {
artifact: "agent",
name: "rust-engineer".into(),
kind: ChangeKind::Changed,
},
CatalogChange {
artifact: "skill",
name: "tm-doctor".into(),
kind: ChangeKind::New,
},
],
};
let lines = report.summary_lines();
assert_eq!(lines[0], "agent rust-engineer: changed");
assert_eq!(lines[1], "skill tm-doctor: new");
}
#[test]
fn detect_for_framework_unknown_without_catalog() {
let fw_root = TempDir::new().unwrap();
let fw = crate::core::paths::FrameworkPaths::under(fw_root.path());
let report = detect_for_framework(&fw, fw_root.path());
assert!(
report.unknown,
"bundled default with no catalog must be unknown: {report:?}"
);
assert!(!report.stale);
}
#[test]
fn detect_unknown_when_only_one_tree_missing_is_not_unknown() {
let root = TempDir::new().unwrap();
let agents = root.path().join("agents");
write_agent(&agents, "solo", "BODY");
let report = detect_staleness(
&agents,
&root.path().join("skills"), &AgentManifest::default(),
&SkillManifest::default(),
&root.path().join("dep-agents"),
&root.path().join("dep-skills"),
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert!(
!report.unknown,
"one present tree means synced, not unknown"
);
assert!(report.stale, "the present agent is new → stale");
}
fn deploy_agent_on_disk(catalog_agents: &Path, deployed_dir: &Path, stem: &str) {
let composed = compose_agent_for_deploy(stem, catalog_agents, &std::env::temp_dir()).unwrap();
fs::create_dir_all(deployed_dir).unwrap();
fs::write(deployed_dir.join(format!("{stem}.md")), composed).unwrap();
}
fn deploy_skill_on_disk(catalog_skills: &Path, deployed_dir: &Path, stem: &str) {
let body = fs::read_to_string(catalog_skills.join(format!("{stem}.md"))).unwrap();
let skill_dir = deployed_dir.join(stem);
fs::create_dir_all(&skill_dir).unwrap();
fs::write(skill_dir.join("SKILL.md"), body).unwrap();
}
#[test]
fn detect_reconciles_deployed_but_unmanifested() {
let root = TempDir::new().unwrap();
let agents = root.path().join("agents");
let skills = root.path().join("skills");
write_agent(&agents, "rust-engineer", "ENGINEER BODY");
write_skill(&skills, "tm-doctor", "DOCTOR BODY");
let dep_agents_dir = root.path().join("dep-agents");
let dep_skills_dir = root.path().join("dep-skills");
deploy_agent_on_disk(&agents, &dep_agents_dir, "rust-engineer");
deploy_skill_on_disk(&skills, &dep_skills_dir, "tm-doctor");
let report = detect_staleness(
&agents,
&skills,
&AgentManifest::default(),
&SkillManifest::default(),
&dep_agents_dir,
&dep_skills_dir,
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert!(!report.unknown);
assert!(
!report.stale,
"deployed-but-unmanifested roster must reconcile to 0 changes: {report:?}"
);
assert!(report.changes.is_empty());
}
#[test]
fn detect_user_owned_on_disk_not_reported() {
let root = TempDir::new().unwrap();
let agents = root.path().join("agents");
write_agent(&agents, "rust-engineer", "CATALOG BODY");
let dep_agents_dir = root.path().join("dep-agents");
fs::create_dir_all(&dep_agents_dir).unwrap();
fs::write(
dep_agents_dir.join("rust-engineer.md"),
"USER-OWNED DIFFERENT CONTENT",
)
.unwrap();
let report = detect_staleness(
&agents,
&root.path().join("skills"),
&AgentManifest::default(),
&SkillManifest::default(),
&dep_agents_dir,
&root.path().join("dep-skills"),
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert!(
!report.stale,
"a user-owned on-disk file `apply` never touches must not be drift: {report:?}"
);
}
#[test]
fn detect_user_modified_managed_not_reported() {
let root = TempDir::new().unwrap();
let agents = root.path().join("agents");
write_agent(&agents, "rust-engineer", "ORIGINAL");
let dep_agents = deployed_agent_matching(&agents, "rust-engineer");
write_agent(&agents, "rust-engineer", "UPSTREAM CHANGED");
let dep_agents_dir = root.path().join("dep-agents");
fs::create_dir_all(&dep_agents_dir).unwrap();
fs::write(dep_agents_dir.join("rust-engineer.md"), "USER HAND-EDIT").unwrap();
let report = detect_staleness(
&agents,
&root.path().join("skills"),
&dep_agents,
&SkillManifest::default(),
&dep_agents_dir,
&root.path().join("dep-skills"),
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert!(
!report.stale,
"a user-modified managed file `apply` skips must not be drift: {report:?}"
);
}
#[test]
fn classify_drift_reconciles_on_disk() {
let cat = checksum("catalog content");
assert_eq!(
super::classify_drift(&cat, None, Some(&checksum("catalog content"))),
None
);
let recorded = checksum("old content");
assert_eq!(
super::classify_drift(&cat, Some(&recorded), Some(&checksum("old content"))),
Some(ChangeKind::Changed)
);
assert_eq!(
super::classify_drift(&cat, Some(&recorded), Some(&checksum("user edit"))),
None
);
}
#[test]
fn classify_drift_new_when_absent() {
let cat = checksum("catalog content");
assert_eq!(
super::classify_drift(&cat, None, None),
Some(ChangeKind::New)
);
assert_eq!(super::classify_drift(&cat, Some(&cat), None), None);
}
#[test]
fn compute_agent_catalog_hashes_skips_compose_failures() {
let root = TempDir::new().unwrap();
let agents = root.path().join("agents");
write_agent(&agents, "good", "v1");
fs::write(agents.join("broken.md"), "---\nunterminated").unwrap();
let hashes = compute_agent_catalog_hashes(&agents, &std::env::temp_dir());
assert!(hashes.contains_key("good"));
assert!(
!hashes.contains_key("broken"),
"a compose failure must be skipped, not panic or poison the map"
);
}
#[test]
fn compute_skill_catalog_hashes_reads_bodies() {
let root = TempDir::new().unwrap();
let skills = root.path().join("skills");
write_skill(&skills, "tm-doctor", "v1");
let hashes = compute_skill_catalog_hashes(&skills);
assert_eq!(
hashes.get("tm-doctor"),
Some(&checksum("# tm-doctor\n\nv1\n"))
);
}
#[test]
fn catalog_hashes_compute_is_unknown_without_either_source() {
let root = TempDir::new().unwrap();
let cache = CatalogHashes::compute(
&root.path().join("no-agents"),
&root.path().join("no-skills"),
&std::env::temp_dir(),
);
let report = cache.detect(
&DeployedAgentHashes::with_manifest(
AgentManifest::default(),
&root.path().join("dep-agents"),
&cache,
),
&SkillManifest::default(),
&root.path().join("dep-skills"),
|_| true,
|_| true,
|_| false,
);
assert!(report.unknown);
assert!(!report.stale);
}
#[test]
fn catalog_hashes_compute_is_unknown_for_an_unresolvable_skills_root() {
let root = TempDir::new().unwrap();
let agents = root.path().join("agents");
write_agent(&agents, "rust-engineer", "v1");
let cache = CatalogHashes::compute(
&agents,
&root.path().join("no-skills"),
Path::new("relative"),
);
assert!(cache.unknown, "an unresolvable root must not read as known");
}
#[test]
fn placeholder_body_is_fresh_only_against_its_deployed_skills_root() {
use crate::core::agent_deployer::deploy_agents_filtered;
use trusty_agents_common::agents::skill_root::SKILLS_ROOT_PLACEHOLDER;
let fw_root = TempDir::new().unwrap();
let fw = crate::core::paths::FrameworkPaths::under(fw_root.path());
let catalog_root = crate::content::catalog_root_for(&fw.root);
let sources = crate::core::manifest::ManifestSources::resolve(fw_root.path(), &catalog_root);
let manifest = crate::core::manifest::resolve_manifest(&sources);
let plan = crate::core::manifest::HarnessPlan::from_manifest(&manifest, &fw, &catalog_root);
write_agent(
&plan.agent_source,
"rust-engineer",
&format!("Read `{SKILLS_ROOT_PLACEHOLDER}/self-improvement-loop/SKILL.md`."),
);
let deployed = deploy_agents_filtered(
&plan.agent_source,
&fw.agent_deploy_dir(),
&fw.skill_deploy_dir(),
|_| true,
)
.unwrap();
assert_eq!(deployed.deployed.len(), 1, "{deployed:?}");
let report = detect_for_framework(&fw, fw_root.path());
assert!(!report.unknown, "{report:?}");
assert!(
!report.stale,
"deployed with its own root must be fresh: {report:?}"
);
let other_root = fw_root.path().join("other-skills");
let catalog = CatalogHashes::compute(&plan.agent_source, &plan.skill_source, &other_root);
let agents_dir = fw.agent_deploy_dir();
let report = catalog.detect(
&DeployedAgentHashes::read(&agents_dir, &catalog),
&SkillManifest::default(),
&fw.claude_skills_dir(),
|name| name == "rust-engineer",
|_| false,
|_| false,
);
assert!(
report.stale,
"a different skills root must read as stale: {report:?}"
);
}
#[test]
fn catalog_hashes_shared_across_two_deployed_targets() {
let root = TempDir::new().unwrap();
let catalog_agents = root.path().join("catalog/agents");
let catalog_skills = root.path().join("catalog/skills");
write_agent(&catalog_agents, "rust-engineer", "v1");
write_skill(&catalog_skills, "tm-doctor", "v1");
let a_agents_dir = root.path().join("a/agents");
let a_skills_dir = root.path().join("a/skills");
let a_agent_manifest = deployed_agent_matching(&catalog_agents, "rust-engineer");
let a_skill_manifest = deployed_skill_matching(&catalog_skills, "tm-doctor");
let b_agents_dir = root.path().join("b/agents");
let b_skills_dir = root.path().join("b/skills");
let mut b_agent_manifest = AgentManifest::default();
b_agent_manifest.managed.insert(
"rust-engineer.md".to_string(),
crate::core::agent_manifest::ManifestEntry {
source_chain: vec!["rust-engineer".to_string()],
checksum: checksum("stale composed content"),
deployed_at: "2026-01-01T00:00:00Z".to_string(),
origin: crate::core::agent_manifest::Origin::Bundled,
},
);
fs::create_dir_all(&b_agents_dir).unwrap();
fs::write(
b_agents_dir.join("rust-engineer.md"),
"stale composed content",
)
.unwrap();
let b_skill_manifest = SkillManifest::default();
let cache = CatalogHashes::compute(&catalog_agents, &catalog_skills, &std::env::temp_dir());
let report_a = cache.detect(
&DeployedAgentHashes::with_manifest(a_agent_manifest.clone(), &a_agents_dir, &cache),
&a_skill_manifest,
&a_skills_dir,
|_| true,
|_| true,
|_| false,
);
assert!(
!report_a.stale,
"target A matches the catalog: {report_a:?}"
);
let report_b = cache.detect(
&DeployedAgentHashes::with_manifest(b_agent_manifest.clone(), &b_agents_dir, &cache),
&b_skill_manifest,
&b_skills_dir,
|_| true,
|_| true,
|_| false,
);
assert!(
report_b.stale,
"target B's agent is stale relative to the shared catalog: {report_b:?}"
);
assert!(
report_b.changes.iter().any(|c| c.name == "rust-engineer"),
"the stale agent must be named: {report_b:?}"
);
let fresh_a = detect_staleness(
&catalog_agents,
&catalog_skills,
&a_agent_manifest,
&a_skill_manifest,
&a_agents_dir,
&a_skills_dir,
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert_eq!(fresh_a.stale, report_a.stale);
let fresh_b = detect_staleness(
&catalog_agents,
&catalog_skills,
&b_agent_manifest,
&b_skill_manifest,
&b_agents_dir,
&b_skills_dir,
&std::env::temp_dir(),
|_| true,
|_| true,
|_| false,
);
assert_eq!(fresh_b.stale, report_b.stale);
}
#[test]
fn single_target_detect_reads_only_selected_agents() {
let root = TempDir::new().unwrap();
let catalog_agents = root.path().join("single-target/catalog/agents");
let catalog_skills = root.path().join("single-target/catalog/skills");
let deployed_agents_dir = root.path().join("single-target/deployed/agents");
let deployed_skills_dir = root.path().join("single-target/deployed/skills");
for stem in ["alpha", "beta", "gamma", "delta"] {
write_agent(&catalog_agents, stem, "v1");
write_agent(&deployed_agents_dir, stem, "v1");
}
write_skill(&catalog_skills, "tm-doctor", "v1");
let _ = detect_staleness(
&catalog_agents,
&catalog_skills,
&AgentManifest::default(),
&SkillManifest::default(),
&deployed_agents_dir,
&deployed_skills_dir,
&std::env::temp_dir(),
|name| name == "alpha",
|_| true,
|_| false,
);
assert_eq!(
super::deployed_reads_under(&deployed_agents_dir),
1,
"a single-target caller selecting 1 of 4 catalog agents must read \
exactly 1 deployed agent file. Observing 4 means the eager fleet-path \
snapshot leaked onto the single-target path and increased its I/O \
(#4619 review MEDIUM)"
);
}