fix: conflict-fix lease lifecycle chain termination and TTL handling (Closes #842)
This commit is contained in:
@@ -19,6 +19,7 @@ from pr_work_lease import ( # noqa: E402
|
||||
assess_reviewer_mutation_blocked,
|
||||
assess_reviewer_stale_head_final_report,
|
||||
format_conflict_fix_lease_body,
|
||||
find_active_conflict_fix_lease,
|
||||
parse_conflict_fix_lease_comment,
|
||||
parse_reviewer_lease_comment,
|
||||
)
|
||||
@@ -203,5 +204,157 @@ class TestFormatLease(unittest.TestCase):
|
||||
self.assertEqual(parsed["pr_number"], 376)
|
||||
|
||||
|
||||
class TestConflictFixLeaseLifecycle(unittest.TestCase):
|
||||
def test_claim_followed_by_matching_release(self):
|
||||
claim_body = _conflict_fix_body(phase="claimed", worktree="branches/fix-376")
|
||||
expires = (NOW + timedelta(minutes=60)).isoformat().replace("+00:00", "Z")
|
||||
release_body = "\n".join([
|
||||
CONFLICT_FIX_LEASE_MARKER,
|
||||
"pr: #376",
|
||||
"branch: feat/fix-376",
|
||||
"worktree: branches/fix-376",
|
||||
"profile: prgs-author",
|
||||
"phase: released",
|
||||
f"head_before: {HEAD_A}",
|
||||
f"head_after: {HEAD_B}",
|
||||
f"expires_at: {expires}",
|
||||
])
|
||||
comments = [{"body": claim_body}, {"body": release_body}]
|
||||
lease = find_active_conflict_fix_lease(comments, pr_number=376, now=NOW)
|
||||
self.assertIsNone(lease)
|
||||
|
||||
def test_expired_claim_without_release(self):
|
||||
past_expires = (NOW - timedelta(minutes=10)).isoformat().replace("+00:00", "Z")
|
||||
claim_body = "\n".join([
|
||||
CONFLICT_FIX_LEASE_MARKER,
|
||||
"pr: #376",
|
||||
"phase: claimed",
|
||||
f"head_before: {HEAD_A}",
|
||||
f"expires_at: {past_expires}",
|
||||
"profile: prgs-author",
|
||||
])
|
||||
comments = [{"body": claim_body}]
|
||||
lease = find_active_conflict_fix_lease(comments, pr_number=376, now=NOW)
|
||||
self.assertIsNone(lease)
|
||||
|
||||
def test_mismatched_release_different_head(self):
|
||||
claim_body = _conflict_fix_body(phase="claimed", worktree="branches/fix-376")
|
||||
expires = (NOW + timedelta(minutes=60)).isoformat().replace("+00:00", "Z")
|
||||
release_body = "\n".join([
|
||||
CONFLICT_FIX_LEASE_MARKER,
|
||||
"pr: #376",
|
||||
"profile: prgs-author",
|
||||
"phase: released",
|
||||
f"head_before: {HEAD_B}",
|
||||
f"expires_at: {expires}",
|
||||
])
|
||||
comments = [{"body": claim_body}, {"body": release_body}]
|
||||
lease = find_active_conflict_fix_lease(comments, pr_number=376, now=NOW)
|
||||
self.assertIsNotNone(lease)
|
||||
self.assertEqual(lease["phase"], "claimed")
|
||||
|
||||
def test_mismatched_release_different_branch(self):
|
||||
claim_body = "\n".join([
|
||||
CONFLICT_FIX_LEASE_MARKER,
|
||||
"pr: #376",
|
||||
"branch: feat/branch-A",
|
||||
"phase: claimed",
|
||||
f"head_before: {HEAD_A}",
|
||||
f"expires_at: {(NOW + timedelta(minutes=60)).isoformat().replace('+00:00', 'Z')}",
|
||||
"profile: prgs-author",
|
||||
])
|
||||
release_body = "\n".join([
|
||||
CONFLICT_FIX_LEASE_MARKER,
|
||||
"pr: #376",
|
||||
"branch: feat/branch-B",
|
||||
"phase: released",
|
||||
f"head_before: {HEAD_A}",
|
||||
f"expires_at: {(NOW + timedelta(minutes=60)).isoformat().replace('+00:00', 'Z')}",
|
||||
"profile: prgs-author",
|
||||
])
|
||||
comments = [{"body": claim_body}, {"body": release_body}]
|
||||
lease = find_active_conflict_fix_lease(comments, pr_number=376, now=NOW)
|
||||
self.assertIsNotNone(lease)
|
||||
self.assertEqual(lease["phase"], "claimed")
|
||||
|
||||
def test_release_followed_by_newer_claim(self):
|
||||
claim_1 = _conflict_fix_body(phase="claimed", worktree="branches/fix-376")
|
||||
expires = (NOW + timedelta(minutes=60)).isoformat().replace("+00:00", "Z")
|
||||
release_1 = "\n".join([
|
||||
CONFLICT_FIX_LEASE_MARKER,
|
||||
"pr: #376",
|
||||
"profile: prgs-author",
|
||||
"phase: released",
|
||||
f"head_before: {HEAD_A}",
|
||||
f"head_after: {HEAD_B}",
|
||||
f"expires_at: {expires}",
|
||||
])
|
||||
claim_2 = "\n".join([
|
||||
CONFLICT_FIX_LEASE_MARKER,
|
||||
"pr: #376",
|
||||
"profile: prgs-author",
|
||||
"phase: claimed",
|
||||
f"head_before: {HEAD_B}",
|
||||
f"expires_at: {expires}",
|
||||
])
|
||||
comments = [{"body": claim_1}, {"body": release_1}, {"body": claim_2}]
|
||||
lease = find_active_conflict_fix_lease(comments, pr_number=376, now=NOW)
|
||||
self.assertIsNotNone(lease)
|
||||
self.assertEqual(lease["head_before"], HEAD_B)
|
||||
|
||||
def test_malformed_or_ambiguous_markers(self):
|
||||
malformed_release = "\n".join([
|
||||
CONFLICT_FIX_LEASE_MARKER,
|
||||
"pr: #376",
|
||||
"phase: released",
|
||||
# missing head_before and profile
|
||||
])
|
||||
claim_body = _conflict_fix_body(phase="claimed")
|
||||
comments = [{"body": claim_body}, {"body": malformed_release}]
|
||||
lease = find_active_conflict_fix_lease(comments, pr_number=376, now=NOW)
|
||||
self.assertIsNotNone(lease)
|
||||
|
||||
def test_pr818_historical_sequence(self):
|
||||
comment_14696 = "\n".join([
|
||||
"<!-- mcp-conflict-fix-lease:v1 -->",
|
||||
"pr: #818",
|
||||
"branch: feat/issue-638-webui-app-shell-phase1",
|
||||
"worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/issue-638-webui-app-shell-phase1",
|
||||
"profile: prgs-author",
|
||||
"session_id: unknown",
|
||||
"phase: claimed",
|
||||
"head_before: 08061b7b8aebdd099a37d1abf5dafcf38e4fd3fb",
|
||||
"expires_at: 2026-07-23T07:12:13Z",
|
||||
"reviewer_active: no",
|
||||
])
|
||||
comment_14730 = "\n".join([
|
||||
"<!-- mcp-conflict-fix-lease:v1 -->",
|
||||
"pr: #818",
|
||||
"branch: feat/issue-638-webui-app-shell-phase1",
|
||||
"worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/issue-638-webui-app-shell-phase1",
|
||||
"profile: prgs-author",
|
||||
"session_id: prgs-author-61241-e5129c60",
|
||||
"phase: released",
|
||||
"head_before: 08061b7b8aebdd099a37d1abf5dafcf38e4fd3fb",
|
||||
"head_after: 64b6eb5d5402663098de5ded3b0617cc3b3df98f",
|
||||
"expires_at: 2026-07-23T06:05:00Z",
|
||||
"reviewer_active: no",
|
||||
])
|
||||
comments = [{"body": comment_14696}, {"body": comment_14730}]
|
||||
check_now = datetime(2026, 7, 23, 6, 30, tzinfo=timezone.utc)
|
||||
lease = find_active_conflict_fix_lease(comments, pr_number=818, now=check_now)
|
||||
self.assertIsNone(lease)
|
||||
|
||||
reviewer_gate = assess_reviewer_mutation_blocked(
|
||||
pr_number=818,
|
||||
comments=comments,
|
||||
reviewed_head_sha="64b6eb5d5402663098de5ded3b0617cc3b3df98f",
|
||||
live_head_sha="64b6eb5d5402663098de5ded3b0617cc3b3df98f",
|
||||
mutation="approve",
|
||||
now=check_now,
|
||||
)
|
||||
self.assertTrue(reviewer_gate["mutation_allowed"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user