use super::*;
use crate::core::mcp_provenance::{ledger_path, record_write};
struct Fixture {
_tmp: tempfile::TempDir,
root: PathBuf,
home: PathBuf,
workspace: PathBuf,
}
fn fixture() -> Fixture {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join(".trusty-mpm");
let home = tmp.path().join("home");
let workspace = home.join("projects").join("thing");
std::fs::create_dir_all(&root).unwrap();
std::fs::create_dir_all(&workspace).unwrap();
Fixture {
_tmp: tmp,
root,
home,
workspace,
}
}
fn write_mcp(dir: &Path, body: &str) -> PathBuf {
std::fs::create_dir_all(dir).unwrap();
let path = dir.join(MCP_JSON);
std::fs::write(&path, body).unwrap();
path
}
const SAMPLE: &str = r#"{"mcpServers":{"apex":{"type":"http","url":"https://x"},"trusty-search":{"type":"stdio","command":"trusty-search"}}}"#;
fn scoped(steps: Vec<RepairStep>, root: &Path) -> Vec<RepairStep> {
steps
.into_iter()
.filter(|s| s.path.starts_with(root))
.collect()
}
#[test]
fn scan_dirs_walks_ancestors_up_to_home() {
let home = PathBuf::from("/u/me");
let ws = home.join("projects").join("thing");
let dirs = scan_dirs(Some(&ws), &home);
assert!(dirs.contains(&home.join("projects")));
assert!(dirs.contains(&home));
assert!(
!dirs.contains(&PathBuf::from("/u")),
"the walk must stop at home — above it is other users' business: {dirs:?}"
);
}
#[test]
fn scan_dirs_excludes_the_workspace_itself() {
let home = PathBuf::from("/u/me");
let ws = home.join("thing");
assert!(
!scan_dirs(Some(&ws), &home).contains(&ws),
"the project's own managed .mcp.json is not a stray"
);
}
#[test]
fn scan_dirs_never_reaches_the_filesystem_root() {
let home = PathBuf::from("/nowhere");
let dirs = scan_dirs(Some(Path::new("/a/b")), &home);
assert!(
!dirs.contains(&PathBuf::from("/")),
"the filesystem root must never be scanned: {dirs:?}"
);
}
#[test]
fn scan_dirs_stops_at_the_depth_cap_outside_home() {
let deep = PathBuf::from("/a/b/c/d/e/f/g/h/i/j/k/l");
let dirs = scan_dirs(Some(&deep), Path::new("/nowhere"));
let ancestors = dirs.iter().filter(|d| d.starts_with("/a")).count();
assert!(
ancestors <= MAX_ANCESTOR_DEPTH,
"the walk must respect the depth cap, walked {ancestors}"
);
}
#[test]
fn scan_dirs_includes_the_temp_roots() {
let dirs = scan_dirs(None, Path::new("/u/me"));
assert!(dirs.contains(&std::env::temp_dir()));
assert!(dirs.contains(&PathBuf::from("/tmp")));
}
#[test]
fn scan_dirs_has_no_duplicates() {
let dirs = scan_dirs(Some(Path::new("/tmp/ws")), Path::new("/tmp"));
let mut sorted = dirs.clone();
sorted.sort();
sorted.dedup();
assert_eq!(sorted.len(), dirs.len(), "duplicate scan dirs: {dirs:?}");
}
#[test]
fn scan_finds_a_stray_above_the_workspace() {
let f = fixture();
let stray = write_mcp(&f.home.join("projects"), SAMPLE);
let found = scan(Some(&f.workspace), &f.home, &LedgerLoad::Missing);
assert!(found.iter().any(|s| s.path == stray));
}
#[test]
fn scan_ignores_the_workspaces_own_file() {
let f = fixture();
let own = write_mcp(&f.workspace, SAMPLE);
let found = scan(Some(&f.workspace), &f.home, &LedgerLoad::Missing);
assert!(!found.iter().any(|s| s.path == own));
}
#[test]
fn scan_lists_the_declared_servers() {
let f = fixture();
write_mcp(&f.home.join("projects"), SAMPLE);
let found = scan(Some(&f.workspace), &f.home, &LedgerLoad::Missing);
let stray = found
.iter()
.find(|s| s.path.starts_with(&f.home))
.expect("stray");
assert_eq!(stray.servers, vec!["apex", "trusty-search"]);
}
#[test]
fn declared_servers_tolerates_garbage() {
let f = fixture();
write_mcp(&f.home.join("projects"), "not json at all");
let found = scan(Some(&f.workspace), &f.home, &LedgerLoad::Missing);
let stray = found
.iter()
.find(|s| s.path.starts_with(&f.home))
.expect("stray");
assert!(
stray.servers.is_empty(),
"an unparseable file is a display gap, not a panic"
);
}
#[cfg(unix)]
#[test]
fn scan_reports_a_symlink_as_unknown() {
let f = fixture();
let real = write_mcp(&f.home.join("real"), SAMPLE);
let link_dir = f.home.join("projects");
std::fs::create_dir_all(&link_dir).unwrap();
std::os::unix::fs::symlink(&real, link_dir.join(MCP_JSON)).unwrap();
let found = scan(Some(&f.workspace), &f.home, &LedgerLoad::Missing);
let link = found
.iter()
.find(|s| s.path == link_dir.join(MCP_JSON))
.expect("the symlink must still be reported");
assert!(matches!(link.provenance, Provenance::Unknown(_)));
}
#[test]
fn sweep_quarantines_a_tm_written_stray() {
let f = fixture();
let stray = write_mcp(&f.home.join("projects"), SAMPLE);
record_write(&f.root, &stray, SAMPLE).unwrap();
let steps = scoped(
quarantine_strays(&f.root, Some(&f.workspace), &f.home, RepairMode::Apply),
f.home.parent().unwrap(),
);
assert_eq!(steps.len(), 1, "{steps:?}");
let StepStatus::Applied { backup } = &steps[0].status else {
panic!("a ledger-proven stray must be quarantined, got {steps:?}");
};
assert!(
!stray.exists(),
"the .mcp.json must no longer be discovered"
);
let dest = backup
.as_ref()
.expect("the rename destination is the backup");
assert!(dest.exists(), "and its bytes must survive at the new name");
assert_eq!(
std::fs::read_to_string(dest).unwrap(),
SAMPLE,
"quarantine must not alter a single byte — nothing here deletes"
);
}
#[test]
fn sweep_releases_the_ledger_claim_after_quarantine() {
let f = fixture();
let stray = write_mcp(&f.home.join("projects"), SAMPLE);
record_write(&f.root, &stray, SAMPLE).unwrap();
quarantine_strays(&f.root, Some(&f.workspace), &f.home, RepairMode::Apply);
let text = std::fs::read_to_string(ledger_path(&f.root)).unwrap();
assert!(
!text.contains(&stray.display().to_string()),
"a claim must not outlive the file it describes: {text}"
);
}
#[test]
fn sweep_refuses_an_unattributed_stray() {
let f = fixture();
let stray = write_mcp(&f.home.join("projects"), SAMPLE);
let steps = scoped(
quarantine_strays(&f.root, Some(&f.workspace), &f.home, RepairMode::Apply),
f.home.parent().unwrap(),
);
assert_eq!(steps.len(), 1, "{steps:?}");
let StepStatus::Refused(why) = &steps[0].status else {
panic!("an unattributed stray must be refused, got {steps:?}");
};
assert!(why.contains("no record"), "the refusal must say why: {why}");
assert!(stray.exists(), "and the file must be untouched");
assert_eq!(std::fs::read_to_string(&stray).unwrap(), SAMPLE);
}
#[test]
fn sweep_refuses_a_stray_edited_after_tm_wrote_it() {
let f = fixture();
let stray = write_mcp(&f.home.join("projects"), SAMPLE);
record_write(&f.root, &stray, SAMPLE).unwrap();
let edited = r#"{"mcpServers":{"my-own-server":{"type":"stdio","command":"mine"}}}"#;
std::fs::write(&stray, edited).unwrap();
let steps = scoped(
quarantine_strays(&f.root, Some(&f.workspace), &f.home, RepairMode::Apply),
f.home.parent().unwrap(),
);
let StepStatus::Refused(why) = &steps[0].status else {
panic!("an edited stray must be refused, got {steps:?}");
};
assert!(why.contains("changed afterwards"), "reason: {why}");
assert_eq!(
std::fs::read_to_string(&stray).unwrap(),
edited,
"the operator's edit must survive"
);
}
#[test]
fn sweep_refuses_when_the_ledger_is_unreadable() {
let f = fixture();
let stray = write_mcp(&f.home.join("projects"), SAMPLE);
std::fs::write(ledger_path(&f.root), "{ not json").unwrap();
let steps = scoped(
quarantine_strays(&f.root, Some(&f.workspace), &f.home, RepairMode::Apply),
f.home.parent().unwrap(),
);
let StepStatus::Refused(why) = &steps[0].status else {
panic!("an unreadable ledger must refuse, got {steps:?}");
};
assert!(why.contains("unreadable"), "reason: {why}");
assert!(stray.exists());
}
#[cfg(unix)]
#[test]
fn sweep_refuses_a_symlink() {
let f = fixture();
let real = write_mcp(&f.home.join("real"), SAMPLE);
let link_dir = f.home.join("projects");
std::fs::create_dir_all(&link_dir).unwrap();
std::os::unix::fs::symlink(&real, link_dir.join(MCP_JSON)).unwrap();
let steps = scoped(
quarantine_strays(&f.root, Some(&f.workspace), &f.home, RepairMode::Apply),
f.home.parent().unwrap(),
);
let link_step = steps
.iter()
.find(|s| s.path == link_dir.join(MCP_JSON))
.expect("the symlink must produce a step");
assert!(matches!(link_step.status, StepStatus::Refused(_)));
assert!(real.exists(), "the symlink target must be untouched");
}
#[test]
fn sweep_dry_run_writes_nothing() {
let f = fixture();
let stray = write_mcp(&f.home.join("projects"), SAMPLE);
record_write(&f.root, &stray, SAMPLE).unwrap();
let steps = scoped(
quarantine_strays(&f.root, Some(&f.workspace), &f.home, RepairMode::DryRun),
f.home.parent().unwrap(),
);
assert!(matches!(steps[0].status, StepStatus::Planned));
assert!(stray.exists(), "a dry run must not move the file");
assert!(
steps[0].what.contains("quarantined-"),
"the preview must name the destination: {}",
steps[0].what
);
}
#[test]
fn sweep_never_touches_the_workspaces_own_file() {
let f = fixture();
let own = write_mcp(&f.workspace, SAMPLE);
record_write(&f.root, &own, SAMPLE).unwrap();
quarantine_strays(&f.root, Some(&f.workspace), &f.home, RepairMode::Apply);
assert!(own.exists(), "the workspace's own .mcp.json must survive");
}
#[test]
fn explicit_quarantines_an_unattributed_file() {
let f = fixture();
let stray = write_mcp(&f.home.join("projects"), SAMPLE);
let step = quarantine_explicit(&f.root, Some(&f.workspace), &stray, RepairMode::Apply);
let StepStatus::Applied { backup } = &step.status else {
panic!("an explicitly named file must be quarantined, got {step:?}");
};
assert!(!stray.exists());
assert_eq!(
std::fs::read_to_string(backup.as_ref().unwrap()).unwrap(),
SAMPLE
);
}
#[test]
fn explicit_refuses_a_non_mcp_path() {
let f = fixture();
let other = f.home.join("important.txt");
std::fs::create_dir_all(&f.home).unwrap();
std::fs::write(&other, "keep me").unwrap();
let step = quarantine_explicit(&f.root, Some(&f.workspace), &other, RepairMode::Apply);
assert!(matches!(step.status, StepStatus::Refused(_)), "{step:?}");
assert!(other.exists(), "an unrelated file must never be renamed");
}
#[test]
fn explicit_refuses_the_workspaces_own_file() {
let f = fixture();
let own = write_mcp(&f.workspace, SAMPLE);
let step = quarantine_explicit(&f.root, Some(&f.workspace), &own, RepairMode::Apply);
let StepStatus::Refused(why) = &step.status else {
panic!("the workspace's own file must be refused, got {step:?}");
};
assert!(why.contains("workspace's own"), "reason: {why}");
assert!(own.exists());
}
#[cfg(unix)]
#[test]
fn explicit_refuses_a_symlink() {
let f = fixture();
let real = write_mcp(&f.home.join("real"), SAMPLE);
let link_dir = f.home.join("link");
std::fs::create_dir_all(&link_dir).unwrap();
let link = link_dir.join(MCP_JSON);
std::os::unix::fs::symlink(&real, &link).unwrap();
let step = quarantine_explicit(&f.root, Some(&f.workspace), &link, RepairMode::Apply);
assert!(matches!(step.status, StepStatus::Refused(_)), "{step:?}");
assert!(link.exists() && real.exists());
}
#[test]
fn explicit_refuses_a_missing_file() {
let f = fixture();
let step = quarantine_explicit(
&f.root,
Some(&f.workspace),
&f.home.join("nope").join(MCP_JSON),
RepairMode::Apply,
);
assert!(matches!(step.status, StepStatus::Refused(_)), "{step:?}");
}
#[test]
fn explicit_refuses_a_directory_named_mcp_json() {
let f = fixture();
let dir = f.home.join("odd").join(MCP_JSON);
std::fs::create_dir_all(&dir).unwrap();
let step = quarantine_explicit(&f.root, Some(&f.workspace), &dir, RepairMode::Apply);
assert!(matches!(step.status, StepStatus::Refused(_)), "{step:?}");
assert!(dir.is_dir());
}
#[test]
fn explicit_dry_run_writes_nothing() {
let f = fixture();
let stray = write_mcp(&f.home.join("projects"), SAMPLE);
let step = quarantine_explicit(&f.root, Some(&f.workspace), &stray, RepairMode::DryRun);
assert!(matches!(step.status, StepStatus::Planned));
assert!(stray.exists());
}
#[test]
fn quarantine_destination_does_not_collide() {
let f = fixture();
let dir = f.home.join("projects");
let first = write_mcp(&dir, "first");
quarantine_explicit(&f.root, Some(&f.workspace), &first, RepairMode::Apply);
let second = write_mcp(&dir, "second");
quarantine_explicit(&f.root, Some(&f.workspace), &second, RepairMode::Apply);
let mut bodies: Vec<String> = std::fs::read_dir(&dir)
.unwrap()
.flatten()
.filter(|e| e.file_name().to_string_lossy().contains("quarantined-"))
.map(|e| std::fs::read_to_string(e.path()).unwrap())
.collect();
bodies.sort();
assert_eq!(bodies, vec!["first", "second"], "both must survive");
}
#[cfg(unix)]
#[test]
fn explicit_refuses_the_workspaces_own_file_via_a_symlinked_workspace_spelling() {
let f = fixture();
let real_ws = f.home.join("real-ws");
let own = write_mcp(&real_ws, SAMPLE);
let linked_ws = f.home.join("linked-ws");
std::os::unix::fs::symlink(&real_ws, &linked_ws).unwrap();
let step = quarantine_explicit(&f.root, Some(&linked_ws), &own, RepairMode::Apply);
assert!(
matches!(step.status, StepStatus::Refused(_)),
"the workspace's own .mcp.json must be refused however the workspace is \
spelled, got {step:?}"
);
assert!(
own.exists(),
"the project's live .mcp.json must still be there"
);
}
#[cfg(unix)]
#[test]
fn scan_dirs_stops_at_home_through_a_symlinked_spelling() {
let tmp = tempfile::tempdir().unwrap();
let real_home = tmp.path().join("real-home");
let ws = real_home.join("a").join("b");
std::fs::create_dir_all(&ws).unwrap();
let linked_home = tmp.path().join("linked-home");
std::os::unix::fs::symlink(&real_home, &linked_home).unwrap();
let dirs = scan_dirs(Some(&ws), &linked_home);
assert!(
!dirs.iter().any(|d| d == tmp.path()),
"the walk must stop at home however home is spelled — it reached above it: {dirs:?}"
);
}
#[test]
fn sweep_refuses_a_file_edited_between_scan_and_rename() {
let f = fixture();
let stray = write_mcp(&f.home.join("projects"), SAMPLE);
record_write(&f.root, &stray, SAMPLE).unwrap();
assert_eq!(
crate::core::mcp_provenance::classify(&mcp_provenance::load(&f.root), &stray),
Provenance::TmWritten
);
let edited = r#"{"mcpServers":{"my-own-server":{"type":"stdio","command":"mine"}}}"#;
std::fs::write(&stray, edited).unwrap();
let step = apply_verified(&f.root, &stray, RepairMode::Apply);
let StepStatus::Refused(why) = &step.status else {
panic!("a file edited inside the window must be refused, got {step:?}");
};
assert!(why.contains("changed between"), "reason: {why}");
assert_eq!(
std::fs::read_to_string(&stray).unwrap(),
edited,
"the edit made inside the window must survive"
);
}
#[test]
fn sweep_still_quarantines_when_nothing_changed_in_the_window() {
let f = fixture();
let stray = write_mcp(&f.home.join("projects"), SAMPLE);
record_write(&f.root, &stray, SAMPLE).unwrap();
let step = apply_verified(&f.root, &stray, RepairMode::Apply);
assert!(
matches!(step.status, StepStatus::Applied { .. }),
"an unchanged tm-written file must still be quarantined, got {step:?}"
);
assert!(!stray.exists());
}