use std::path::{Path, PathBuf};
use std::process::Command;
use super::*;
fn git(dir: &Path, args: &[&str]) {
let out = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.unwrap_or_else(|e| panic!("git {args:?} in {} failed to spawn: {e}", dir.display()));
assert!(
out.status.success(),
"git {args:?} in {} failed: {}",
dir.display(),
String::from_utf8_lossy(&out.stderr)
);
}
fn init_repo(dir: &Path) {
std::fs::create_dir_all(dir).unwrap();
git(dir, &["init", "-q", "-b", "main"]);
git(dir, &["config", "user.email", "t@example.com"]);
git(dir, &["config", "user.name", "T"]);
std::fs::write(dir.join("README.md"), "# t\n").unwrap();
git(dir, &["add", "-A"]);
git(dir, &["commit", "-qm", "init"]);
}
fn canon(p: &Path) -> PathBuf {
std::fs::canonicalize(p).unwrap()
}
#[test]
fn harness_root_is_the_main_checkout_for_the_main_checkout() {
let tmp = crate::test_support::hermetic_temp_dir();
let repo = tmp.path().join("proj");
init_repo(&repo);
let resolved = harness_root_for(&repo).expect("a checkout resolves");
assert_eq!(canon(&resolved), canon(&repo));
}
#[test]
fn harness_root_is_the_main_checkout_for_a_worktree() {
let tmp = crate::test_support::hermetic_temp_dir();
let repo = tmp.path().join("proj");
init_repo(&repo);
let wt = tmp.path().join("wt");
git(
&repo,
&["worktree", "add", "-q", "-b", "feat", wt.to_str().unwrap()],
);
let resolved = harness_root_for(&wt).expect("a worktree resolves");
assert_eq!(
canon(&resolved),
canon(&repo),
"a worktree must resolve to its main checkout, not to itself"
);
assert_ne!(canon(&resolved), canon(&wt));
assert!(
!harness_dir(&wt).starts_with(canon(&wt)),
"the harness dir must not land inside the worktree: {}",
harness_dir(&wt).display()
);
}
#[test]
fn linked_worktree_owner_is_the_main_checkout_only_for_a_linked_worktree() {
let tmp = crate::test_support::hermetic_temp_dir();
let upstream = tmp.path().join("upstream");
init_repo(&upstream);
let repo = tmp.path().join("proj");
init_repo(&repo);
let wt = repo.join(".claude").join("worktrees").join("wt");
std::fs::create_dir_all(wt.parent().unwrap()).unwrap();
git(
&repo,
&["worktree", "add", "-q", "-b", "feat", wt.to_str().unwrap()],
);
git(
&repo,
&[
"-c",
"protocol.file.allow=always",
"submodule",
"add",
"-q",
upstream.to_str().unwrap(),
"sm",
],
);
let owner = linked_worktree_owner(&wt).expect("a linked worktree has an owner");
assert_eq!(canon(&owner), canon(&repo));
assert_eq!(linked_worktree_owner(&repo), None, "a main checkout");
assert_eq!(linked_worktree_owner(&repo.join("sm")), None, "a submodule");
}
#[test]
fn harness_root_is_the_repo_root_from_a_subdirectory() {
let tmp = crate::test_support::hermetic_temp_dir();
let repo = tmp.path().join("proj");
init_repo(&repo);
let sub = repo.join("crates").join("thing");
std::fs::create_dir_all(&sub).unwrap();
let resolved = harness_root_for(&sub).expect("a subdirectory resolves");
assert_eq!(canon(&resolved), canon(&repo));
}
#[test]
fn harness_root_maps_a_base_clone_back_to_the_project() {
let tmp = crate::test_support::hermetic_temp_dir();
let repo = tmp.path().join("proj");
init_repo(&repo);
let base = repo.join(".base");
let out = Command::new("git")
.args(["clone", "--bare", "-q"])
.arg(&repo)
.arg(&base)
.output()
.expect("git clone --bare spawns");
assert!(
out.status.success(),
"bare clone failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let wt = base.join(".worktrees").join("s1");
git(
&base,
&["worktree", "add", "-q", "-b", "feat", wt.to_str().unwrap()],
);
let resolved = harness_root_for(&wt).expect("a base-clone worktree resolves");
assert_eq!(
canon(&resolved),
canon(&repo),
"a `.base`-registered worktree must resolve to the project, not the bare clone"
);
}
#[test]
fn harness_root_for_a_non_bare_repo_named_base_is_itself() {
let tmp = crate::test_support::hermetic_temp_dir();
let project = tmp.path().join("proj");
let repo = project.join(".base");
init_repo(&repo);
let resolved = harness_root_for(&repo).expect("a checkout named .base resolves");
assert_eq!(
canon(&resolved),
canon(&repo),
"a non-bare repo named `.base` must resolve to itself, not to its parent"
);
assert_ne!(canon(&resolved), canon(&project));
}
#[test]
fn harness_root_for_a_submodule_is_the_submodule_checkout() {
let tmp = crate::test_support::hermetic_temp_dir();
let upstream = tmp.path().join("upstream");
init_repo(&upstream);
let sup = tmp.path().join("super");
init_repo(&sup);
git(
&sup,
&[
"-c",
"protocol.file.allow=always",
"submodule",
"add",
"-q",
upstream.to_str().unwrap(),
"sm",
],
);
let sm = sup.join("sm");
let resolved = harness_root_for(&sm).expect("a submodule resolves");
assert_eq!(
canon(&resolved),
canon(&sm),
"a submodule must resolve to its own checkout"
);
assert!(
!harness_dir(&sm)
.components()
.any(|c| c.as_os_str() == ".git"),
"harness state must never land inside a git directory: {}",
harness_dir(&sm).display()
);
}
#[test]
fn harness_root_for_a_separate_git_dir_checkout_is_the_working_tree() {
let tmp = crate::test_support::hermetic_temp_dir();
let work = tmp.path().join("work");
let store = tmp.path().join("store.git");
std::fs::create_dir_all(&work).unwrap();
let out = Command::new("git")
.arg("init")
.arg("-q")
.arg(format!("--separate-git-dir={}", store.display()))
.arg(&work)
.output()
.expect("git init --separate-git-dir spawns");
assert!(
out.status.success(),
"separate-git-dir init failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let resolved = harness_root_for(&work).expect("a separate-git-dir checkout resolves");
assert_eq!(
canon(&resolved),
canon(&work),
"a --separate-git-dir checkout must resolve to its working tree, not its store"
);
}
#[test]
fn harness_root_for_is_none_outside_a_git_repo() {
let tmp = crate::test_support::hermetic_temp_dir();
let plain = tmp.path().join("not-a-repo");
std::fs::create_dir_all(&plain).unwrap();
assert!(
harness_root_for(&plain).is_none(),
"a non-git directory must not resolve to a harness root"
);
}
#[test]
fn harness_root_falls_back_to_the_given_dir_outside_git() {
let tmp = crate::test_support::hermetic_temp_dir();
let plain = tmp.path().join("not-a-repo");
std::fs::create_dir_all(&plain).unwrap();
assert_eq!(harness_root(&plain), plain);
}
#[test]
fn harness_dir_is_under_the_harness_root() {
let tmp = crate::test_support::hermetic_temp_dir();
let plain = tmp.path().join("p");
std::fs::create_dir_all(&plain).unwrap();
assert_eq!(harness_dir(&plain), plain.join(HARNESS_DIR));
}
#[test]
fn framework_dir_is_under_the_harness_dir() {
let tmp = crate::test_support::hermetic_temp_dir();
let plain = tmp.path().join("p");
std::fs::create_dir_all(&plain).unwrap();
assert_eq!(
framework_dir(&plain),
plain.join(HARNESS_DIR).join(FRAMEWORK_DIR)
);
}
#[test]
fn session_dir_is_per_session_under_the_harness_root() {
let tmp = crate::test_support::hermetic_temp_dir();
let plain = tmp.path().join("p");
std::fs::create_dir_all(&plain).unwrap();
let a = session_dir(&plain, "sess-a");
let b = session_dir(&plain, "sess-b");
assert_eq!(
a,
plain.join(HARNESS_DIR).join(SESSIONS_DIR).join("sess-a"),
"the layout is `.trusty-mpm/sessions/<id>/`"
);
assert_ne!(a, b, "two sessions must not share a directory");
}
#[test]
fn session_scope_prefers_the_explicit_id() {
assert_eq!(
session_scope_from(Some("abc-123"), Some("ambient-id")),
"abc-123",
"an explicit managed id outranks the ambient one"
);
}
#[test]
fn session_scope_reads_the_managed_env_var() {
assert_eq!(session_scope_from(None, Some("ambient-id")), "ambient-id");
}
#[test]
fn session_scope_falls_back_to_the_unmanaged_bucket() {
assert_eq!(session_scope_from(None, None), UNMANAGED_SESSION_SCOPE);
assert_eq!(
session_scope_from(Some(" "), None),
UNMANAGED_SESSION_SCOPE
);
}
#[test]
fn session_scope_rejects_a_path_traversal_segment() {
for hostile in ["..", ".", "../../etc", "a/b", "a\\b", ""] {
assert_eq!(
session_scope_from(Some(hostile), None),
UNMANAGED_SESSION_SCOPE,
"{hostile:?} must not become a path component as an explicit id"
);
assert_eq!(
session_scope_from(None, Some(hostile)),
UNMANAGED_SESSION_SCOPE,
"{hostile:?} must not become a path component as an ambient id"
);
}
}
#[test]
fn protected_state_in_names_each_protected_entry() {
for name in [".git", ".base", ".worktrees"] {
let tmp = crate::test_support::hermetic_temp_dir();
let dir = tmp.path().join("owner").join("repo");
std::fs::create_dir_all(dir.join(name)).expect("create protected entry");
assert_eq!(
protected_state_in(&dir).as_deref(),
Some(name),
"{name} must be recognised as protected state"
);
}
}
#[test]
fn protected_state_in_is_none_for_a_foreign_directory() {
let tmp = crate::test_support::hermetic_temp_dir();
let dir = tmp.path().join("owner").join("repo");
std::fs::create_dir_all(&dir).expect("create dir");
std::fs::write(dir.join("HEAD"), "ref: refs/heads/main\n").expect("write HEAD");
assert_eq!(protected_state_in(&dir), None);
}
#[cfg(unix)]
#[test]
fn protected_state_in_treats_an_unreadable_entry_as_present() {
use std::os::unix::fs::PermissionsExt as _;
let tmp = crate::test_support::hermetic_temp_dir();
let dir = tmp.path().join("owner").join("repo");
std::fs::create_dir_all(dir.join(".git")).expect("create .git");
let restore = std::fs::metadata(&dir).expect("stat dir").permissions();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o000)).expect("chmod 000");
let unreadable = std::fs::symlink_metadata(dir.join(".git"))
.err()
.is_some_and(|e| e.kind() != std::io::ErrorKind::NotFound);
let observed = protected_state_in(&dir);
std::fs::set_permissions(&dir, restore).expect("restore permissions");
if !unreadable {
eprintln!(
"protected_state_in_treats_an_unreadable_entry_as_present: stat still \
succeeds (running as root?), skipping"
);
return;
}
assert_eq!(
observed.as_deref(),
Some(".git"),
"an entry that cannot be stat'd must count as present — Path::exists would \
report false here and the caller would rename live work"
);
}