"""Tests for gitea_reconcile_already_landed_pr MCP tool (#310).""" import sys import unittest from unittest.mock import patch sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent)) import mcp_server from mcp_server import gitea_reconcile_already_landed_pr RECONCILER_PROFILE = { "profile_name": "prgs-reconciler", "allowed_operations": [ "gitea.read", "gitea.pr.comment", "gitea.issue.comment", "gitea.issue.close", "gitea.pr.close", ], "forbidden_operations": [ "gitea.pr.approve", "gitea.pr.merge", "gitea.pr.create", "gitea.branch.push", ], "audit_label": "prgs-reconciler", } AUTHOR_PROFILE = { "profile_name": "prgs-author", "allowed_operations": [ "gitea.read", "gitea.pr.create", "gitea.issue.comment", ], "forbidden_operations": ["gitea.pr.close", "gitea.pr.approve", "gitea.pr.merge"], "audit_label": "prgs-author", } OPEN_LANDED_PR = { "number": 278, "title": "Already landed (Closes #263)", "body": "", "state": "open", "head": {"ref": "feat/issue-263", "sha": "2a544b78d1371925761d16f1c451bb3b2984470e"}, "base": {"ref": "master"}, } class TestReconcileAlreadyLandedPrTool(unittest.TestCase): def setUp(self): self.mock_api = patch("mcp_server.api_request").start() self.mock_auth = patch( "mcp_server.get_auth_header", return_value="token test" ).start() patch("mcp_server.verify_preflight_purity").start() patch("gitea_audit.audit_enabled", return_value=False).start() mcp_server._IDENTITY_CACHE.clear() def tearDown(self): patch.stopall() mcp_server._IDENTITY_CACHE.clear() def _set_profile(self, profile): patch("mcp_server.get_profile", return_value=profile).start() def test_close_blocked_without_pr_close_capability(self): self._set_profile(AUTHOR_PROFILE) res = gitea_reconcile_already_landed_pr( pr_number=278, close_pr=True, remote="prgs", ) self.assertFalse(res["success"]) self.assertFalse(res["performed"]) self.assertEqual(res["required_permission"], "gitea.pr.close") self.mock_api.assert_not_called() def test_not_landed_pr_close_denied_after_live_fetch(self): self._set_profile(RECONCILER_PROFILE) self.mock_api.return_value = OPEN_LANDED_PR with patch( "mcp_server.already_landed_reconcile.assess_already_landed_reconciliation", return_value={ "pr_state": "open", "candidate_head_sha": OPEN_LANDED_PR["head"]["sha"], "target_branch": "master", "target_branch_sha": "deadbeef", "ancestor_proof": False, "eligibility_class": "NOT_ALREADY_LANDED", "linked_issue": 263, "close_allowed": False, "git_ref_mutations": ["git fetch prgs master"], "reasons": ["not ancestor"], }, ): res = gitea_reconcile_already_landed_pr( pr_number=278, close_pr=True, remote="prgs", ) self.assertFalse(res["success"]) self.assertFalse(res.get("pr_closed")) patch_calls = [ c for c in self.mock_api.call_args_list if c.args[0] == "PATCH" ] self.assertEqual(patch_calls, []) def test_already_landed_close_allowed_with_capability(self): self._set_profile(RECONCILER_PROFILE) def api_side_effect(method, url, auth, payload=None): if method == "GET" and "/pulls/278" in url: return dict(OPEN_LANDED_PR) if method == "PATCH" and "/pulls/278" in url: closed = dict(OPEN_LANDED_PR) closed["state"] = "closed" return closed return {} self.mock_api.side_effect = api_side_effect with patch( "mcp_server.already_landed_reconcile.assess_already_landed_reconciliation", return_value={ "pr_state": "open", "candidate_head_sha": OPEN_LANDED_PR["head"]["sha"], "target_branch": "master", "target_branch_sha": "e017d80256217f17b0f421cd051cfa5cbcf5ae0f", "ancestor_proof": True, "eligibility_class": "ALREADY_LANDED_RECONCILE_REQUIRED", "linked_issue": 263, "close_allowed": True, "git_ref_mutations": ["git fetch prgs master"], "reasons": [], }, ): res = gitea_reconcile_already_landed_pr( pr_number=278, close_pr=True, remote="prgs", ) self.assertTrue(res["success"]) self.assertTrue(res["performed"]) self.assertTrue(res["pr_closed"]) self.assertEqual(res["eligibility_class"], "ALREADY_LANDED_RECONCILE_REQUIRED") if __name__ == "__main__": unittest.main()