feat: surface explicit adoption proof in gitea_lock_issue response (Closes #477)
When gitea_lock_issue adopts an existing own branch, the live tool response now carries explicit, citable adoption-proof fields so #473-style recovery sessions can quote the lock output directly instead of inferring adoption from separate offline checks. - issue_lock_adoption.py: add DECISION_LABELS + decision_label()/ safe_next_action() helpers; extend build_adoption_proof() with adoption_decision, adopted, adopted_branch, adopted_branch_head, matcher_summary (boundary-safe reason), competing_branch_check, and safe_next_action for all outcomes; add build_non_adoption_lock_proof() for adoption-free NO_MATCH metadata. - gitea_mcp_server.py: attach adoption-free adoption_check block to normal (non-adopt) lock responses so they cannot be misread as claiming adoption. - docs/llm-workflow-runbooks.md: document the adoption/adoption_check response blocks and instruct recovery reports to cite them directly. - tests: explicit-field proof for ADOPT/BLOCK_COMPETING/NO_MATCH, substring-collision boundary safety (issue-42 vs issue-420), non-adoption proof, and MCP-level live-response assertions. BLOCK_COMPETING lock attempts still fail closed (raise); no raw API fallback, branch deletion, force-push, or manual lock seeding introduced. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -3325,6 +3325,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(),
|
||||
|
||||
Reference in New Issue
Block a user