use super::*;
use tempfile::TempDir;
fn workspace(tmp: &TempDir) -> PathBuf {
let dir = std::fs::canonicalize(tmp.path()).unwrap().join("workspace");
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn fill(dir: &Path, count: usize) {
for i in 0..count {
std::fs::create_dir_all(dir.join(format!("d{i:03}"))).unwrap();
}
}
#[cfg(unix)]
struct RestoreMode(PathBuf);
#[cfg(unix)]
impl Drop for RestoreMode {
fn drop(&mut self) {
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&self.0, std::fs::Permissions::from_mode(0o755));
}
}
#[test]
fn a_wide_node_modules_does_not_hide_a_child_repository() {
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
fill(&ws.join("node_modules"), WORKSPACE_SCAN_BUDGET + 44);
let app = ws.join("packages").join("app");
std::fs::create_dir_all(app.join(".git")).unwrap();
assert_eq!(scan_for_child_repo(&ws), ChildRepoScan::Found(app));
}
#[test]
fn a_directory_wider_than_the_budget_is_incomplete() {
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
fill(&ws, WORKSPACE_SCAN_BUDGET + 44);
assert_eq!(
scan_for_child_repo(&ws),
ChildRepoScan::Incomplete(ScanIncomplete::BudgetExhausted)
);
}
#[test]
fn a_directory_within_the_budget_with_no_repository_is_clear() {
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
fill(&ws, WORKSPACE_SCAN_BUDGET);
assert_eq!(scan_for_child_repo(&ws), ChildRepoScan::Clear);
}
#[test]
fn a_root_that_does_not_exist_yet_is_clear() {
let tmp = TempDir::new().unwrap();
let missing = workspace(&tmp).join("not-created-yet");
assert_eq!(scan_for_child_repo(&missing), ChildRepoScan::Clear);
}
#[test]
fn a_deep_single_chain_beyond_the_budget_is_incomplete() {
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
let mut deep = ws.clone();
for _ in 0..(WORKSPACE_SCAN_BUDGET + 50) {
deep = deep.join("d");
}
std::fs::create_dir_all(deep.join(".git")).unwrap();
assert_eq!(
scan_for_child_repo(&ws),
ChildRepoScan::Incomplete(ScanIncomplete::BudgetExhausted)
);
}
#[test]
fn a_scoped_package_repository_three_levels_down_is_found() {
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
let deep = ws.join("packages").join("@scope").join("pkg-a");
std::fs::create_dir_all(deep.join(".git")).unwrap();
assert_eq!(scan_for_child_repo(&ws), ChildRepoScan::Found(deep));
}
#[cfg(unix)]
#[test]
fn an_unreadable_child_directory_is_incomplete() {
use std::os::unix::fs::PermissionsExt;
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
let locked = ws.join("locked");
std::fs::create_dir_all(&locked).unwrap();
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
let _restore = RestoreMode(locked.clone());
if std::fs::read_dir(&locked).is_ok() {
eprintln!("#7673 tests: running with permission overrides (root?), skipping");
return;
}
match scan_for_child_repo(&ws) {
ChildRepoScan::Incomplete(ScanIncomplete::Unreadable { path, .. }) => {
assert_eq!(path, locked);
}
other => panic!("an unreadable child must stop the scan, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn a_symlink_cycle_is_not_descended() {
use std::os::unix::fs::symlink;
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
symlink(&ws, ws.join("loop_a")).unwrap();
symlink(&ws, ws.join("loop_b")).unwrap();
let mut repo = ws.join("chain");
for i in 1..10 {
repo = repo.join(format!("c{i}"));
}
std::fs::create_dir_all(repo.join(".git")).unwrap();
assert_eq!(scan_for_child_repo(&ws), ChildRepoScan::Found(repo));
let bare = ws.join("chain").join("bare");
std::fs::create_dir_all(&bare).unwrap();
symlink(&bare, bare.join("self")).unwrap();
assert_eq!(scan_for_child_repo(&bare), ChildRepoScan::Clear);
}
#[cfg(unix)]
#[test]
fn a_symlink_to_an_outside_directory_is_not_traversed() {
use std::os::unix::fs::symlink;
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
let outside = ws.parent().unwrap().join("outside");
std::fs::create_dir_all(outside.join("proj").join(".git")).unwrap();
symlink(&outside, ws.join("elsewhere")).unwrap();
assert_eq!(scan_for_child_repo(&ws), ChildRepoScan::Clear);
}
#[cfg(unix)]
#[test]
fn a_symlink_to_a_repository_counts_as_found() {
use std::os::unix::fs::symlink;
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
let repo = ws.parent().unwrap().join("repo");
std::fs::create_dir_all(repo.join(".git")).unwrap();
let linked = ws.join("linked");
symlink(&repo, &linked).unwrap();
assert_eq!(scan_for_child_repo(&ws), ChildRepoScan::Found(linked));
}
#[test]
fn a_repository_under_a_skip_listed_name_is_found() {
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
let lib = ws.join("vendor").join("some-lib");
std::fs::create_dir_all(lib.join(".git")).unwrap();
assert_eq!(scan_for_child_repo(&ws), ChildRepoScan::Found(lib));
}
#[test]
fn a_repository_that_is_a_skip_listed_directory_is_found() {
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
let build = ws.join("build");
std::fs::create_dir_all(build.join(".git")).unwrap();
assert_eq!(scan_for_child_repo(&ws), ChildRepoScan::Found(build));
}
#[test]
fn a_skip_listed_directory_wider_than_its_check_cap_is_incomplete() {
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
let node_modules = ws.join("node_modules");
fill(&node_modules, SKIP_DIR_CHECK_CAP + 1);
assert_eq!(
scan_for_child_repo(&ws),
ChildRepoScan::Incomplete(ScanIncomplete::SkipDirTooWide { path: node_modules })
);
}
#[cfg(unix)]
#[test]
fn an_unreadable_symlink_target_is_incomplete() {
use std::os::unix::fs::{PermissionsExt, symlink};
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
let locked = ws.parent().unwrap().join("locked-repo");
std::fs::create_dir_all(locked.join(".git")).unwrap();
let linked = ws.join("linked");
symlink(&locked, &linked).unwrap();
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
let _restore = RestoreMode(locked.clone());
if std::fs::read_dir(&locked).is_ok() {
eprintln!("#7673 tests: running with permission overrides (root?), skipping");
return;
}
match scan_for_child_repo(&ws) {
ChildRepoScan::Incomplete(ScanIncomplete::Unreadable { path, .. }) => {
assert_eq!(path, linked);
}
other => panic!("an unreadable symlink target must stop the scan, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn a_symlink_to_a_file_or_to_nothing_is_not_a_repository() {
use std::os::unix::fs::symlink;
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
std::fs::write(ws.join("CLAUDE.md"), "x").unwrap();
symlink(ws.join("CLAUDE.md"), ws.join("AGENTS.md")).unwrap();
symlink(ws.join("missing"), ws.join("dangling")).unwrap();
std::fs::create_dir_all(ws.join("vendor")).unwrap();
symlink(ws.join("CLAUDE.md"), ws.join("vendor").join("linked-file")).unwrap();
assert_eq!(scan_for_child_repo(&ws), ChildRepoScan::Clear);
}
#[test]
fn clear_is_constructed_in_one_place() {
let source = include_str!("child_repo_scan.rs");
let code: Vec<&str> = source
.lines()
.take_while(|l| !l.trim_start().starts_with("#[cfg(test)]"))
.map(|l| l.split("//").next().unwrap_or("").trim())
.collect();
let needle = "ChildRepoScan::Clear";
let sites: Vec<usize> = (0..code.len())
.filter(|&i| code[i].contains(needle))
.collect();
assert_eq!(
sites.len(),
1,
"`{needle}` must be constructed exactly once"
);
let site = sites[0];
assert_eq!(
code[site], needle,
"`Clear` must be a bare tail expression, not a mapped arm"
);
let loop_start = code
.iter()
.position(|l| *l == "while let Some(current) = queue.pop_front() {")
.expect("the walk drains a queue");
assert!(loop_start < site, "`Clear` must follow the queue loop");
assert!(
code[loop_start..site].iter().all(|l| !l.contains("fn ")),
"`Clear` must sit in the function that drains the queue"
);
let word_uses = code
.iter()
.filter(|l| {
l.split(|c: char| !c.is_alphanumeric() && c != '_')
.any(|w| w == "Clear")
})
.count();
assert_eq!(
word_uses, 2,
"only the variant declaration and its one construction name `Clear`"
);
}
#[test]
fn an_excepted_entry_is_skipped_and_its_siblings_are_still_found() {
let tmp = TempDir::new().unwrap();
let ws = workspace(&tmp);
let local = ws.join("local");
std::fs::create_dir_all(local.join(".git")).unwrap();
std::fs::create_dir_all(local.join("nested/.git")).unwrap();
assert_eq!(
scan_for_child_repo(&ws),
ChildRepoScan::Found(local.clone())
);
assert_eq!(
scan_for_child_repo_except(&ws, &local),
ChildRepoScan::Clear
);
let deeper = ws.join("sub/local");
std::fs::create_dir_all(deeper.join(".git")).unwrap();
assert_eq!(
scan_for_child_repo_except(&ws, &local),
ChildRepoScan::Found(deeper)
);
}