use std::path::Path;
use super::{
ignored_entries, ignored_output_blocks_removal, ignored_output_refusal,
inspect_dirt_with_ignored_output, is_harness_path, is_regenerable, kept_ignored_output,
};
use crate::session_manager::DirtyWorktreePolicy;
use crate::session_manager::worktree_git_fixture::GitWorktreeFixture;
fn exclude(fx: &GitWorktreeFixture, patterns: &str) {
std::fs::write(fx.repo.join(".git/info/exclude"), patterns).expect("write info/exclude");
}
fn ignore_target_and_results(fx: &GitWorktreeFixture) {
exclude(fx, "target/\nresults/\n");
}
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");
}
#[test]
fn rule_excuses_build_output_and_keeps_run_output() {
for (entry, regenerable) in [
("target/", true),
("crates/x/target/", true),
("web/node_modules/", true),
("crates/a/ui/dist/", true),
("crates/a/ui/src-tauri/gen/", true),
("site/.nuxt/", true),
(".parcel-cache/", true),
("infra/.terraform/", true),
("pkg/__pycache__/", true),
("mod.pyc", true),
(".DS_Store", true),
("build/out.json", false),
("target/report.json", false),
("target-analysis/", false),
("target-worktree/", false),
("gen/", false),
("results/", false),
("run-output.json", false),
(".env.local", false),
("targets/", false),
] {
assert_eq!(is_regenerable(entry), regenerable, "{entry}");
}
}
#[test]
fn tool_caches_added_in_round_3_are_regenerable() {
for (entry, regenerable) in [
(".vercel/", false),
("site/.astro/", true),
(".output/", false),
("app/.angular/", true),
(".expo/", true),
("docs/.docusaurus/", true),
(".wrangler/", false),
("pkg/.dart_tool/", true),
(".direnv/", true),
(".hypothesis/", true),
("htmlcov/", true),
(".eggs/", true),
("py/mypkg.egg-info/", true),
("web/.eslintcache", true),
("web/tsconfig.tsbuildinfo", true),
(".coverage", true),
("htmlcov/index.html", false),
(".vercel/output/notes.md", false),
("egg-info-notes/", false),
(".coverage-report.json", false),
] {
assert_eq!(is_regenerable(entry), regenerable, "{entry}");
}
}
#[test]
fn harness_paths_are_excused_and_look_alikes_are_not() {
for (rel, harness) in [
(".trusty-mpm-worktree", true),
(".trusty-mpm/", true),
(".trusty-mpm/sessions/a.json", true),
(".claude/settings.json", true),
(".claude/settings.json.20260926T000000Z.bak", true),
(".claude/settings.json.bak", true),
(".claude/settings.json.lock", true),
(".claude/settings.local.json", true),
(".claude/agents/", false),
(".claude/agents/engineer.md", false),
(".claude/agents/.trusty-mpm-manifest.json", true),
(".claude/agents/.trusty-mpm-manifest.json.lock", true),
(".claude/skills/tm/SKILL.md", false),
(".claude/skills/.trusty-mpm-skills-manifest.json", true),
(".claude/skills/.trusty-mpm-project-tier-stamp", true),
(".claude/skills/.trusty-mpm-mine/SKILL.md", false),
(".claude/output-styles/trusty-mpm.md", true),
(".claude/output-styles/x.tm-floor.md", true),
(".claude/output-styles/mine.md", false),
(".claude/commands/deploy.md", false),
("crates/a/.claude/settings.json", false),
(".trusty-mpmx", false),
("results/out.json", false),
] {
assert_eq!(is_harness_path(rel), harness, "{rel}");
}
}
#[test]
fn settings_json_look_alikes_are_not_harness() {
for rel in [
".claude/settings.json-notes.md",
".claude/settings.jsonc",
".claude/settings.json.mine.bak",
] {
assert!(!is_harness_path(rel), "{rel}");
}
}
#[test]
fn settings_local_json_look_alikes_are_not_harness() {
for rel in [
".claude/settings.local.json-notes.md",
".claude/settings.local.jsonc",
".claude/settings.local.json.mine",
] {
assert!(!is_harness_path(rel), "{rel}");
}
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("local-look-alike");
exclude(&fx, ".claude/\n");
put(&wt, ".claude/settings.local.json", "{}");
put(&wt, ".claude/settings.local.json-notes.md", "keep me");
let kept = kept_ignored_output(&wt)
.expect("the check completes")
.expect("the notes file is kept output");
assert_eq!((kept.files, kept.first.as_str()), (1, ".claude/"));
}
#[test]
fn settings_staging_leftovers_are_harness() {
for (rel, harness) in [
(".claude/settings.json.tmp.4242.0", true),
(".claude/settings.json.bak.4242.17", true),
(".claude/settings.json.4242.3.tmp", true),
(".claude/settings.local.json.tmp.4242.0", true),
(".claude/settings.local.json.4242.3.tmp", true),
(".claude/settings.local.json.bak", true),
(".claude/settings.json.tmp", false),
(".claude/settings.json.tmp.4242", false),
(".claude/settings.json.tmp.notes.0", false),
(".claude/settings.json.1.2.3.tmp", false),
(".claude/settings.local.json.4242.x.tmp", false),
] {
assert_eq!(is_harness_path(rel), harness, "{rel}");
}
}
#[test]
fn kept_output_counts_files_in_an_ignored_results_dir() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("ignored-results");
ignore_target_and_results(&fx);
put(&wt, "results/run.json", "{}");
put(&wt, "results/batch/part.json", "{}");
let kept = kept_ignored_output(&wt)
.expect("the check completes")
.expect("results/ is kept output");
assert_eq!(kept.files, 2);
assert_eq!(kept.first, "results/");
}
#[test]
fn kept_output_is_none_for_build_output_only() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("ignored-target");
ignore_target_and_results(&fx);
put(&wt, "target/debug/bin", "elf");
std::fs::create_dir_all(wt.join("results")).expect("an empty ignored dir");
assert_eq!(kept_ignored_output(&wt), Ok(None));
}
#[test]
fn a_vercel_dir_holding_an_env_file_keeps_the_tree() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("vercel-env");
exclude(&fx, ".vercel\n");
put(&wt, ".vercel/project.json", "{}");
put(&wt, ".vercel/.env.production.local", "API_KEY=x\n");
let kept = kept_ignored_output(&wt)
.expect("the check completes")
.expect(".vercel/ is kept output");
assert_eq!((kept.files, kept.first.as_str()), (2, ".vercel/"));
}
#[test]
fn an_ignored_file_under_a_tracked_build_dir_is_kept() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("tracked-build");
put(&wt, "build/keep.txt", "tracked\n");
GitWorktreeFixture::commit_all_and_push(&wt, "track build/");
exclude(&fx, "build/*.json\n");
put(&wt, "build/out.json", "{}");
let kept = kept_ignored_output(&wt)
.expect("the check completes")
.expect("build/out.json is kept output");
assert_eq!(kept.first, "build/out.json");
}
#[test]
fn tagged_cache_dirs_are_excused_by_content_not_name() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("tagged-caches");
exclude(&fx, "target-*/\n");
put(
&wt,
"target-8510/CACHEDIR.TAG",
"Signature: 8a477f597d28d172789f06886806bc55\n# cargo\n",
);
put(&wt, "target-8510/debug/app", "elf");
put(&wt, "crates/py/.pytest_cache/.gitignore", "*\n");
put(
&wt,
"crates/py/.pytest_cache/CACHEDIR.TAG",
"Signature: 8a477f597d28d172789f06886806bc55\n# pytest\n",
);
put(&wt, "crates/py/.pytest_cache/v/cache/lastfailed", "{}");
assert_eq!(kept_ignored_output(&wt), Ok(None), "tagged caches only");
put(&wt, "target-analysis/x", "a user's notes");
let kept = kept_ignored_output(&wt)
.expect("the check completes")
.expect("target-analysis/ is kept output");
assert_eq!(kept.first, "target-analysis/");
}
#[test]
fn a_rustc_info_file_alone_does_not_excuse_a_directory() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("rustc-info-only");
exclude(&fx, "target-*/\n");
put(&wt, "target-copy/.rustc_info.json", "{}");
put(&wt, "target-copy/report.json", "{}");
let kept = kept_ignored_output(&wt)
.expect("the check completes")
.expect("an untagged directory is kept output");
assert_eq!((kept.files, kept.first.as_str()), (2, "target-copy/"));
}
#[test]
fn a_cachedir_tag_without_the_signature_is_not_a_cache() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("forged-tag");
exclude(&fx, "runs/\n");
put(&wt, "runs/CACHEDIR.TAG", "not a cache\n");
put(&wt, "runs/out.json", "{}");
let kept = kept_ignored_output(&wt).expect("the check completes");
assert_eq!(kept.map(|k| k.files), Some(2));
}
#[test]
fn a_wholly_ignored_claude_dir_counts_only_non_harness_files() {
use crate::core::agent_manifest::{AgentManifest, ManifestEntry, Origin, checksum};
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("ignored-claude");
exclude(&fx, ".claude/\n");
put(&wt, ".claude/settings.json", "{}");
put(&wt, ".claude/agents/engineer.md", "# agent");
let mut ledger = AgentManifest::default();
ledger.managed.insert(
"engineer.md".to_string(),
ManifestEntry {
source_chain: vec!["engineer".to_string()],
checksum: checksum("# agent"),
deployed_at: "2026-09-26T00:00:00Z".to_string(),
origin: Origin::Bundled,
},
);
ledger
.save(&wt.join(".claude/agents"))
.expect("save ledger");
assert_eq!(kept_ignored_output(&wt), Ok(None));
put(&wt, ".claude/notes.md", "keep me");
let kept = kept_ignored_output(&wt)
.expect("the check completes")
.expect(".claude/notes.md is kept output");
assert_eq!((kept.files, kept.first.as_str()), (1, ".claude/"));
}
#[test]
fn kept_output_errors_when_git_cannot_answer() {
let dir = tempfile::tempdir().expect("tempdir");
let err = kept_ignored_output(dir.path()).expect_err("git status fails outside a repo");
assert!(err.contains("git"), "{err}");
}
#[test]
fn non_ascii_names_are_classified_not_refused() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("non-ascii");
exclude(&fx, "node_modules/\nrésultats/\n*.json\n");
put(&wt, "café/node_modules/pkg/index.js", "x");
put(&wt, "we\"ird/node_modules/pkg/index.js", "x");
assert_eq!(kept_ignored_output(&wt), Ok(None));
put(&wt, "résultats/sortie.txt", "x");
put(&wt, "a\"b.json", "{}");
put(&wt, "tab\tname.json", "{}");
let kept = kept_ignored_output(&wt)
.expect("the check completes")
.expect("run output under a quoted name is kept");
assert_eq!(kept.files, 3, "{kept:?}");
assert!(
["résultats/", "a\"b.json", "tab\tname.json"].contains(&kept.first.as_str()),
"{kept:?}"
);
}
#[test]
fn ignored_entries_skips_a_rename_source() {
let status = "R new.rs\0!! old.rs\0!! results/\0 M lib.rs\0!! é/\0";
assert_eq!(ignored_entries(status), ["results/", "é/"]);
}
#[test]
fn kept_output_errors_on_an_unreadable_directory() {
use std::os::unix::fs::PermissionsExt;
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("unreadable-results");
ignore_target_and_results(&fx);
put(&wt, "results/sub/out.json", "{}");
let sub = wt.join("results/sub");
std::fs::set_permissions(&sub, std::fs::Permissions::from_mode(0o000)).expect("chmod");
let result = kept_ignored_output(&wt);
std::fs::set_permissions(&sub, std::fs::Permissions::from_mode(0o755)).expect("chmod back");
let err = result.expect_err("an unreadable directory cannot be counted");
assert!(err.contains("unreadable"), "{err}");
}
#[test]
fn refusal_keeps_the_tree_when_the_check_fails() {
let dir = tempfile::tempdir().expect("tempdir");
let reason = ignored_output_refusal(dir.path()).expect("a failed check must refuse");
assert!(reason.contains("check failed"), "{reason}");
assert!(
reason.contains(&dir.path().display().to_string()),
"{reason}"
);
assert!(ignored_output_blocks_removal(dir.path(), DirtyWorktreePolicy::Skip).is_some());
}
#[test]
fn force_discard_policy_lets_gitignored_output_go() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("discard-policy");
ignore_target_and_results(&fx);
put(&wt, "results/out.json", "{}");
assert!(ignored_output_blocks_removal(&wt, DirtyWorktreePolicy::Skip).is_some());
assert_eq!(
ignored_output_blocks_removal(&wt, DirtyWorktreePolicy::ForceDiscard),
None
);
}
#[test]
fn probe_counts_ignored_output_as_dirt() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("probe-ahead");
GitWorktreeFixture::commit_unpushed(&wt);
ignore_target_and_results(&fx);
put(&wt, "results/out.json", "{}");
let dirt = inspect_dirt_with_ignored_output(&wt).expect("kept output is dirt");
assert_eq!(dirt.dirty_files, 1, "{dirt:?}");
assert!(dirt.unpushed_commits > 0, "{dirt:?}");
assert!(dirt.reason.contains("results/"), "{dirt:?}");
std::fs::remove_dir_all(wt.join("results")).expect("clear results");
let plain = inspect_dirt_with_ignored_output(&wt).expect("still ahead");
assert_eq!(plain.dirty_files, 0, "only the unpushed commit: {plain:?}");
}
#[test]
fn probe_fails_toward_dirty_when_the_ignored_check_fails() {
use std::os::unix::fs::PermissionsExt;
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("probe-unreadable");
ignore_target_and_results(&fx);
put(&wt, "results/sub/out.json", "{}");
let sub = wt.join("results/sub");
std::fs::set_permissions(&sub, std::fs::Permissions::from_mode(0o000)).expect("chmod");
let dirt = inspect_dirt_with_ignored_output(&wt);
std::fs::set_permissions(&sub, std::fs::Permissions::from_mode(0o755)).expect("chmod back");
let dirt = dirt.expect("a failed check is dirt");
assert_eq!(
(dirt.dirty_files, dirt.unpushed_commits),
(0, 0),
"{dirt:?}"
);
assert!(dirt.reason.contains("check failed"), "{dirt:?}");
}