fix(pr-694): resolve master merge conflicts for #693 decision-lock isolation

Preserve #693 classification/correction helpers and integrate master
#709 recovery provenance APIs; keep both capability-map entry sets.
This commit is contained in:
2026-07-17 08:44:32 -04:00
50 changed files with 14236 additions and 316 deletions
+42 -15
View File
@@ -836,16 +836,24 @@ class TestMergePR(unittest.TestCase):
)
def _feedback_reads(self, author="author-bot", sha="abc123"):
"""PR + reviews GETs for gitea_get_pr_review_feedback during merge."""
"""PR + reviews GETs for one gitea_get_pr_review_feedback call."""
return [self._pr(author, sha=sha), _visible_approval_reviews(sha=sha)]
def _eligibility_merge_reads(self, author="author-bot", sha="abc123"):
"""Eligibility user/PR plus #695 merge-approval feedback PR+reviews."""
return [
{"login": "merger-bot"},
self._pr(author, sha=sha),
*self._feedback_reads(author=author, sha=sha),
]
# -- success --------------------------------------------------------------
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_merge_succeeds_when_all_gates_pass(self, _auth, mock_api):
mock_api.side_effect = [
{"login": "merger-bot"}, self._pr("author-bot"),
*self._eligibility_merge_reads(),
*self._feedback_reads(),
{}, # merge POST
{"merged_commit_sha": "mergecommit99"}, # read-back
@@ -864,7 +872,7 @@ class TestMergePR(unittest.TestCase):
self.assertEqual(r["merge_method"], "squash")
self.assertEqual(r["merge_commit"], "mergecommit99")
# 5th call is the merge POST with the requested method/title/message.
merge_call = mock_api.call_args_list[4]
merge_call = mock_api.call_args_list[6]
self.assertEqual(merge_call.args[0], "POST")
self.assertTrue(merge_call.args[1].endswith("/pulls/8/merge"))
payload = merge_call.args[3]
@@ -877,7 +885,7 @@ class TestMergePR(unittest.TestCase):
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_expected_changed_files_match_allows(self, _auth, mock_api):
mock_api.side_effect = [
{"login": "merger-bot"}, self._pr("author-bot"),
*self._eligibility_merge_reads(),
[{"filename": "a.py"}, {"filename": "b.py"}], # files
*self._feedback_reads(),
{}, # merge POST
@@ -899,7 +907,7 @@ class TestMergePR(unittest.TestCase):
def test_readback_failure_reports_skipped_cleanup(self, _auth, mock_api):
"""Merge OK + read-back GET failure => explicit cleanup skip, not silence."""
mock_api.side_effect = [
{"login": "merger-bot"}, self._pr("author-bot"),
*self._eligibility_merge_reads(),
*self._feedback_reads(),
{}, # merge POST
RuntimeError("HTTP 502: Gitea upstream unavailable"), # read-back fails
@@ -919,7 +927,7 @@ class TestMergePR(unittest.TestCase):
self.assertEqual(r["cleanup_status"], "skipped (merge read-back failed)")
# No tracker-cleanup API traffic after the failed read-back:
# user, PR (eligibility), feedback PR+reviews, merge POST, read-back.
self.assertEqual(mock_api.call_count, 6)
self.assertEqual(mock_api.call_count, 8)
for c in mock_api.call_args_list:
self.assertNotEqual(c.args[0], "DELETE")
@@ -930,7 +938,7 @@ class TestMergePR(unittest.TestCase):
def test_cleanup_exception_surfaced_and_redacted(self, _auth, mock_api, _cleanup):
"""Unexpected cleanup exception => merge still succeeds; error surfaced redacted."""
mock_api.side_effect = [
{"login": "merger-bot"}, self._pr("author-bot"),
*self._eligibility_merge_reads(),
*self._feedback_reads(),
{}, # merge POST
{"merged_commit_sha": "c9"}, # read-back OK
@@ -1142,8 +1150,10 @@ class TestMergePR(unittest.TestCase):
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_head_sha_mismatch_blocks(self, _auth, mock_api):
# Eligibility (#695) needs approval feedback before head-gate runs.
mock_api.side_effect = [
{"login": "merger-bot"}, self._pr("author-bot", sha="abc123")]
*self._eligibility_merge_reads(sha="abc123"),
]
env = {"GITEA_PROFILE_NAME": "gitea-merger",
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
with patch.dict(os.environ, env, clear=True):
@@ -1162,7 +1172,7 @@ class TestMergePR(unittest.TestCase):
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_changed_files_mismatch_blocks(self, _auth, mock_api):
mock_api.side_effect = [
{"login": "merger-bot"}, self._pr("author-bot"),
*self._eligibility_merge_reads(),
[{"filename": "a.py"}, {"filename": "c.py"}], # actual files
]
env = {"GITEA_PROFILE_NAME": "gitea-merger",
@@ -1193,7 +1203,7 @@ class TestMergePR(unittest.TestCase):
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_output_redacts_secrets(self, _auth, mock_api):
mock_api.side_effect = [
{"login": "merger-bot"}, self._pr("author-bot"),
*self._eligibility_merge_reads(),
*self._feedback_reads(),
{}, {"merged_commit_sha": "c1"},
]
@@ -1213,7 +1223,7 @@ class TestMergePR(unittest.TestCase):
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_merge_error_message_redacts_credential(self, _auth, mock_api):
mock_api.side_effect = [
{"login": "merger-bot"}, self._pr("author-bot"),
*self._eligibility_merge_reads(),
*self._feedback_reads(),
RuntimeError("HTTP 500: token abc-secret-xyz rejected"),
]
@@ -1234,6 +1244,7 @@ class TestMergePR(unittest.TestCase):
def test_merge_blocked_on_stale_approval_head(self, _auth, mock_api):
old_sha = "8b61c4b41f1b49b271ed3b99657431cf06eeda3e"
new_sha = "3e4b721d60e97147ba0704773cf57cd0d42cbe31"
# #695: eligibility itself now fails closed on stale approval head.
mock_api.side_effect = [
{"login": "merger-bot"}, self._pr("author-bot", sha=new_sha),
self._pr("author-bot", sha=new_sha),
@@ -1256,6 +1267,7 @@ class TestMergePR(unittest.TestCase):
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_merge_blocked_without_visible_approval(self, _auth, mock_api):
# #695: eligibility denies merge when no non-quarantined APPROVED review.
mock_api.side_effect = [
{"login": "merger-bot"}, self._pr("author-bot"),
self._pr("author-bot"),
@@ -1270,12 +1282,21 @@ class TestMergePR(unittest.TestCase):
)
self.assertFalse(r["performed"])
self.assertFalse(r.get("approval_visible"))
self.assertTrue(any("no visible APPROVED review" in x for x in r["reasons"]))
self.assertTrue(
any(
"no non-quarantined APPROVED review" in x
or "no visible APPROVED review" in x
or "merge eligibility denied" in x
for x in r["reasons"]
),
msg=r["reasons"],
)
self._assert_no_merge_call(mock_api)
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_merge_blocked_on_request_changes(self, _auth, mock_api):
# #695: eligibility denies merge on undismissed REQUEST_CHANGES.
mock_api.side_effect = [
{"login": "merger-bot"}, self._pr("author-bot"),
self._pr("author-bot"),
@@ -1425,10 +1446,16 @@ class TestReviewPR(unittest.TestCase):
class TestDeleteBranch(unittest.TestCase):
DELETE_PROFILE = {
"profile_name": "test-deleter",
"allowed_operations": ["gitea.read", "gitea.branch.delete"],
"profile_name": "test-author-deleter",
"role": "author",
"allowed_operations": [
"gitea.read",
"gitea.pr.create",
"gitea.branch.push",
"gitea.branch.delete",
],
"forbidden_operations": [],
"audit_label": "test-deleter",
"audit_label": "test-author-deleter",
}
@patch("mcp_server.get_profile", return_value=DELETE_PROFILE)