Merge pull request 'feat: expose explicit adoption proof in gitea_lock_issue response (Closes #477)' (#481) from feat/issue-477-lock-adoption-proof into master

This commit was merged in pull request #481.
This commit is contained in:
2026-07-08 02:31:47 -05:00
5 changed files with 263 additions and 0 deletions
+89
View File
@@ -11,6 +11,7 @@ from issue_lock_adoption import ( # noqa: E402
NO_MATCH,
assess_own_branch_adoption,
build_adoption_proof,
build_non_adoption_lock_proof,
)
REQ = "feat/issue-420-server-code-parity"
@@ -134,5 +135,93 @@ class TestBuildAdoptionProof(unittest.TestCase):
self.assertTrue(proof["no_competing_live_lock_proof"])
class TestExplicitAdoptionProofFields(unittest.TestCase):
"""#477: explicit, citable adoption-proof fields for all outcomes."""
def _proof(self, assessment, branch):
return build_adoption_proof(
issue_number=420,
branch_name=branch,
assessment=assessment,
open_pr_checked=True,
competing_lock_checked=True,
lock_file_path="/tmp/example-lock.json",
lock_file_status="written",
)
def test_adopt_proof_exposes_explicit_fields(self):
assessment = assess_own_branch_adoption(
issue_number=420,
requested_branch=REQ,
existing_branches=[{"name": REQ, "commit_sha": "934688a"}],
)
proof = self._proof(assessment, REQ)
self.assertEqual(proof["adoption_decision"], "ADOPT")
self.assertTrue(proof["adopted"])
self.assertEqual(proof["adopted_branch"], REQ)
self.assertEqual(proof["adopted_branch_head"], "934688a")
self.assertEqual(proof["competing_branch_check"]["result"], "clear")
self.assertEqual(proof["competing_branch_check"]["competing_branches"], [])
self.assertIn("gitea_create_pr", proof["safe_next_action"])
self.assertIn("exactly matches", proof["matcher_summary"])
def test_block_proof_reports_competing_and_does_not_claim_adoption(self):
assessment = assess_own_branch_adoption(
issue_number=420,
requested_branch=REQ,
existing_branches=[{"name": "feat/issue-420-rogue"}],
)
proof = self._proof(assessment, REQ)
self.assertEqual(proof["adoption_decision"], "BLOCK_COMPETING")
self.assertFalse(proof["adopted"])
self.assertIsNone(proof["adopted_branch"])
self.assertIsNone(proof["adopted_branch_head"])
self.assertEqual(proof["competing_branch_check"]["result"], "blocked")
self.assertIn(
"feat/issue-420-rogue",
proof["competing_branch_check"]["competing_branches"],
)
self.assertIn("fail closed", proof["safe_next_action"])
def test_no_match_proof_does_not_claim_adoption(self):
assessment = assess_own_branch_adoption(
issue_number=420,
requested_branch=REQ,
existing_branches=[{"name": "feat/issue-999-unrelated"}],
)
proof = self._proof(assessment, REQ)
self.assertEqual(proof["adoption_decision"], "NO_MATCH")
self.assertFalse(proof["adopted"])
self.assertIsNone(proof["adopted_branch"])
self.assertEqual(proof["competing_branch_check"]["result"], "clear")
def test_substring_collision_stays_boundary_safe(self):
# issue-42 must not adopt/claim against an issue-420 branch (#440/#477).
own = "feat/issue-42-widget"
assessment = assess_own_branch_adoption(
issue_number=42,
requested_branch=own,
existing_branches=[
{"name": own, "commit_sha": "abc1234"},
{"name": "feat/issue-420-server-code-parity"},
],
)
proof = self._proof(assessment, own)
self.assertEqual(proof["adoption_decision"], "ADOPT")
self.assertEqual(proof["adopted_branch"], own)
self.assertEqual(proof["competing_branch_check"]["competing_branches"], [])
def test_non_adoption_lock_proof_is_adoption_free(self):
proof = build_non_adoption_lock_proof(
issue_number=196, branch_name="feat/issue-196-mutations"
)
self.assertEqual(proof["adoption_decision"], "NO_MATCH")
self.assertFalse(proof["adopted"])
self.assertIsNone(proof["adopted_branch"])
self.assertIsNone(proof["adopted_branch_head"])
self.assertEqual(proof["competing_branch_check"]["result"], "clear")
self.assertIn("no adoption", proof["safe_next_action"].lower())
if __name__ == "__main__":
unittest.main()
+39
View File
@@ -3460,6 +3460,45 @@ class TestIssueLocking(unittest.TestCase):
self.assertIn("adoption", res)
self.assertEqual(res["adoption"]["branch_head_commit"], "abc123")
@patch(
"mcp_server.issue_lock_worktree.read_worktree_git_state",
return_value=_clean_master_git_state_for_lock(),
)
@patch("mcp_server.api_get_all")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_lock_issue_adoption_response_has_explicit_proof(self, _auth, mock_api, _git_state):
# #477 AC1: the live lock response must carry citable adoption proof.
branch = "feat/issue-196-mutations"
self.mock_dup_fetcher.return_value = ([], [branch], {"status": "not_claimed"})
mock_api.return_value = [{"name": branch, "commit": {"id": "abc123"}}]
res = gitea_lock_issue(issue_number=196, branch_name=branch, remote="prgs")
proof = res["adoption"]
self.assertEqual(proof["adoption_decision"], "ADOPT")
self.assertTrue(proof["adopted"])
self.assertEqual(proof["adopted_branch"], branch)
self.assertEqual(proof["adopted_branch_head"], "abc123")
self.assertEqual(proof["competing_branch_check"]["result"], "clear")
self.assertIn("gitea_create_pr", proof["safe_next_action"])
self.assertIn("196", proof["matcher_summary"])
@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=[])
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_lock_issue_no_match_response_does_not_claim_adoption(self, _auth, _api, _git_state):
# #477 AC2/AC3: a normal (NO_MATCH) lock must carry adoption-free proof
# and must NOT expose an ``adoption`` block.
res = gitea_lock_issue(issue_number=196, branch_name="feat/issue-196-mutations", remote="prgs")
self.assertTrue(res["success"])
self.assertNotIn("adoption", res)
check = res["adoption_check"]
self.assertEqual(check["adoption_decision"], "NO_MATCH")
self.assertFalse(check["adopted"])
self.assertIsNone(check["adopted_branch"])
self.assertEqual(check["competing_branch_check"]["result"], "clear")
@patch(
"mcp_server.issue_lock_worktree.read_worktree_git_state",
return_value=_clean_master_git_state_for_lock(),