use super::*;
#[test]
fn launch_and_daemon_agree_on_repos_root_env() {
let _g = crate::core::trusty_tools_config::env_test_lock();
unsafe {
std::env::set_var(REPOS_ROOT_ENV, "/explicit/repos/root");
std::env::remove_var(crate::core::trusty_tools_config::WORKSPACE_ROOT_ENV);
}
let unified = base_clone_path("owner", "repo");
let cfg = crate::core::trusty_tools_config::TrustyToolsConfig::load();
let gh = trusty_common::github_path::GithubPath {
owner: "owner".into(),
repo: "repo".into(),
};
let pre_fix_launch = crate::core::trusty_tools_config::workspace_subpath(&cfg, &gh);
unsafe { std::env::remove_var(REPOS_ROOT_ENV) };
assert_eq!(
unified,
PathBuf::from("/explicit/repos/root/owner/repo"),
"the unified resolver (used by BOTH entry points) must honour \
TRUSTY_MPM_REPOS_ROOT"
);
assert_ne!(
unified, pre_fix_launch,
"the pre-#1807 tm launch path (workspace_subpath) diverged from the \
daemon root; tm launch must now use base_clone_path instead"
);
}
#[test]
fn ensure_base_clone_migrates_old_layout_dir_aside() {
let src = crate::test_support::hermetic_temp_dir();
let src_path = src.path();
let init = std::process::Command::new("git")
.args(["init", src_path.to_str().expect("src utf8")])
.output()
.expect("git init src");
assert!(init.status.success(), "git init src failed");
for (k, v) in [("user.email", "t@example.com"), ("user.name", "T")] {
let ok = std::process::Command::new("git")
.args(["-C", src_path.to_str().expect("utf8"), "config", k, v])
.status()
.expect("git config");
assert!(ok.success(), "git config {k} failed");
}
std::fs::write(src_path.join("README"), b"src").expect("write README");
let add = std::process::Command::new("git")
.args(["-C", src_path.to_str().expect("utf8"), "add", "."])
.status()
.expect("git add");
assert!(add.success(), "git add failed");
let commit = std::process::Command::new("git")
.args([
"-C",
src_path.to_str().expect("utf8"),
"commit",
"-m",
"init",
])
.status()
.expect("git commit");
assert!(commit.success(), "git commit failed");
let parent = crate::test_support::hermetic_temp_dir();
let base = parent.path().join("owner").join("repo");
let old_session = base.join("00000000-old-session-uuid");
std::fs::create_dir_all(&old_session).expect("create old-layout subdir");
std::fs::write(old_session.join("MARKER"), b"legacy").expect("write marker");
assert!(
!base.join(".git").exists(),
"precondition: no top-level .git"
);
let url = src_path.to_str().expect("src url utf8");
ensure_base_clone(url, &base).expect("ensure_base_clone must migrate + clone");
assert!(
base.join(".git").exists(),
"fresh shared-base clone must exist at the base path after migration"
);
let repo_parent = base.parent().expect("base has parent");
let backup = std::fs::read_dir(repo_parent)
.expect("read owner dir")
.filter_map(Result::ok)
.map(|e| e.path())
.find(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| n.contains(OLD_LAYOUT_BACKUP_SUFFIX))
.unwrap_or(false)
})
.expect("a migrated backup dir must exist");
assert!(
backup
.join("00000000-old-session-uuid")
.join("MARKER")
.exists(),
"the old per-session data must be preserved under the backup dir"
);
}
#[test]
fn migrate_old_layout_aside_ignores_empty_and_git_dirs() {
let empty = crate::test_support::hermetic_temp_dir();
let empty_base = empty.path().join("owner").join("repo");
std::fs::create_dir_all(&empty_base).expect("create empty base");
assert!(
migrate_old_layout_aside(&empty_base)
.expect("empty dir must not error")
.is_none(),
"an empty base dir must not be migrated"
);
assert!(empty_base.is_dir(), "empty base dir must remain in place");
let git = crate::test_support::hermetic_temp_dir();
let git_base = git.path().join("owner").join("repo");
std::fs::create_dir_all(git_base.join(".git")).expect("create .git");
std::fs::write(git_base.join("file"), b"x").expect("write file");
assert!(
migrate_old_layout_aside(&git_base)
.expect("git dir must not error")
.is_none(),
"a dir with a top-level .git must not be migrated"
);
assert!(
git_base.join(".git").is_dir(),
".git dir must remain in place"
);
let absent = git.path().join("does").join("not").join("exist");
assert!(
migrate_old_layout_aside(&absent)
.expect("absent path must not error")
.is_none(),
"an absent path must not be migrated"
);
}
#[test]
fn try_inproject_spawn_returns_none_for_non_git_path() {
let tmp = std::env::temp_dir();
let result = try_inproject_spawn(&tmp);
assert!(
matches!(result, Ok(None)),
"non-git path should yield Ok(None), got {result:?}"
);
}
#[test]
fn get_origin_url_returns_none_for_non_git() {
let tmp = std::env::temp_dir();
assert!(get_origin_url(&tmp).is_none());
}
#[test]
fn repos_root_default_ends_with_canonical_segment() {
if std::env::var(REPOS_ROOT_ENV).is_ok()
|| std::env::var(crate::core::trusty_tools_config::WORKSPACE_ROOT_ENV).is_ok()
{
return;
}
let root = repos_root();
assert!(
root.ends_with(DEFAULT_REPOS_DIR),
"repos_root() default must end with '{DEFAULT_REPOS_DIR}', got {}",
root.display()
);
}
#[test]
fn base_clone_path_resolves_to_canonical_root() {
if std::env::var(REPOS_ROOT_ENV).is_ok()
|| std::env::var(crate::core::trusty_tools_config::WORKSPACE_ROOT_ENV).is_ok()
{
return;
}
let base = base_clone_path("myorg", "myrepo");
assert!(
base.ends_with("trusty-mpm-projects/myorg/myrepo"),
"base_clone_path must resolve to …/trusty-mpm-projects/myorg/myrepo, got {}",
base.display()
);
}
#[test]
fn session_worktree_path_uses_dot_prefix() {
let tmp = crate::test_support::hermetic_temp_dir();
let base = tmp.path();
let init = std::process::Command::new("git")
.args(["init", base.to_str().expect("base is utf8")])
.output()
.expect("git init");
assert!(
init.status.success(),
"git init failed: {}",
String::from_utf8_lossy(&init.stderr)
);
for (k, v) in [("user.email", "test@example.com"), ("user.name", "Test")] {
let ok = std::process::Command::new("git")
.args(["-C", base.to_str().expect("base is utf8"), "config", k, v])
.status()
.expect("git config");
assert!(ok.success(), "git config {k} failed");
}
std::fs::write(base.join("README"), b"init").expect("write README");
let add = std::process::Command::new("git")
.args(["-C", base.to_str().expect("base is utf8"), "add", "."])
.status()
.expect("git add");
assert!(add.success(), "git add failed");
let commit = std::process::Command::new("git")
.args([
"-C",
base.to_str().expect("base is utf8"),
"commit",
"-m",
"init",
])
.status()
.expect("git commit");
assert!(commit.success(), "git commit failed");
let name = "tm-test-repo-01";
let worktree_path =
create_session_worktree(base, name, &crate::session_manager::ManagedSessionId::new())
.expect("create_session_worktree must succeed on a real git repo");
let expected_parent = base.join(".worktrees");
assert!(
worktree_path.starts_with(&expected_parent),
"worktree must be under <base>/.worktrees/, got {}",
worktree_path.display()
);
assert!(
worktree_path.ends_with(name),
"worktree path must end with worktree name {name}, got {}",
worktree_path.display()
);
assert!(
worktree_path.is_dir(),
"worktree directory must exist on disk, got {}",
worktree_path.display()
);
}
#[test]
fn worktree_name_collides_detects_existing_dir_and_branch() {
let tmp = crate::test_support::hermetic_temp_dir();
let base = tmp.path();
let init = std::process::Command::new("git")
.args(["init", base.to_str().expect("base is utf8")])
.output()
.expect("git init");
assert!(init.status.success(), "git init failed");
for (k, v) in [("user.email", "test@example.com"), ("user.name", "Test")] {
let ok = std::process::Command::new("git")
.args(["-C", base.to_str().expect("utf8"), "config", k, v])
.status()
.expect("git config");
assert!(ok.success(), "git config {k} failed");
}
std::fs::write(base.join("README"), b"init").expect("write README");
let add = std::process::Command::new("git")
.args(["-C", base.to_str().expect("utf8"), "add", "."])
.status()
.expect("git add");
assert!(add.success(), "git add failed");
let commit = std::process::Command::new("git")
.args(["-C", base.to_str().expect("utf8"), "commit", "-m", "init"])
.status()
.expect("git commit");
assert!(commit.success(), "git commit failed");
assert!(
!worktree_name_collides(base, "tm-fresh-01"),
"an unused name must not collide"
);
create_session_worktree(
base,
"tm-fresh-01",
&crate::session_manager::ManagedSessionId::new(),
)
.expect("create_session_worktree");
assert!(
worktree_name_collides(base, "tm-fresh-01"),
"an in-use name (dir + branch both exist) must collide"
);
let remove = std::process::Command::new("git")
.arg("-C")
.arg(base)
.args(["worktree", "remove", "--force"])
.arg(base.join(".worktrees").join("tm-fresh-01"))
.output()
.expect("git worktree remove");
assert!(
remove.status.success(),
"git worktree remove failed: {}",
String::from_utf8_lossy(&remove.stderr)
);
assert!(
!base.join(".worktrees").join("tm-fresh-01").exists(),
"worktree dir must be gone after `git worktree remove`"
);
assert!(
worktree_name_collides(base, "tm-fresh-01"),
"a name whose branch still exists must still collide even after the \
worktree dir was removed"
);
}
#[test]
fn create_session_worktree_rejects_existing_worktree_dir() {
let tmp = crate::test_support::hermetic_temp_dir();
let base = tmp.path();
let init = std::process::Command::new("git")
.args(["init", base.to_str().expect("base is utf8")])
.output()
.expect("git init");
assert!(init.status.success(), "git init failed");
for (k, v) in [("user.email", "test@example.com"), ("user.name", "Test")] {
let ok = std::process::Command::new("git")
.args(["-C", base.to_str().expect("utf8"), "config", k, v])
.status()
.expect("git config");
assert!(ok.success(), "git config {k} failed");
}
std::fs::write(base.join("README"), b"init").expect("write README");
let add = std::process::Command::new("git")
.args(["-C", base.to_str().expect("utf8"), "add", "."])
.status()
.expect("git add");
assert!(add.success(), "git add failed");
let commit = std::process::Command::new("git")
.args(["-C", base.to_str().expect("utf8"), "commit", "-m", "init"])
.status()
.expect("git commit");
assert!(commit.success(), "git commit failed");
let first = create_session_worktree(
base,
"tm-dup-01",
&crate::session_manager::ManagedSessionId::new(),
)
.expect("first create must succeed");
assert!(first.is_dir(), "first worktree must exist");
let second = create_session_worktree(
base,
"tm-dup-01",
&crate::session_manager::ManagedSessionId::new(),
);
assert!(
second.is_err(),
"creating a worktree for an already-existing name must error, not clobber"
);
assert!(
first.is_dir(),
"the original worktree must remain untouched after the rejected re-create"
);
}
#[test]
fn ensure_worktrees_gitignored_idempotent() {
let tmp = crate::test_support::hermetic_temp_dir();
let base = tmp.path();
std::fs::create_dir_all(base.join(".git")).expect("create .git");
ensure_worktrees_gitignored(base).expect("first call must succeed");
let exclude = base.join(".git").join("info").join("exclude");
let content1 = std::fs::read_to_string(&exclude).expect("exclude readable");
let count1 = content1
.lines()
.filter(|l| l.trim() == ".worktrees/")
.count();
assert_eq!(
count1, 1,
"must contain exactly one .worktrees/ entry after first call"
);
ensure_worktrees_gitignored(base).expect("second call must succeed (idempotent)");
let content2 = std::fs::read_to_string(&exclude).expect("exclude readable");
let count2 = content2
.lines()
.filter(|l| l.trim() == ".worktrees/")
.count();
assert_eq!(
count2, 1,
"second call must not add a duplicate .worktrees/ entry"
);
}
#[tokio::test]
async fn ensure_base_clone_emits_cloning_repo_only_on_fresh_clone() {
use crate::core::provisioning_stage::{ProvisioningStage, StageEmitter, scoped};
let src = crate::test_support::hermetic_temp_dir();
let src_path = src.path();
let init = std::process::Command::new("git")
.args(["init", src_path.to_str().expect("src utf8")])
.output()
.expect("git init src");
assert!(init.status.success(), "git init src failed");
for (k, v) in [("user.email", "t@example.com"), ("user.name", "T")] {
let ok = std::process::Command::new("git")
.args(["-C", src_path.to_str().expect("utf8"), "config", k, v])
.status()
.expect("git config");
assert!(ok.success(), "git config {k} failed");
}
std::fs::write(src_path.join("README"), b"src").expect("write README");
let add = std::process::Command::new("git")
.args(["-C", src_path.to_str().expect("utf8"), "add", "."])
.status()
.expect("git add");
assert!(add.success(), "git add failed");
let commit = std::process::Command::new("git")
.args([
"-C",
src_path.to_str().expect("utf8"),
"commit",
"-m",
"init",
])
.status()
.expect("git commit");
assert!(commit.success(), "git commit failed");
let parent = crate::test_support::hermetic_temp_dir();
let base = parent.path().join("owner").join("repo");
let url = src_path.to_str().expect("src url utf8").to_string();
let (tx1, mut rx1) = tokio::sync::broadcast::channel(8);
let emitter1 = StageEmitter::new("s-1", "https://example.com/owner/repo", tx1);
let base_for_clone = base.clone();
let url_for_clone = url.clone();
scoped(emitter1, async move {
ensure_base_clone(&url_for_clone, &base_for_clone)
.expect("first ensure_base_clone must succeed (fresh clone)");
})
.await;
let mut stages1 = Vec::new();
while let Ok(value) = rx1.try_recv() {
stages1.push(value["stage"].as_str().unwrap().to_string());
}
assert_eq!(
stages1,
vec![ProvisioningStage::CloningRepo.wire_name()],
"a fresh base clone must emit exactly one CloningRepo stage event"
);
let (tx2, mut rx2) = tokio::sync::broadcast::channel(8);
let emitter2 = StageEmitter::new("s-2", "https://example.com/owner/repo", tx2);
scoped(emitter2, async move {
ensure_base_clone(&url, &base).expect("second ensure_base_clone must succeed (reuse)");
})
.await;
let mut stages2 = Vec::new();
while let Ok(value) = rx2.try_recv() {
stages2.push(value["stage"].as_str().unwrap().to_string());
}
assert!(
stages2.is_empty(),
"reusing an existing base clone must NOT re-emit CloningRepo, got: {stages2:?}"
);
}
#[test]
fn create_session_worktree_sets_pull_upstream_and_worktree_scoped_push() {
let origin_dir = crate::test_support::hermetic_temp_dir();
let origin_path = origin_dir.path();
let init = std::process::Command::new("git")
.args([
"init",
"--bare",
"-b",
"main",
origin_path.to_str().expect("origin utf8"),
])
.output()
.expect("git init --bare origin");
assert!(
init.status.success(),
"git init --bare origin failed: {}",
String::from_utf8_lossy(&init.stderr)
);
let seed = crate::test_support::hermetic_temp_dir();
let seed_path = seed.path();
let clone_seed = std::process::Command::new("git")
.args([
"clone",
origin_path.to_str().expect("origin utf8"),
seed_path.to_str().expect("seed utf8"),
])
.output()
.expect("git clone seed");
assert!(
clone_seed.status.success(),
"git clone seed failed: {}",
String::from_utf8_lossy(&clone_seed.stderr)
);
for (k, v) in [("user.email", "t@example.com"), ("user.name", "T")] {
let ok = std::process::Command::new("git")
.args(["-C", seed_path.to_str().expect("seed utf8"), "config", k, v])
.status()
.expect("git config");
assert!(ok.success(), "git config {k} failed");
}
std::fs::write(seed_path.join("README"), b"seed").expect("write README");
let add = std::process::Command::new("git")
.args(["-C", seed_path.to_str().expect("seed utf8"), "add", "."])
.status()
.expect("git add");
assert!(add.success(), "git add failed");
let commit = std::process::Command::new("git")
.args([
"-C",
seed_path.to_str().expect("seed utf8"),
"commit",
"-m",
"init",
])
.status()
.expect("git commit");
assert!(commit.success(), "git commit failed");
let push_seed = std::process::Command::new("git")
.args([
"-C",
seed_path.to_str().expect("seed utf8"),
"push",
"origin",
"main",
])
.status()
.expect("git push seed");
assert!(push_seed.success(), "git push seed failed");
let base_parent = crate::test_support::hermetic_temp_dir();
let base = base_parent.path().join("base");
let clone_base = std::process::Command::new("git")
.args([
"clone",
origin_path.to_str().expect("origin utf8"),
base.to_str().expect("base utf8"),
])
.output()
.expect("git clone base");
assert!(
clone_base.status.success(),
"git clone base failed: {}",
String::from_utf8_lossy(&clone_base.stderr)
);
for (k, v) in [("user.email", "t@example.com"), ("user.name", "T")] {
let ok = std::process::Command::new("git")
.args(["-C", base.to_str().expect("base utf8"), "config", k, v])
.status()
.expect("git config");
assert!(ok.success(), "git config {k} failed");
}
let worktree_path = create_session_worktree(
&base,
"tm-pull-01",
&crate::session_manager::ManagedSessionId::new(),
)
.expect("create_session_worktree must succeed");
let upstream = std::process::Command::new("git")
.arg("-C")
.arg(&worktree_path)
.args(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
.output()
.expect("git rev-parse @{u}");
assert!(
upstream.status.success(),
"worktree branch must have an upstream set (git pull must work): {}",
String::from_utf8_lossy(&upstream.stderr)
);
assert_eq!(
String::from_utf8_lossy(&upstream.stdout).trim(),
"origin/main",
"worktree branch upstream must track origin/main"
);
let push_default = std::process::Command::new("git")
.arg("-C")
.arg(&worktree_path)
.args(["config", "--worktree", "push.default"])
.output()
.expect("git config --worktree push.default");
assert!(
push_default.status.success(),
"worktree-scoped push.default must be set: {}",
String::from_utf8_lossy(&push_default.stderr)
);
assert_eq!(
String::from_utf8_lossy(&push_default.stdout).trim(),
"current",
"push.default must be worktree-scoped to 'current' so push never targets main"
);
let ext = std::process::Command::new("git")
.arg("-C")
.arg(&base)
.args(["config", "extensions.worktreeConfig"])
.output()
.expect("git config extensions.worktreeConfig");
assert!(
ext.status.success(),
"extensions.worktreeConfig must be set on the base clone"
);
assert_eq!(
String::from_utf8_lossy(&ext.stdout).trim(),
"true",
"extensions.worktreeConfig must be true"
);
}