use super::*;
use crate::core::content_source::test_support::rc;
use tempfile::TempDir;
fn ensure_managed_config_dir_with_root(
fw: &FrameworkPaths,
config_dir: &Path,
project_dir: &Path,
) -> anyhow::Result<()> {
super::ensure_managed_config_dir_with_root_and_exe(
fw,
config_dir,
project_dir,
Some(Path::new(crate::test_support::STABLE_HOOK_EXE)),
)
}
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(".claude.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::content_source::test_support::repo_roster()
.require("BASE-AGENT.md")
.unwrap(),
"the bundled agent SOURCE must be re-materialized from content (#9011)"
);
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();
for (src, dst) in [
(
crate::core::content_source::test_support::repo_agents_dir(),
&fw.agents,
),
(
crate::core::content_source::test_support::repo_root().join("content/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(rc()),
)
.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();
}
fn seed_user_skill(fw: &FrameworkPaths, stem: &str, body: &str) {
let dir = fw.user_skill_source_dir();
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.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,
crate::core::content_source::test_support::rc(),
)
.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);
crate::test_support::enable_event_capture();
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");
seed_user_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");
seed_user_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");
seed_user_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"
);
}
const SPLIT_BRAIN_REMOTE: &str = "git@github.com:bobmatnyc/trusty-tools.git";
const OWNER_REPO_PALACE: &str = "bobmatnyc-trusty-tools";
const BARE_PALACE: &str = "trusty-tools";
struct EnvGuard {
key: &'static str,
prev: Option<String>,
}
impl EnvGuard {
fn set(key: &'static str, value: &Path) -> Self {
let prev = std::env::var(key).ok();
unsafe { std::env::set_var(key, value) };
Self { key, prev }
}
fn clear(key: &'static str) -> Self {
let prev = std::env::var(key).ok();
unsafe { std::env::remove_var(key) };
Self { key, prev }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
match self.prev.take() {
Some(v) => std::env::set_var(self.key, v),
None => std::env::remove_var(self.key),
}
}
}
}
fn git_init_with_origin(dir: &Path, remote: &str) {
let run = |args: &[&str]| {
let status = std::process::Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.status()
.expect("git must be on PATH to run this test");
assert!(status.success(), "git {args:?} failed");
};
run(&["init", "-q"]);
run(&["config", "remote.origin.url", remote]);
}
fn make_palace(registry_dir: &Path, id: &str) {
let dir = registry_dir.join(id);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("palace.json"), b"{}").unwrap();
}
#[test]
#[serial_test::serial]
fn ensure_managed_config_dir_heals_a_bare_repo_palace_alias() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
let workspace = project_dir(tmp.path());
git_init_with_origin(&workspace, SPLIT_BRAIN_REMOTE);
let data = TempDir::new().unwrap();
let _no_override = EnvGuard::clear("TRUSTY_MEMORY_PALACE");
let _data_dir = EnvGuard::set("TRUSTY_DATA_DIR_OVERRIDE", data.path());
let registry = data.path().join("trusty-memory");
make_palace(®istry, BARE_PALACE);
let pinned = crate::core::session_launch::resolve_palace_slug(&workspace, None)
.expect("the workspace remote must yield an owner-repo palace slug");
assert_eq!(
pinned, OWNER_REPO_PALACE,
"precondition: the pinned slug is the owner-repo name, not the bare one"
);
assert!(
!registry.join(&pinned).join("palace.json").exists(),
"precondition: the pinned palace must NOT exist — that is the split-brain"
);
ensure_managed_config_dir_with_root(&fw, &config_dir, &workspace).unwrap();
assert_eq!(
trusty_common::palace_alias::PalaceAliasStore::resolve_alias(®istry, &pinned)
.unwrap()
.as_deref(),
Some(BARE_PALACE),
"provisioning must register {pinned} -> {BARE_PALACE} so the pinned palace \
resolves to the claude-mpm-era store instead of failing with missing metadata"
);
}