"""Tests for the branch-worktree helper scripts (#38/#39). Exercises path generation and refuse-to-overwrite via the scripts' ``--dry-run`` mode, so no real worktrees, branches, network, or deletions are involved. """ import os import subprocess import unittest from pathlib import Path REPO = Path(__file__).resolve().parent.parent SCRIPTS = REPO / "scripts" BRANCHES = REPO / "branches" def run(script, *args): branch = None for arg in args: if not arg.startswith("-"): branch = arg break lock_dir_ctx = None extra_env = os.environ.copy() if script == "worktree-start" and branch: import re import tempfile import issue_lock_store m = re.search(r"issue-(\d+)", branch) if not m: m = re.search(r"pr-(\d+)", branch) issue_num = int(m.group(1)) if m else 999 lock_dir_ctx = tempfile.TemporaryDirectory() extra_env["GITEA_ISSUE_LOCK_DIR"] = lock_dir_ctx.name record = { "issue_number": issue_num, "branch_name": branch, "remote": "prgs", "org": "Scaled-Tech-Consulting", "repo": "Gitea-Tools", "worktree_path": "/tmp/test-worktree", "work_lease": { "operation_type": "author_issue_work", "expires_at": "2999-01-01T00:00:00Z", }, } path = issue_lock_store.lock_file_path( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue_number=issue_num, lock_dir=lock_dir_ctx.name, ) issue_lock_store.save_lock_file(path, record) try: proc = subprocess.run( ["bash", str(SCRIPTS / script), *args], capture_output=True, text=True, cwd=str(REPO), env=extra_env, ) return proc.returncode, proc.stdout, proc.stderr finally: if lock_dir_ctx is not None: lock_dir_ctx.cleanup() class TestWorktreeStart(unittest.TestCase): def test_dry_run_path_generation(self): rc, out, _ = run("worktree-start", "--dry-run", "fix/issue-123-example") self.assertEqual(rc, 0) self.assertIn("branches/fix-issue-123-example", out) self.assertIn("fix/issue-123-example", out) self.assertIn("prgs/master", out) # default start-ref def test_bad_args_exit_2(self): rc, _, _ = run("worktree-start") self.assertEqual(rc, 2) def test_refuses_existing_worktree(self): branch = f"fix/issue-999-refuse-{os.getpid()}" slug = branch.replace("/", "-") target = BRANCHES / slug target.mkdir(parents=True, exist_ok=True) try: rc, _, err = run("worktree-start", "--dry-run", branch) self.assertEqual(rc, 1) self.assertIn("Refusing to reuse", err) finally: target.rmdir() # -- issue-linked branch validation (#48) -------------------------------- def test_accepts_issue_linked_impl_branches(self): for branch in ("fix/issue-123-example", "feat/issue-123-example", "docs/issue-123-example", "chore/issue-123-example"): rc, out, err = run("worktree-start", "--dry-run", branch) self.assertEqual(rc, 0, f"{branch}: {err}") self.assertIn(f"branches/{branch.replace('/', '-')}", out) def test_accepts_review_branch(self): rc, out, _ = run("worktree-start", "--dry-run", "review/pr-456-example") self.assertEqual(rc, 0) self.assertIn("branches/review-pr-456-example", out) def test_rejects_untraceable_branches(self): for branch in ("fix/random-name", "my-branch", "feat/no-issue-here", "fix/issue-abc-x"): rc, _, err = run("worktree-start", "--dry-run", branch) self.assertEqual(rc, 2, branch) self.assertIn("Untraceable branch name", err) def test_allow_unlinked_override(self): rc, out, _ = run("worktree-start", "--dry-run", "--allow-unlinked", "my-branch") self.assertEqual(rc, 0) self.assertIn("branches/my-branch", out) class TestWorktreeReview(unittest.TestCase): def test_dry_run_detached_review_path(self): rc, out, _ = run("worktree-review", "--dry-run", "fix/issue-9-x") self.assertEqual(rc, 0) self.assertIn("branches/review-fix-issue-9-x", out) self.assertIn("--detach", out) self.assertIn("prgs/fix/issue-9-x", out) # default start-ref def test_refuses_existing_review_worktree(self): slug = f"review-zz-refuse-rev-{os.getpid()}" target = BRANCHES / slug target.mkdir(parents=True, exist_ok=True) try: # branch name maps to the same slug: review- rc, _, err = run("worktree-review", "--dry-run", f"zz-refuse-rev-{os.getpid()}") self.assertEqual(rc, 1) self.assertIn("Refusing to reuse", err) finally: target.rmdir() class TestWorktreeClean(unittest.TestCase): def test_missing_worktree_errors(self): rc, _, err = run("worktree-clean", "--dry-run", "does-not-exist-xyz") self.assertEqual(rc, 1) self.assertIn("No such worktree", err) def test_dry_run_does_not_delete(self): slug = f"zz-clean-{os.getpid()}" target = BRANCHES / slug target.mkdir(parents=True, exist_ok=True) try: rc, out, _ = run("worktree-clean", "--dry-run", slug) self.assertEqual(rc, 0) self.assertIn("worktree remove", out) self.assertTrue(target.is_dir()) # nothing removed in dry-run finally: target.rmdir() def test_dry_run_delete_branch_lists_branch_command(self): slug = f"zz-clean-b-{os.getpid()}" target = BRANCHES / slug target.mkdir(parents=True, exist_ok=True) try: rc, out, _ = run("worktree-clean", "--dry-run", "--delete-branch", slug) self.assertEqual(rc, 0) self.assertIn("branch -d", out) finally: target.rmdir() if __name__ == "__main__": unittest.main()