"""Tests for preserving and resuming prepared Gitea review drafts (#609).""" from __future__ import annotations import os import sys import tempfile import unittest from pathlib import Path from unittest.mock import patch, MagicMock sys_path_root = str(Path(__file__).resolve().parent.parent) if sys_path_root not in sys.path: sys.path.insert(0, sys_path_root) import mcp_session_state import gitea_mcp_server import reviewer_pr_lease class TestReviewDrafts(unittest.TestCase): def setUp(self): self._tmpdir = tempfile.TemporaryDirectory() self.state_dir = self._tmpdir.name self._env = patch.dict( os.environ, { mcp_session_state.STATE_DIR_ENV: self.state_dir, mcp_session_state.SESSION_PROFILE_LOCK_ENV: "prgs-reviewer", "GITEA_MCP_PROFILE": "prgs-reviewer", "GITEA_PROFILE_NAME": "prgs-reviewer", "PYTEST_CURRENT_TEST": "1", # skip runtime stale checks }, clear=False, ) self._env.start() # Mock workspace binding to pass in test context self._nwb_patch = patch( "gitea_mcp_server.nwb.assess_namespace_mutation_workspace", return_value={"block": False, "mutation_workspace": "/tmp/test-worktree"}, ) self._nwb_patch.start() # Mock authentication headers self._auth_patch = patch( "gitea_mcp_server.get_auth_header", return_value="Bearer mock-token", ) self._auth_patch.start() self._username_patch = patch( "gitea_mcp_server._authenticated_username", return_value="sysadmin", ) self._username_patch.start() # Clear/Mock local session lease in-memory state reviewer_pr_lease.clear_session_lease() def tearDown(self): self._username_patch.stop() self._auth_patch.stop() self._nwb_patch.stop() self._env.stop() self._tmpdir.cleanup() reviewer_pr_lease.clear_session_lease() @patch("gitea_mcp_server.api_request") def test_save_draft_success(self, mock_api): print("GITEA_MCP_SERVER FILE:", gitea_mcp_server.__file__) print("DIR OF GITEA_MCP_SERVER:", dir(gitea_mcp_server)) mock_api.return_value = { "head": {"sha": "headsha1234567890"}, "base": {"ref": "master", "sha": "basesha0987654321"}, } res = gitea_mcp_server.gitea_save_review_draft( pr_number=587, action="approve", body="Good changes", expected_head_sha="headsha1234567890", validation_commands="pytest tests/", validation_results="all pass", blocker_reason="stale daemon", remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", worktree_path="/tmp/test-worktree", ) self.assertTrue(res.get("success")) self.assertEqual(res.get("head_sha"), "headsha1234567890") # Load draft and verify fields draft = mcp_session_state.load_state( kind=mcp_session_state.KIND_REVIEW_DRAFT, remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", profile_identity="prgs-reviewer", ) self.assertIsNotNone(draft) self.assertEqual(draft.get("pr_number"), 587) self.assertEqual(draft.get("action"), "approve") self.assertEqual(draft.get("body"), "Good changes") self.assertEqual(draft.get("validation_commands"), "pytest tests/") self.assertEqual(draft.get("validation_results"), "all pass") self.assertEqual(draft.get("blocker_reason"), "stale daemon") self.assertEqual(os.path.realpath(draft.get("worktree_path")), os.path.realpath("/tmp/test-worktree")) @patch("gitea_mcp_server.api_request") @patch("gitea_mcp_server._fetch_pr_comments") @patch("gitea_mcp_server.gitea_submit_pr_review") def test_resume_draft_and_submit_success(self, mock_submit, mock_comments, mock_api): # 1. Save draft mock_api.return_value = { "state": "open", "head": {"sha": "headsha1234567890"}, "base": {"ref": "master", "sha": "basesha0987654321"}, } gitea_mcp_server.gitea_save_review_draft( pr_number=587, action="approve", body="Good changes", expected_head_sha="headsha1234567890", remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", worktree_path="/tmp/test-worktree", ) # Seed local session lease reviewer_pr_lease.record_session_lease({ "pr_number": 587, "session_id": "session-1234", }) # Mock Gitea comments to return active reviewer lease owned by us mock_comments.return_value = [ { "id": 1, "user": {"username": "sysadmin"}, "body": ( "\n" "repo: Scaled-Tech-Consulting/Gitea-Tools\n" "pr: #587\n" "reviewer_identity: sysadmin\n" "profile: prgs-reviewer\n" "session_id: session-1234\n" "phase: claimed\n" ), } ] mock_submit.return_value = {"performed": True, "success": True} # 2. Resume draft res = gitea_mcp_server.gitea_resume_review_draft( pr_number=587, submit=True, remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", worktree_path="/tmp/test-worktree", ) self.assertTrue(res.get("success")) self.assertTrue(res.get("marked_ready")) self.assertTrue(res.get("submitted")) mock_submit.assert_called_once() # Verify draft was deleted after successful submit draft = mcp_session_state.load_state( kind=mcp_session_state.KIND_REVIEW_DRAFT, remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", profile_identity="prgs-reviewer", ) self.assertIsNone(draft) @patch("gitea_mcp_server.api_request") @patch("gitea_mcp_server._fetch_pr_comments") def test_resume_draft_fails_checks(self, mock_comments, mock_api): # Save a draft mock_api.return_value = { "state": "open", "head": {"sha": "headsha1234567890"}, "base": {"ref": "master", "sha": "basesha0987654321"}, } gitea_mcp_server.gitea_save_review_draft( pr_number=587, action="approve", body="Good changes", expected_head_sha="headsha1234567890", remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", worktree_path="/tmp/test-worktree", ) # Seed local session lease reviewer_pr_lease.record_session_lease({ "pr_number": 587, "session_id": "session-1234", }) # Mock Gitea comments to return active reviewer lease owned by us mock_comments.return_value = [ { "id": 1, "user": {"username": "sysadmin"}, "body": ( "\n" "repo: Scaled-Tech-Consulting/Gitea-Tools\n" "pr: #587\n" "reviewer_identity: sysadmin\n" "profile: prgs-reviewer\n" "session_id: session-1234\n" "phase: claimed\n" ), } ] # Case A: PR closed mock_api.return_value = { "state": "closed", "head": {"sha": "headsha1234567890"}, "base": {"ref": "master", "sha": "basesha0987654321"}, } res = gitea_mcp_server.gitea_resume_review_draft( pr_number=587, remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", worktree_path="/tmp/test-worktree", ) self.assertFalse(res.get("success")) self.assertIn("PR #587 is not open", res.get("reasons")[0]) # Case B: Head changed mock_api.return_value = { "state": "open", "head": {"sha": "headsha_NEW"}, "base": {"ref": "master", "sha": "basesha0987654321"}, } res = gitea_mcp_server.gitea_resume_review_draft( pr_number=587, remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", worktree_path="/tmp/test-worktree", ) self.assertFalse(res.get("success")) self.assertIn("PR head SHA changed", res.get("reasons")[0]) # Case C: Target branch changed mock_api.return_value = { "state": "open", "head": {"sha": "headsha1234567890"}, "base": {"ref": "master", "sha": "basesha_NEW"}, } res = gitea_mcp_server.gitea_resume_review_draft( pr_number=587, remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", worktree_path="/tmp/test-worktree", ) self.assertFalse(res.get("success")) self.assertIn("target branch SHA changed", res.get("reasons")[0]) # Case D: Worktree mismatch mock_api.return_value = { "state": "open", "head": {"sha": "headsha1234567890"}, "base": {"ref": "master", "sha": "basesha0987654321"}, } res = gitea_mcp_server.gitea_resume_review_draft( pr_number=587, remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", worktree_path="/tmp/different-worktree", ) self.assertFalse(res.get("success")) self.assertIn("worktree binding mismatch", res.get("reasons")[0])