use super::*;
use tempfile::TempDir;
fn project_dir(base: &Path) -> std::path::PathBuf {
let dir = base.join("workspace");
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn seed_framework(base: &Path) -> FrameworkPaths {
let fw = FrameworkPaths::under(base);
std::fs::create_dir_all(&fw.agents).unwrap();
std::fs::create_dir_all(&fw.skills).unwrap();
let agents: &[(&str, &str)] = &[
(
"BASE-AGENT.md",
"---\nname: BASE-AGENT\ndescription: base\n---\n\nBase.\n",
),
(
"engineer.md",
"---\nname: engineer\ndescription: implementation specialist\n---\n\nEngineer.\n",
),
(
"rust-engineer.md",
"---\nname: rust-engineer\ndescription: rust specialist\n---\n\nRust.\n",
),
(
"research.md",
"---\nname: research\ndescription: research specialist\n---\n\nResearch.\n",
),
(
"qa.md",
"---\nname: qa\ndescription: quality specialist\n---\n\nQA.\n",
),
(
"version-control.md",
"---\nname: version-control\ndescription: git specialist\n---\n\nVCS.\n",
),
(
"ticketing.md",
"---\nname: ticketing\ndescription: ticketing specialist\n---\n\nTickets.\n",
),
];
for (name, body) in agents {
std::fs::write(fw.agents.join(name), body).unwrap();
}
std::fs::write(
fw.skills.join("tm-doctor.md"),
"---\nname: tm-doctor\ndescription: doctor\n---\n\nDoctor.\n",
)
.unwrap();
fw
}
#[test]
fn ensure_managed_config_dir_deploys_full_roster() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
assert!(config_dir.join("settings.json").exists());
assert!(config_dir.join(".mcp.json").exists());
let agents_dir = config_dir.join("agents");
for name in [
"engineer",
"rust-engineer",
"research",
"qa",
"version-control",
"ticketing",
] {
let path = agents_dir.join(format!("{name}.md"));
assert!(path.exists(), "agent {name} must be deployed to config dir");
let body = std::fs::read_to_string(&path).unwrap();
assert!(
body.contains("description:"),
"deployed agent {name} must carry a description (spawnable)"
);
}
assert!(
config_dir.join("skills/tm-doctor/SKILL.md").exists(),
"tm-* skills must be deployed to config dir"
);
}
#[test]
fn ensure_managed_config_dir_is_idempotent() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
let first = std::fs::read_to_string(config_dir.join("agents/engineer.md")).unwrap();
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
let second = std::fs::read_to_string(config_dir.join("agents/engineer.md")).unwrap();
assert_eq!(first, second, "re-provisioning must be idempotent");
}
#[test]
fn ensure_managed_config_dir_refreshes_stale_bundled_agents() {
let tmp = TempDir::new().unwrap();
let fw = FrameworkPaths::under(tmp.path());
std::fs::create_dir_all(&fw.agents).unwrap();
std::fs::write(
fw.agents.join("BASE-AGENT.md"),
"---\nname: BASE-AGENT\ndescription: base\n---\n\nSTALE-SENTINEL-4840\n",
)
.unwrap();
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
assert_eq!(
std::fs::read_to_string(fw.agents.join("BASE-AGENT.md")).unwrap(),
crate::core::bundle::BASE_AGENT,
"the bundled agent SOURCE must be re-materialized from the running binary"
);
let deployed = std::fs::read_to_string(config_dir.join("agents/BASE-AGENT.md")).unwrap();
assert!(
!deployed.contains("STALE-SENTINEL-4840"),
"the stale composition must not survive into the deploy target"
);
}
#[test]
fn ensure_managed_config_dir_survives_an_unwritable_agent_target() {
let tmp = TempDir::new().unwrap();
let fw = FrameworkPaths::under(tmp.path());
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
std::fs::create_dir_all(&config_dir).unwrap();
std::fs::write(config_dir.join("agents"), "blocking file\n").unwrap();
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path()))
.expect("an undeployable agent roster must not fail provisioning");
assert!(
config_dir.join("settings.json").exists(),
"the rest of the config dir must still be provisioned"
);
}
#[test]
fn ensure_managed_config_dir_deploys_user_tier_skill() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
std::fs::create_dir_all(&fw.user_skills).unwrap();
std::fs::write(
fw.user_skills.join("my-custom-skill.md"),
"---\nname: my-custom-skill\n---\n\nUSER CUSTOM.\n",
)
.unwrap();
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
assert!(
config_dir.join("skills/my-custom-skill/SKILL.md").exists(),
"user-custom skill must be deployed to the tm-global config dir"
);
}
#[test]
#[ignore = "static verification — composes the full real roster; run explicitly"]
fn manual_static_verify_full_roster() {
let tmp = TempDir::new().unwrap();
let fw = FrameworkPaths::under(tmp.path());
std::fs::create_dir_all(&fw.agents).unwrap();
std::fs::create_dir_all(&fw.skills).unwrap();
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
for (src, dst) in [
(manifest.join("src/assets/agents"), &fw.agents),
(manifest.join("src/assets/skills"), &fw.skills),
] {
for entry in std::fs::read_dir(&src).unwrap() {
let entry = entry.unwrap();
if entry.path().extension().and_then(|e| e.to_str()) == Some("md") {
std::fs::copy(entry.path(), dst.join(entry.file_name())).unwrap();
}
}
}
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
let agents_dir = config_dir.join("agents");
let mut deployed: Vec<String> = std::fs::read_dir(&agents_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("md"))
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
deployed.sort();
println!("\n=== deployed agents ({}) ===", deployed.len());
let mut missing_desc = Vec::new();
for name in &deployed {
let body = std::fs::read_to_string(agents_dir.join(name)).unwrap();
let has_desc = body.contains("description:");
println!(" {name} spawnable={has_desc}");
if !has_desc && !name.starts_with("BASE-") {
missing_desc.push(name.clone());
}
}
for required in [
"engineer.md",
"rust-engineer.md",
"research.md",
"qa.md",
"local-ops.md",
"version-control.md",
"ticketing.md",
"documentation.md",
"security.md",
] {
assert!(
deployed.iter().any(|d| d == required),
"required agent {required} missing from deployed roster: {deployed:?}"
);
}
assert!(
missing_desc.is_empty(),
"these deployed agents are not spawnable (no description): {missing_desc:?}"
);
}
fn pin_skill_source_stamp(fw: &FrameworkPaths) {
std::fs::create_dir_all(&fw.skills).unwrap();
std::fs::write(
fw.skills.join(crate::core::skill_source::STAMP_FILE_NAME),
crate::core::skill_source::skill_bundle_stamp(),
)
.unwrap();
}
fn seed_skill(fw: &FrameworkPaths, stem: &str, body: &str) {
std::fs::create_dir_all(&fw.skills).unwrap();
std::fs::write(fw.skills.join(format!("{stem}.md")), body).unwrap();
}
#[test]
fn pinning_the_stamp_preserves_a_seeded_skill_source() {
let tmp = TempDir::new().unwrap();
let fw = FrameworkPaths::under(tmp.path());
pin_skill_source_stamp(&fw);
seed_skill(&fw, "probe-skill", "---\nname: probe-skill\n---\n\nV1\n");
crate::core::skill_source::ensure_skill_source_fresh(&fw).unwrap();
assert_eq!(
std::fs::read_to_string(fw.skills.join("probe-skill.md")).unwrap(),
"---\nname: probe-skill\n---\n\nV1\n",
"a pinned-current source must not be re-materialized from the bundle"
);
}
#[test]
fn ensure_managed_config_dir_refreshes_a_stale_managed_skill() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
pin_skill_source_stamp(&fw);
seed_skill(&fw, "probe-skill", "---\nname: probe-skill\n---\n\nV1\n");
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
let deployed = config_dir.join("skills/probe-skill/SKILL.md");
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
assert_eq!(
std::fs::read_to_string(&deployed).unwrap(),
"---\nname: probe-skill\n---\n\nV1\n"
);
seed_skill(
&fw,
"probe-skill",
"---\nname: probe-skill\n---\n\nV2-REFRESHED\n",
);
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
assert_eq!(
std::fs::read_to_string(&deployed).unwrap(),
"---\nname: probe-skill\n---\n\nV2-REFRESHED\n",
"a managed, user-unmodified skill must be refreshed on every run, not only on `tm install`"
);
}
#[test]
fn ensure_managed_config_dir_skill_deploy_is_a_noop_when_unchanged() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
pin_skill_source_stamp(&fw);
seed_skill(&fw, "probe-skill", "---\nname: probe-skill\n---\n\nV1\n");
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
let deployed = config_dir.join("skills/probe-skill/SKILL.md");
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
let before = std::fs::metadata(&deployed).unwrap().modified().unwrap();
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
let after = std::fs::metadata(&deployed).unwrap().modified().unwrap();
assert_eq!(
before, after,
"an unchanged skill must take the manifest checksum no-op path, not a rewrite"
);
}
#[test]
fn ensure_managed_config_dir_preserves_a_project_custom_skill() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
pin_skill_source_stamp(&fw);
seed_skill(
&fw,
"probe-skill",
"---\nname: probe-skill\n---\n\nBUNDLED\n",
);
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
let custom = config_dir.join("skills/probe-skill/SKILL.md");
std::fs::create_dir_all(custom.parent().unwrap()).unwrap();
std::fs::write(&custom, "PROJECT CUSTOM — DO NOT TOUCH\n").unwrap();
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
assert_eq!(
std::fs::read_to_string(&custom).unwrap(),
"PROJECT CUSTOM — DO NOT TOUCH\n",
"an unmanaged on-disk skill must survive the every-run deploy untouched"
);
}
#[test]
fn ensure_managed_config_dir_skips_a_frozen_skill() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
let (config_dir, deployed) = frozen_skill_fixture(&tmp, &fw);
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
assert_eq!(
std::fs::read_to_string(&deployed).unwrap(),
"HAND EDITED BY THE OPERATOR\n",
"a checksum-frozen skill must still be preserved — `tm doctor --fix-skills \
--include-frozen` remains the only way to adopt it"
);
}
fn frozen_skill_fixture(
tmp: &TempDir,
fw: &FrameworkPaths,
) -> (std::path::PathBuf, std::path::PathBuf) {
pin_skill_source_stamp(fw);
seed_skill(fw, "probe-skill", "---\nname: probe-skill\n---\n\nV1\n");
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
let deployed = config_dir.join("skills/probe-skill/SKILL.md");
ensure_managed_config_dir_with_root(fw, &config_dir, &project_dir(tmp.path())).unwrap();
std::fs::write(&deployed, "HAND EDITED BY THE OPERATOR\n").unwrap();
seed_skill(fw, "probe-skill", "---\nname: probe-skill\n---\n\nV2\n");
(config_dir, deployed)
}
#[test]
#[serial_test::serial]
fn ensure_managed_config_dir_emits_the_frozen_skill_warning() {
use tracing_subscriber::layer::SubscriberExt;
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
let (config_dir, _deployed) = frozen_skill_fixture(&tmp, &fw);
let buffer = trusty_common::log_buffer::LogBuffer::new(64);
let subscriber = tracing_subscriber::registry().with(
trusty_common::log_buffer::LogBufferLayer::new(buffer.clone()),
);
tracing::subscriber::with_default(subscriber, || {
ensure_managed_config_dir_with_root(&fw, &config_dir, &project_dir(tmp.path())).unwrap();
});
let lines = buffer.tail(64);
let hit = lines
.iter()
.find(|l| l.contains("managed config dir:") && l.contains("probe-skill"));
let line = hit.unwrap_or_else(|| {
panic!(
"provisioning declined `probe-skill` but emitted no warning naming it — \
the emit block in `ensure_managed_config_dir_with_root` is missing or \
unreachable. Captured lines: {lines:#?}"
)
});
assert!(
line.contains("WARN"),
"the declined-skill line must be logged at WARN, not a lower level: {line}"
);
assert!(
line.contains("tm doctor --fix-skills --include-frozen"),
"the emitted warning must carry the actionable remedy: {line}"
);
}
#[test]
fn a_declined_skill_reaches_skill_skip_summary_from_a_real_deploy() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
let (config_dir, _deployed) = frozen_skill_fixture(&tmp, &fw);
let stats = deploy_all_skill_tiers(
&fw.skill_source_dir(),
&fw.user_skill_source_dir(),
&config_dir.join("skills"),
|_| true,
)
.unwrap();
assert!(
stats.stats.skipped.iter().any(|s| s == "probe-skill"),
"a checksum-frozen skill must be reported as skipped: {:?}",
stats.stats.skipped
);
let line = skill_skip_summary(&stats.stats.skipped)
.expect("a declined skill must produce exactly one summary line");
assert!(
line.contains("probe-skill") && line.contains("tm doctor --fix-skills --include-frozen"),
"{line}"
);
}
#[test]
fn skill_skip_summary_is_none_on_a_clean_deploy() {
assert!(skill_skip_summary(&[]).is_none());
}
#[test]
fn skill_skip_summary_counts_and_previews() {
let line = skill_skip_summary(&["tm".to_string(), "code-review-standards".to_string()])
.expect("a non-empty skip set must summarise");
assert!(line.contains("2 skill file(s)"), "{line}");
assert!(line.contains("tm, code-review-standards"), "{line}");
}
#[test]
fn skill_skip_summary_elides_beyond_five() {
let many: Vec<String> = (0..9).map(|i| format!("skill-{i}")).collect();
let line = skill_skip_summary(&many).expect("summary");
assert!(line.contains("9 skill file(s)"), "{line}");
assert!(line.contains("skill-4, …"), "{line}");
assert!(
!line.contains("skill-5"),
"beyond the preview limit: {line}"
);
}
#[test]
fn ensure_managed_config_dir_deploys_the_project_skill_tier() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
pin_skill_source_stamp(&fw);
seed_skill(&fw, "probe-skill", "---\nname: probe-skill\n---\n\nV1\n");
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
let workspace = project_dir(tmp.path());
let project_copy = workspace
.join(".claude")
.join("skills")
.join("probe-skill")
.join("SKILL.md");
ensure_managed_config_dir_with_root(&fw, &config_dir, &workspace).unwrap();
assert_eq!(
std::fs::read_to_string(&project_copy).unwrap(),
"---\nname: probe-skill\n---\n\nV1\n",
"the project tier — which OUTRANKS the config-dir tier — must be deployed \
by the shared choke point, not only by `prepare_session`"
);
seed_skill(&fw, "probe-skill", "---\nname: probe-skill\n---\n\nV2\n");
let framework = crate::core::harness_root::framework_dir(&workspace);
std::fs::create_dir_all(&framework).unwrap();
std::fs::write(
framework.join("manifest.toml"),
"[skills]\nexclude = [\"something-else\"]\n",
)
.unwrap();
ensure_managed_config_dir_with_root(&fw, &config_dir, &workspace).unwrap();
assert_eq!(
std::fs::read_to_string(&project_copy).unwrap(),
"---\nname: probe-skill\n---\n\nV2\n",
"an updated skill selection must refresh the project tier on resume"
);
}
#[test]
fn ensure_managed_config_dir_project_tier_is_a_noop_when_unchanged() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
pin_skill_source_stamp(&fw);
seed_skill(&fw, "probe-skill", "---\nname: probe-skill\n---\n\nV1\n");
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
let workspace = project_dir(tmp.path());
let project_copy = workspace
.join(".claude")
.join("skills")
.join("probe-skill")
.join("SKILL.md");
ensure_managed_config_dir_with_root(&fw, &config_dir, &workspace).unwrap();
let before = std::fs::metadata(&project_copy)
.unwrap()
.modified()
.unwrap();
ensure_managed_config_dir_with_root(&fw, &config_dir, &workspace).unwrap();
let after = std::fs::metadata(&project_copy)
.unwrap()
.modified()
.unwrap();
assert_eq!(
before, after,
"an unchanged version and selection must take the stamp no-op path"
);
}