"""Tests for root checkout guard (#475).""" from __future__ import annotations import os import sys import unittest from pathlib import Path from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) import gitea_mcp_server as srv # noqa: E402 import root_checkout_guard as rcg # noqa: E402 current_file_path = Path(__file__).resolve() if "branches" in current_file_path.parts: CONTROL_ROOT = str(current_file_path.parents[3]) BRANCHES_WORKTREE = str(current_file_path.parents[1]) else: CONTROL_ROOT = str(current_file_path.parents[1]) BRANCHES_WORKTREE = str(current_file_path.parents[1] / "branches" / "mock-worktree") MASTER_SHA = "a" * 40 OTHER_SHA = "b" * 40 class TestAssessRootCheckoutGuard(unittest.TestCase): def _assess(self, **kwargs): defaults = { "workspace_path": CONTROL_ROOT, "canonical_repo_root": CONTROL_ROOT, "current_branch": "master", "head_sha": MASTER_SHA, "porcelain_status": "", "remote_master_sha": MASTER_SHA, "resolved_role": "author", } defaults.update(kwargs) return rcg.assess_root_checkout_guard(**defaults) def test_clean_master_control_checkout_allowed(self): result = self._assess() self.assertTrue(result["proven"]) self.assertFalse(result["block"]) def test_branches_worktree_allowed_for_author(self): result = self._assess( workspace_path=BRANCHES_WORKTREE, current_branch="feat/issue-475-root-checkout-guard", head_sha=OTHER_SHA, resolved_role="author", ) self.assertTrue(result["proven"]) def test_branches_worktree_allowed_for_reviewer(self): result = self._assess( workspace_path=f"{CONTROL_ROOT}/branches/review-pr-1", current_branch="review-pr-1", resolved_role="reviewer", ) self.assertTrue(result["proven"]) def test_reconciler_always_allowed(self): result = self._assess( current_branch="feat/some-branch", head_sha=OTHER_SHA, porcelain_status=" M gitea_mcp_server.py\n", resolved_role="reconciler", ) self.assertTrue(result["proven"]) def test_feature_branch_on_control_checkout_blocked(self): result = self._assess( current_branch="feat/issue-99-example", head_sha=OTHER_SHA, ) self.assertTrue(result["block"]) self.assertIn("not a stable base branch", result["reasons"][0]) def test_detached_head_blocked(self): result = self._assess(current_branch=None) self.assertTrue(result["block"]) self.assertIn("detached HEAD", result["reasons"][0]) def test_dirty_control_checkout_blocked(self): result = self._assess(porcelain_status=" M gitea_mcp_server.py\n") self.assertTrue(result["block"]) self.assertIn("tracked local edits", result["reasons"][0]) def test_head_behind_prgs_master_blocked(self): result = self._assess( head_sha=OTHER_SHA, remote_master_sha=MASTER_SHA, ) self.assertTrue(result["block"]) self.assertIn("does not match prgs/master", result["reasons"][0]) def test_merger_requires_clean_control_checkout(self): result = self._assess( workspace_path=BRANCHES_WORKTREE, current_branch="feat/issue-475-root-checkout-guard", head_sha=OTHER_SHA, resolved_role="merger", ) self.assertTrue(result["block"]) class TestVerifyPreflightRootGuardIntegration(unittest.TestCase): def setUp(self): srv._preflight_whoami_called = True srv._preflight_capability_called = True srv._preflight_resolved_role = "reviewer" srv._preflight_whoami_violation = False srv._preflight_capability_violation = False self._orig_in_test = srv._preflight_in_test_mode srv._preflight_in_test_mode = lambda: False def tearDown(self): srv._preflight_in_test_mode = self._orig_in_test @patch("gitea_mcp_server._get_workspace_porcelain", return_value="") @patch("gitea_mcp_server.root_checkout_guard.resolve_remote_master_sha", return_value=MASTER_SHA) @patch("gitea_mcp_server.issue_lock_worktree.read_worktree_git_state") @patch("gitea_mcp_server._resolve_author_mutation_context") def test_reviewer_from_contaminated_root_blocked( self, mock_ctx, mock_git, _remote_sha, _porcelain, ): srv._preflight_capability_baseline_porcelain = "" mock_ctx.return_value = { "workspace_path": CONTROL_ROOT, "canonical_repo_root": CONTROL_ROOT, "process_project_root": CONTROL_ROOT, } mock_git.return_value = { "current_branch": "feat/hijacked-root", "head_sha": OTHER_SHA, "porcelain_status": "", } with self.assertRaises(RuntimeError) as ctx: srv.verify_preflight_purity("prgs", worktree_path=CONTROL_ROOT) self.assertIn("Root checkout guard (#475)", str(ctx.exception)) self.assertIn(rcg.REMEDIATION, str(ctx.exception)) @patch("gitea_mcp_server._get_workspace_porcelain", return_value="") @patch("gitea_mcp_server.root_checkout_guard.resolve_remote_master_sha", return_value=MASTER_SHA) @patch("gitea_mcp_server.issue_lock_worktree.read_worktree_git_state") @patch("gitea_mcp_server._resolve_author_mutation_context") def test_reviewer_from_branches_worktree_allowed( self, mock_ctx, mock_git, _remote_sha, _porcelain, ): srv._preflight_capability_baseline_porcelain = "" mock_ctx.return_value = { "workspace_path": BRANCHES_WORKTREE, "canonical_repo_root": CONTROL_ROOT, "process_project_root": BRANCHES_WORKTREE, } mock_git.return_value = { "current_branch": "feat/issue-475-root-checkout-guard", "head_sha": OTHER_SHA, "porcelain_status": "", } srv.verify_preflight_purity("prgs", worktree_path=BRANCHES_WORKTREE) if __name__ == "__main__": unittest.main()