"""Tests for src/git_worktree (ORCH-2 / S-4): isolated worktree per task/branch. Uses real local git repos in tmp (a bare 'origin' + a working main clone) so that `git fetch origin`, `git worktree add`, branch creation from origin/main, reuse and removal are all exercised without network access. """ import os import subprocess import tempfile import pytest # Env must be set before importing app modules (same convention as the other suites). _test_db = os.path.join(tempfile.gettempdir(), "test_orchestrator_wt.db") os.environ["ORCH_DB_PATH"] = _test_db os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir() os.environ["ORCH_GITEA_TOKEN"] = "test-token" os.environ["ORCH_PLANE_API_TOKEN"] = "test-token" from src import git_worktree from src.git_worktree import ( _safe, get_worktree_path, ensure_worktree, remove_worktree, ) def _git(cwd, *args): return subprocess.run(["git", "-C", cwd, *args], capture_output=True, text=True) @pytest.fixture def repos(tmp_path, monkeypatch): """Build a bare 'origin' with main + a feature branch, plus a main clone at repos_dir/. Returns the repo name. settings.repos_dir / worktrees_dir are pointed at tmp. """ repo = "enduro-trails" repos_dir = tmp_path / "repos" wt_dir = tmp_path / "repos" / "_wt" repos_dir.mkdir(parents=True) monkeypatch.setattr(git_worktree.settings, "repos_dir", str(repos_dir)) monkeypatch.setattr(git_worktree.settings, "worktrees_dir", str(wt_dir)) # Bare origin origin = tmp_path / "origin.git" subprocess.run(["git", "init", "--bare", "-b", "main", str(origin)], capture_output=True) # Seed repo seed = tmp_path / "seed" seed.mkdir() _git(str(seed), "init", "-b", "main") _git(str(seed), "config", "user.email", "t@t") _git(str(seed), "config", "user.name", "t") (seed / "README.md").write_text("# seed\n") _git(str(seed), "add", ".") _git(str(seed), "commit", "-m", "init") _git(str(seed), "remote", "add", "origin", str(origin)) _git(str(seed), "push", "origin", "main") # An existing feature branch on origin _git(str(seed), "checkout", "-b", "feature/existing") (seed / "f.txt").write_text("feature\n") _git(str(seed), "add", ".") _git(str(seed), "commit", "-m", "feat") _git(str(seed), "push", "origin", "feature/existing") # Main clone at repos_dir/ main_clone = repos_dir / repo subprocess.run(["git", "clone", str(origin), str(main_clone)], capture_output=True) _git(str(main_clone), "config", "user.email", "t@t") _git(str(main_clone), "config", "user.name", "t") return repo # --------------------------------------------------------------------------- # _safe / get_worktree_path # --------------------------------------------------------------------------- class TestSafeAndPath: def test_safe_replaces_slashes_and_specials(self): assert _safe("feature/ET-001-x") == "feature_ET-001-x" assert _safe("a b/c:d") == "a_b_c_d" assert _safe("keep.dots-and_underscores") == "keep.dots-and_underscores" def test_get_worktree_path(self, monkeypatch): monkeypatch.setattr(git_worktree.settings, "worktrees_dir", "/repos/_wt") assert get_worktree_path("repo", "feature/x") == "/repos/_wt/repo/feature_x" # --------------------------------------------------------------------------- # ensure_worktree # --------------------------------------------------------------------------- class TestEnsureWorktree: def test_missing_main_repo_raises(self, tmp_path, monkeypatch): monkeypatch.setattr(git_worktree.settings, "repos_dir", str(tmp_path / "nope")) monkeypatch.setattr(git_worktree.settings, "worktrees_dir", str(tmp_path / "_wt")) with pytest.raises(FileNotFoundError): ensure_worktree("enduro-trails", "main") def test_creates_worktree_for_existing_branch(self, repos): wt = ensure_worktree(repos, "feature/existing") assert os.path.isdir(wt) assert wt == get_worktree_path(repos, "feature/existing") # On the right branch cur = _git(wt, "branch", "--show-current").stdout.strip() assert cur == "feature/existing" # Feature file from that branch is present (proves correct checkout) assert os.path.isfile(os.path.join(wt, "f.txt")) def test_creates_new_branch_from_origin_main(self, repos): wt = ensure_worktree(repos, "feature/brand-new") assert os.path.isdir(wt) cur = _git(wt, "branch", "--show-current").stdout.strip() assert cur == "feature/brand-new" # Based on main -> README present, no feature file assert os.path.isfile(os.path.join(wt, "README.md")) assert not os.path.isfile(os.path.join(wt, "f.txt")) def test_reuse_returns_same_path(self, repos): wt1 = ensure_worktree(repos, "feature/existing") wt2 = ensure_worktree(repos, "feature/existing") assert wt1 == wt2 assert os.path.isdir(wt2) def test_two_branches_are_isolated(self, repos): a = ensure_worktree(repos, "feature/wt-A") b = ensure_worktree(repos, "feature/wt-B") assert a != b ba = _git(a, "branch", "--show-current").stdout.strip() bb = _git(b, "branch", "--show-current").stdout.strip() assert ba == "feature/wt-A" assert bb == "feature/wt-B" # Writing in A must not affect B with open(os.path.join(a, "only-a.txt"), "w") as f: f.write("a") assert not os.path.isfile(os.path.join(b, "only-a.txt")) # --------------------------------------------------------------------------- # remove_worktree # --------------------------------------------------------------------------- class TestRemoveWorktree: def test_remove_deletes_worktree_dir(self, repos): wt = ensure_worktree(repos, "feature/to-remove") assert os.path.isdir(wt) remove_worktree(repos, "feature/to-remove") assert not os.path.isdir(wt) def test_remove_nonexistent_is_noop(self, repos): # Should not raise even if the worktree was never created. remove_worktree(repos, "feature/never-made")