use super::*;
use trusty_agents_common::agents::manifest::{AgentManifest, ManifestEntry, Origin, checksum};
use trusty_agents_common::agents::provenance::Provenance;
use trusty_agents_common::agents::tier_audit::TierResidentClass;
fn hermetic_paths(base: &Path) -> FrameworkPaths {
let mut paths = FrameworkPaths::under(base);
paths.trusty_mpm_root = None;
paths
}
fn doc(name: &str) -> String {
format!("---\nname: {name}\nrole: engineer\n---\n\nBody.\n")
}
fn place_agent(base: &Path, file_name: &str, body: &str) {
let dir = base.join(".claude").join("agents");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(file_name), body).unwrap();
}
fn track(base: &Path, file_name: &str, origin: Origin) {
let dir = base.join(".claude").join("agents");
let mut manifest = AgentManifest::default();
manifest.managed.insert(
file_name.to_owned(),
ManifestEntry {
source_chain: vec![],
checksum: checksum("whatever"),
deployed_at: "2026-07-31T00:00:00Z".to_owned(),
origin,
},
);
manifest.save(&dir).unwrap();
}
fn scan(label: &'static str, dir: &Path, names: &[&str]) -> TierScan {
TierScan {
label,
dir: dir.to_path_buf(),
outcome: TierOutcome::Scanned(
names
.iter()
.map(|s| MisplacedAgent {
path: dir.join(format!("{s}.md")),
name: (*s).to_owned(),
class: TierResidentClass::ShadowsBundled,
})
.collect(),
),
}
}
fn unscannable(label: &'static str, dir: &Path) -> TierScan {
TierScan {
label,
dir: dir.to_path_buf(),
outcome: TierOutcome::Unscannable(format!(
"cannot scan agent tier {}: Permission denied (os error 13)",
dir.display()
)),
}
}
#[test]
fn project_tier_stub_on_a_bundled_name_fails() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("home");
let project = tmp.path().join("project");
std::fs::create_dir_all(&home).unwrap();
std::fs::create_dir_all(&project).unwrap();
place_agent(
&project,
"rust-engineer.md",
"---\nname: rust-engineer\n---\n",
);
let paths = hermetic_paths(&home);
let check = check_asset_tier(&paths, Some(&project), &home);
assert_eq!(
check.status,
CheckStatus::Fail,
"message: {}",
check.message
);
assert!(check.message.contains("rust-engineer"), "{}", check.message);
assert!(check.message.contains("project tier"), "{}", check.message);
}
#[test]
fn clean_project_tier_is_ok() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("home");
let project = tmp.path().join("project");
std::fs::create_dir_all(&home).unwrap();
std::fs::create_dir_all(&project).unwrap();
let paths = hermetic_paths(&home);
let check = check_asset_tier(&paths, Some(&project), &home);
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}
#[test]
fn custom_project_agent_does_not_fire() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("home");
let project = tmp.path().join("project");
std::fs::create_dir_all(&home).unwrap();
std::fs::create_dir_all(&project).unwrap();
place_agent(
&project,
"acme-internal-reviewer.md",
&doc("acme-internal-reviewer"),
);
let paths = hermetic_paths(&home);
let check = check_asset_tier(&paths, Some(&project), &home);
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}
#[test]
fn user_owned_project_agent_on_a_bundled_name_does_not_fire() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("home");
let project = tmp.path().join("project");
std::fs::create_dir_all(&home).unwrap();
std::fs::create_dir_all(&project).unwrap();
place_agent(&project, "qa.md", &doc("qa"));
track(&project, "qa.md", Origin::User);
let paths = hermetic_paths(&home);
let check = check_asset_tier(&paths, Some(&project), &home);
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}
#[test]
fn a_renamed_project_file_declaring_a_bundled_name_fails() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("home");
let project = tmp.path().join("project");
std::fs::create_dir_all(&home).unwrap();
std::fs::create_dir_all(&project).unwrap();
place_agent(&project, "helper.md", &doc("qa"));
let paths = hermetic_paths(&home);
let check = check_asset_tier(&paths, Some(&project), &home);
assert_eq!(
check.status,
CheckStatus::Fail,
"message: {}",
check.message
);
assert!(check.message.contains("qa"), "{}", check.message);
}
#[test]
fn home_tier_copy_is_only_a_warn() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("home");
let project = tmp.path().join("project");
std::fs::create_dir_all(&home).unwrap();
std::fs::create_dir_all(&project).unwrap();
place_agent(&home, "qa.md", &doc("qa"));
let paths = hermetic_paths(&home);
let check = check_asset_tier(&paths, Some(&project), &home);
assert_eq!(
check.status,
CheckStatus::Warn,
"message: {}",
check.message
);
assert!(check.message.contains("qa"), "{}", check.message);
}
#[test]
fn canonical_tier_is_never_scanned() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("home");
std::fs::create_dir_all(&home).unwrap();
let paths = hermetic_paths(&home);
let canonical = paths.agent_deploy_dir();
std::fs::create_dir_all(&canonical).unwrap();
std::fs::write(canonical.join("rust-engineer.md"), doc("rust-engineer")).unwrap();
let check = check_asset_tier(&paths, None, &home);
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}
#[test]
fn roster_falls_back_to_the_embedded_bundle() {
let tmp = tempfile::tempdir().unwrap();
let paths = hermetic_paths(tmp.path());
assert!(!paths.agent_source_dir().is_dir());
let roster = crate::core::bundled_roster::bundled_roster_with(
&paths,
&crate::core::content_source::test_support::repo_roster(),
);
assert!(
roster.contains("rust-engineer"),
"embedded roster missing rust-engineer: {roster:?}"
);
}
#[test]
fn roster_keys_the_embedded_half_by_declared_name() {
let tmp = tempfile::tempdir().unwrap();
let paths = hermetic_paths(tmp.path());
let roster = crate::core::bundled_roster::bundled_roster_with(
&paths,
&crate::core::content_source::test_support::repo_roster(),
);
assert!(roster.contains("base-agent"), "roster: {roster:?}");
assert!(!roster.contains("BASE-AGENT"), "roster: {roster:?}");
}
#[test]
fn roster_includes_on_disk_source_names() {
let tmp = tempfile::tempdir().unwrap();
let paths = hermetic_paths(tmp.path());
let source = paths.agent_source_dir();
std::fs::create_dir_all(&source).unwrap();
std::fs::write(source.join("whatever.md"), doc("locally-installed")).unwrap();
assert!(
crate::core::bundled_roster::bundled_roster_with(
&paths,
&crate::core::content_source::test_support::repo_roster(),
)
.contains("locally-installed")
);
}
#[test]
fn verdict_project_hit_is_fail() {
let dir = PathBuf::from("/w/.claude/agents");
let canonical = PathBuf::from("/c/agents");
let check = verdict(&[scan("project", &dir, &["qa"])], &canonical, &[]);
assert_eq!(check.status, CheckStatus::Fail);
}
#[test]
fn verdict_home_only_is_warn() {
let dir = PathBuf::from("/h/.claude/agents");
let canonical = PathBuf::from("/c/agents");
let check = verdict(&[scan("operator home", &dir, &["qa"])], &canonical, &[]);
assert_eq!(check.status, CheckStatus::Warn);
}
#[test]
fn verdict_clean_is_ok() {
let dir = PathBuf::from("/w/.claude/agents");
let canonical = PathBuf::from("/c/agents");
let check = verdict(&[scan("project", &dir, &[])], &canonical, &[]);
assert_eq!(check.status, CheckStatus::Ok);
assert!(check.message.contains("/c/agents"), "{}", check.message);
}
#[test]
fn verdict_unscannable_tier_is_warn() {
let dir = PathBuf::from("/w/.claude/agents");
let canonical = PathBuf::from("/c/agents");
let check = verdict(&[unscannable("project", &dir)], &canonical, &[]);
assert_eq!(check.status, CheckStatus::Warn, "{}", check.message);
assert!(check.message.contains("UNDETERMINED"), "{}", check.message);
}
#[test]
fn verdict_unscannable_tier_is_not_reported_as_scanned() {
let scanned_dir = PathBuf::from("/h/.claude/agents");
let failed_dir = PathBuf::from("/w/.claude/agents");
let canonical = PathBuf::from("/c/agents");
let check = verdict(
&[
scan("operator home", &scanned_dir, &[]),
unscannable("project", &failed_dir),
],
&canonical,
&[],
);
let scanned_clause = check
.message
.split("Scanned: ")
.nth(1)
.unwrap_or_else(|| panic!("no Scanned: clause in {}", check.message));
assert!(
scanned_clause.contains("/h/.claude/agents"),
"the tier that WAS read must be listed: {}",
check.message
);
assert!(
!scanned_clause.contains("/w/.claude/agents"),
"the tier that failed must not be claimed as scanned: {}",
check.message
);
}
#[test]
fn verdict_names_the_files_and_both_tiers() {
let project = PathBuf::from("/w/.claude/agents");
let home = PathBuf::from("/h/.claude/agents");
let canonical = PathBuf::from("/c/agents");
let check = verdict(
&[
scan("project", &project, &["qa"]),
scan("operator home", &home, &["engineer"]),
],
&canonical,
&[],
);
assert_eq!(check.status, CheckStatus::Fail);
for needle in [
"/w/.claude/agents",
"/h/.claude/agents",
"/c/agents",
"qa",
"engineer",
"--reset-agents-workspaces",
] {
assert!(
check.message.contains(needle),
"missing {needle}: {}",
check.message
);
}
}
#[test]
fn verdict_summarises_a_long_list() {
let dir = PathBuf::from("/w/.claude/agents");
let canonical = PathBuf::from("/c/agents");
let names = ["a", "b", "c", "d", "e", "f"];
let check = verdict(&[scan("project", &dir, &names)], &canonical, &[]);
assert!(check.message.contains("(+1 more)"), "{}", check.message);
}
fn disagreement(
path: &str,
declared: Provenance,
ledger_framework_owned: bool,
) -> ProvenanceDisagreement {
ProvenanceDisagreement {
path: PathBuf::from(path),
declared,
ledger_framework_owned,
detail: format!(
"'{}' declares `provenance: {}` but the deployed-agent manifest records it as {}; \
the manifest wins (#4698)",
path,
declared.as_str(),
if ledger_framework_owned {
"framework-owned"
} else {
"not framework-owned"
}
),
}
}
#[test]
fn verdict_disagreement_alone_is_warn_not_ok() {
let dir = PathBuf::from("/w/.claude/agents");
let canonical = PathBuf::from("/c/agents");
let check = verdict(
&[scan("project", &dir, &[])],
&canonical,
&[disagreement(
"/c/agents/rust-engineer.md",
Provenance::UserAuthored,
true,
)],
);
assert_eq!(
check.status,
CheckStatus::Warn,
"a clean placement scan with a contradicting declaration is not Ok: {}",
check.message
);
for needle in [
"/c/agents/rust-engineer.md",
"user-authored",
"framework-owned",
"#4698",
] {
assert!(
check.message.contains(needle),
"message must carry `{needle}`: {}",
check.message
);
}
}
#[test]
fn verdict_reports_a_provenance_disagreement_alongside_shadowing() {
let dir = PathBuf::from("/w/.claude/agents");
let canonical = PathBuf::from("/c/agents");
let check = verdict(
&[scan("project", &dir, &["qa"])],
&canonical,
&[disagreement(
"/c/agents/qa.md",
Provenance::FrameworkOwned,
false,
)],
);
assert_eq!(check.status, CheckStatus::Fail, "{}", check.message);
assert!(
check.message.contains("/c/agents/qa.md"),
"the disagreement is not dropped when shadowing also fires: {}",
check.message
);
}
#[test]
fn verdict_clean_run_says_nothing_about_provenance() {
let dir = PathBuf::from("/w/.claude/agents");
let canonical = PathBuf::from("/c/agents");
let check = verdict(&[scan("project", &dir, &[])], &canonical, &[]);
assert_eq!(check.status, CheckStatus::Ok);
assert!(
!check.message.contains("provenance"),
"no finding, no mention: {}",
check.message
);
}
#[test]
fn check_asset_tier_reports_a_hand_edited_deployed_agent() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let paths = FrameworkPaths::for_managed_project(home.path(), project.path());
let canonical = paths.agent_deploy_dir();
std::fs::create_dir_all(&canonical).unwrap();
let body = "---\nname: qa\nrole: qa\nprovenance: user-authored\n---\n\nMine now.\n";
std::fs::write(canonical.join("qa.md"), body).unwrap();
let mut manifest = AgentManifest::default();
manifest.managed.insert(
"qa.md".to_owned(),
ManifestEntry {
source_chain: vec![],
checksum: checksum(body),
deployed_at: "2026-07-31T00:00:00Z".to_owned(),
origin: Origin::Bundled,
},
);
manifest.save(&canonical).unwrap();
let check = check_asset_tier(&paths, Some(project.path()), home.path());
assert_eq!(
check.status,
CheckStatus::Warn,
"the canonical tier is scanned for disagreements: {}",
check.message
);
assert!(
check.message.contains("qa.md") && check.message.contains("user-authored"),
"names the file and the declaration: {}",
check.message
);
}