"""Regression tests for gitea_lock_issue MCP tool registration (#521).""" from __future__ import annotations import os import tempfile import unittest from unittest.mock import patch import issue_lock_store import mcp_server from mcp_server import gitea_create_pr, gitea_lock_issue FAKE_AUTH = "Basic dGVzdDp0ZXN0" ISSUE_WRITE_ENV = { "GITEA_ALLOWED_OPERATIONS": ( "gitea.issue.create,gitea.issue.close,gitea.issue.comment" ), } CREATE_PR_ENV = { "GITEA_PROFILE_NAME": "author-test", "GITEA_ALLOWED_OPERATIONS": ( "gitea.read,gitea.pr.create,gitea.branch.push," "gitea.issue.create,gitea.issue.close,gitea.issue.comment" ), "GITEA_FORBIDDEN_OPERATIONS": ( "gitea.pr.approve,gitea.pr.merge,gitea.pr.review" ), } def _clean_master_git_state_for_lock(): return { "current_branch": "master", "porcelain_status": "", "base_equivalent": True, "inspected_git_root": "/scratch/wt", "base_branch": "origin/master", } def _registered_tool_names() -> set[str]: manager = mcp_server.mcp._tool_manager tools = getattr(manager, "_tools", None) or {} return set(tools.keys()) class TestLockIssueMcpRegistration(unittest.TestCase): def test_gitea_lock_issue_registered_as_public_mcp_tool(self): names = _registered_tool_names() self.assertIn("gitea_lock_issue", names) def test_internal_list_open_pulls_not_exposed_as_mcp_tool(self): names = _registered_tool_names() self.assertNotIn("_list_open_pulls", names) class TestCreatePrLockRegistrationFlow(unittest.TestCase): def setUp(self): self._lock_dir = tempfile.TemporaryDirectory() self._env_patcher = patch.dict( os.environ, { **CREATE_PR_ENV, "GITEA_ISSUE_LOCK_DIR": self._lock_dir.name, }, clear=True, ) self._env_patcher.start() self._dup_fetcher_patcher = patch( "mcp_server.issue_duplicate_context_fetcher", return_value=([], [], {"status": "not_claimed"}), ) self._dup_fetcher_patcher.start() def tearDown(self): self._dup_fetcher_patcher.stop() self._env_patcher.stop() self._lock_dir.cleanup() @patch("mcp_server.role_session_router.check_author_mutation_after_reviewer_stop", return_value=(True, [])) @patch("mcp_server.get_auth_header", return_value=FAKE_AUTH) def test_create_pr_still_fails_closed_without_issue_lock(self, _auth, _role): with self.assertRaises(RuntimeError) as ctx: gitea_create_pr( title="feat: X Closes #521", head="feat/issue-521-lock-issue-registration", remote="prgs", ) self.assertIn("Issue lock is missing", str(ctx.exception)) @patch("mcp_server.api_request") @patch( "mcp_server.issue_lock_worktree.read_worktree_git_state", return_value=_clean_master_git_state_for_lock(), ) @patch("mcp_server.api_get_all", return_value=[{"id": 4, "name": "status:pr-open"}]) @patch("mcp_server.role_session_router.check_author_mutation_after_reviewer_stop", return_value=(True, [])) @patch("mcp_server.get_auth_header", return_value=FAKE_AUTH) def test_create_pr_proceeds_after_valid_issue_lock( self, _auth, _role, _api, _git_state, mock_api_request ): worktree = os.path.realpath(os.getcwd()) def mock_api(method, url, auth, data=None): if "/labels" in url: return [{"name": "status:pr-open"}] return {"number": 521, "html_url": "https://example/pr/521"} mock_api_request.side_effect = mock_api lock_res = gitea_lock_issue( issue_number=521, branch_name="feat/issue-521-lock-issue-registration", remote="prgs", worktree_path=worktree, ) self.assertTrue(lock_res["success"]) lock_path = lock_res["lock_file_path"] self.assertTrue(os.path.exists(lock_path)) lock = issue_lock_store.read_lock_file(lock_path) self.assertEqual(lock["issue_number"], 521) res = gitea_create_pr( title="fix: restore lock tool registration Closes #521", head="feat/issue-521-lock-issue-registration", remote="prgs", worktree_path=worktree, ) self.assertEqual(res["number"], 521)