use std::path::Path;
use crate::core::agent_manifest::{AgentManifest, ManifestEntry, Origin, checksum};
use crate::core::skill_manifest::{SkillManifest, SkillManifestEntry};
use crate::session_manager::decommission_force::{ProvisioningDirt, remove_in_project_worktree};
use crate::session_manager::record::ManagedSessionId;
use crate::session_manager::worktree_git_fixture::GitWorktreeFixture;
use crate::session_manager::worktree_ignored_output::kept_ignored_output;
const SKILL_BODY: &str = "---\nname: deploy-check\n---\n# Deploy check\n";
const REFERENCE_BODY: &str = "# Reference\n";
const AGENT_BODY: &str = "# Engineer\n";
fn ignore_deploy_dirs(fx: &GitWorktreeFixture) {
std::fs::write(
fx.repo.join(".git/info/exclude"),
"/.claude/agents/\n/.claude/skills/*\n/.trusty-mpm-worktree\n",
)
.expect("write info/exclude");
}
fn put(wt: &Path, rel: &str, body: &str) {
let path = wt.join(rel);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("mkdir");
}
std::fs::write(path, body).expect("write");
}
fn tree_with_skill(fx: &GitWorktreeFixture, name: &str) -> std::path::PathBuf {
ignore_deploy_dirs(fx);
let wt = fx.add_worktree(name);
GitWorktreeFixture::stamp_reclaimable_sentinel(&wt);
put(&wt, ".claude/skills/deploy-check/SKILL.md", SKILL_BODY);
put(
&wt,
".claude/skills/deploy-check/references/steps.md",
REFERENCE_BODY,
);
wt
}
fn record_skills(wt: &Path, entries: &[(&str, &str)]) {
let mut ledger = SkillManifest::default();
for (key, body) in entries {
ledger.managed.insert(
(*key).to_string(),
SkillManifestEntry {
checksum: checksum(body),
deployed_at: "2026-09-26T00:00:00Z".to_string(),
},
);
}
ledger
.save(&wt.join(".claude/skills"))
.expect("save skills ledger");
}
fn record_deploy_check(wt: &Path) {
record_skills(
wt,
&[
("deploy-check", SKILL_BODY),
("deploy-check/references/steps.md", REFERENCE_BODY),
],
);
}
#[tokio::test]
async fn a_user_skill_keeps_the_tree() {
let fx = GitWorktreeFixture::new();
let wt = tree_with_skill(&fx, "user-skill-8534");
let kept = kept_ignored_output(&wt)
.expect("the check completes")
.expect("a user skill is kept output");
assert_eq!(
(kept.files, kept.first.as_str()),
(2, ".claude/skills/deploy-check/")
);
let verdict = remove_in_project_worktree(
&ManagedSessionId::new(),
None,
&wt,
ProvisioningDirt::Refuse,
)
.await;
assert!(!verdict.removed, "{verdict:?}");
assert!(wt.join(".claude/skills/deploy-check/SKILL.md").exists());
}
#[test]
fn a_user_skill_with_a_disposable_name_keeps_the_tree() {
let fx = GitWorktreeFixture::new();
ignore_deploy_dirs(&fx);
let wt = fx.add_worktree("skill-named-build-8534");
put(&wt, ".claude/skills/build/SKILL.md", SKILL_BODY);
let kept = kept_ignored_output(&wt)
.expect("the check completes")
.expect("an unrecorded skill is kept output");
assert_eq!(
(kept.files, kept.first.as_str()),
(1, ".claude/skills/build/")
);
}
#[test]
fn a_user_agent_with_a_disposable_name_keeps_the_tree() {
let fx = GitWorktreeFixture::new();
std::fs::write(fx.repo.join(".git/info/exclude"), "dist/\n").expect("write info/exclude");
let wt = fx.add_worktree("agent-named-dist-8534");
put(&wt, ".claude/agents/dist/x.md", AGENT_BODY);
let kept = kept_ignored_output(&wt)
.expect("the check completes")
.expect("an unrecorded agent is kept output");
assert_eq!(
(kept.files, kept.first.as_str()),
(1, ".claude/agents/dist/")
);
}
#[tokio::test]
async fn a_manifest_named_skill_does_not_block_removal() {
let fx = GitWorktreeFixture::new();
let wt = tree_with_skill(&fx, "tm-skill-8534");
record_deploy_check(&wt);
put(
&wt,
".claude/skills/.trusty-mpm-skills-manifest.json.lock",
"",
);
put(&wt, ".claude/skills/.trusty-mpm-project-tier-stamp", "abc");
assert_eq!(kept_ignored_output(&wt), Ok(None));
let verdict = remove_in_project_worktree(
&ManagedSessionId::new(),
None,
&wt,
ProvisioningDirt::Refuse,
)
.await;
assert!(verdict.removed, "{:?}", verdict.kept_reason);
}
#[test]
fn a_hand_edited_deployed_skill_keeps_the_tree() {
let fx = GitWorktreeFixture::new();
let wt = tree_with_skill(&fx, "edited-skill-8534");
record_deploy_check(&wt);
put(&wt, ".claude/skills/deploy-check/notes.md", "mine");
let kept = kept_ignored_output(&wt).expect("the check completes");
assert_eq!(kept.map(|k| k.files), Some(1), "only the added file");
std::fs::remove_file(wt.join(".claude/skills/deploy-check/notes.md")).expect("rm notes");
put(
&wt,
".claude/skills/deploy-check/SKILL.md",
"edited by hand\n",
);
let kept = kept_ignored_output(&wt).expect("the check completes");
assert_eq!(kept.map(|k| k.files), Some(1), "only the edited file");
}
#[test]
fn an_unreadable_skill_manifest_keeps_the_tree() {
let fx = GitWorktreeFixture::new();
let wt = tree_with_skill(&fx, "torn-ledger-8534");
put(
&wt,
".claude/skills/.trusty-mpm-skills-manifest.json",
"{ torn",
);
let kept = kept_ignored_output(&wt)
.expect("the check completes")
.expect("an unreadable ledger keeps every skill file");
assert_eq!(kept.files, 2);
}
fn record_agent(ledger: &mut AgentManifest, name: &str, origin: Origin) {
ledger.managed.insert(
name.to_string(),
ManifestEntry {
source_chain: vec!["engineer".to_string()],
checksum: checksum(AGENT_BODY),
deployed_at: "2026-09-26T00:00:00Z".to_string(),
origin,
},
);
}
#[test]
fn only_framework_agents_in_the_ledger_are_excused() {
let fx = GitWorktreeFixture::new();
ignore_deploy_dirs(&fx);
let wt = fx.add_worktree("agents-8534");
let mut ledger = AgentManifest::default();
record_agent(&mut ledger, "engineer.md", Origin::Bundled);
ledger
.save(&wt.join(".claude/agents"))
.expect("save agents ledger");
put(&wt, ".claude/agents/engineer.md", AGENT_BODY);
assert_eq!(kept_ignored_output(&wt), Ok(None));
put(&wt, ".claude/agents/mine.md", AGENT_BODY);
let kept = kept_ignored_output(&wt).expect("the check completes");
assert_eq!(kept.map(|k| k.files), Some(1), "an unrecorded agent");
record_agent(&mut ledger, "mine.md", Origin::User);
ledger
.save(&wt.join(".claude/agents"))
.expect("save agents ledger");
let kept = kept_ignored_output(&wt).expect("the check completes");
assert_eq!(kept.map(|k| k.files), Some(1), "a user-origin agent");
}