Files
Gitea-Tools/tests/test_pr_sync_status.py
T
sysadmin 98fbfb3de7 feat(pr-sync): native assess and author update-by-merge lifecycle
Prevent approved PRs from stalling when master advances. Add
gitea_assess_pr_sync_status and gitea_update_pr_branch_by_merge with
head/base pinning, author-only updates, conflict handoff, and approval
invalidation after head changes. Wire task capability map, sequential
controller routing in review-merge workflow, and hermetic AC tests.
2026-07-17 10:42:53 -04:00

339 lines
12 KiB
Python

"""Hermetic tests for PR synchronization / conflict-remediation lifecycle."""
from __future__ import annotations
import unittest
from pr_sync_status import (
ACTION_AUTHOR_CONFLICT_REMEDIATION,
ACTION_BLOCKED,
ACTION_FRESH_REVIEW_REQUIRED,
ACTION_MERGE_NOW,
ACTION_UPDATE_BRANCH_BY_MERGE,
assess_post_update_head_transition,
assess_pr_sync_status,
assess_sequential_queue_step,
assess_update_pr_branch_preflight,
lease_usable_at_head,
merge_approval_usable_at_head,
)
def _sha(prefix: str) -> str:
return (prefix + "0" * 40)[:40]
PR_HEAD = _sha("aaaaaaaa")
BASE_HEAD = _sha("bbbbbbbb")
NEW_HEAD = _sha("cccccccc")
OLD_HEAD = _sha("dddddddd")
def _base_kwargs(**overrides):
data = {
"host": "gitea.prgs.cc",
"org": "Scaled-Tech-Consulting",
"repo": "Gitea-Tools",
"pr_number": 100,
"pr_state": "open",
"source_branch": "fix/issue-100",
"pr_head_sha": PR_HEAD,
"base_head_sha": BASE_HEAD,
"commits_behind": 0,
"mergeable": True,
"has_conflicts": False,
"branch_protection_requires_current_base": False,
"approval_at_current_head": True,
"checks_status": "success",
"active_author_lock": True,
"active_reviewer_lease": False,
"active_merger_lease": False,
}
data.update(overrides)
return data
class TestAssessPrSyncStatus(unittest.TestCase):
def test_approved_current_mergeable_merge_now(self):
result = assess_pr_sync_status(**_base_kwargs())
self.assertEqual(result["recommended_next_action"], ACTION_MERGE_NOW)
self.assertTrue(result["approval_valid_for_merge"])
self.assertFalse(result["stale_approval"])
self.assertEqual(result["pr_head_sha"], PR_HEAD)
self.assertEqual(result["base_head_sha"], BASE_HEAD)
def test_approved_outdated_update_not_required_merge_now(self):
result = assess_pr_sync_status(
**_base_kwargs(
commits_behind=3,
branch_protection_requires_current_base=False,
)
)
self.assertEqual(result["recommended_next_action"], ACTION_MERGE_NOW)
self.assertTrue(result["approval_valid_for_merge"])
self.assertTrue(any("does not require" in r for r in result["reasons"]))
def test_outdated_update_required_no_conflict(self):
result = assess_pr_sync_status(
**_base_kwargs(
commits_behind=2,
branch_protection_requires_current_base=True,
mergeable=True,
has_conflicts=False,
)
)
self.assertEqual(
result["recommended_next_action"], ACTION_UPDATE_BRANCH_BY_MERGE
)
self.assertFalse(result["approval_valid_for_merge"])
self.assertTrue(any("update_branch_by_merge" in r for r in result["reasons"]))
def test_outdated_has_conflicts_author_remediation(self):
result = assess_pr_sync_status(
**_base_kwargs(
commits_behind=5,
branch_protection_requires_current_base=True,
mergeable=False,
has_conflicts=True,
)
)
self.assertEqual(
result["recommended_next_action"], ACTION_AUTHOR_CONFLICT_REMEDIATION
)
self.assertFalse(result["approval_valid_for_merge"])
def test_stale_approval_fresh_review_required(self):
result = assess_pr_sync_status(
**_base_kwargs(
approval_at_current_head=False,
commits_behind=0,
)
)
self.assertEqual(
result["recommended_next_action"], ACTION_FRESH_REVIEW_REQUIRED
)
self.assertTrue(result["stale_approval"])
self.assertFalse(result["approval_valid_for_merge"])
def test_stale_prepared_verdict_cannot_cross_heads(self):
result = assess_pr_sync_status(
**_base_kwargs(
approval_at_current_head=True,
prepared_verdict_head_sha=OLD_HEAD,
)
)
self.assertEqual(
result["recommended_next_action"], ACTION_FRESH_REVIEW_REQUIRED
)
self.assertTrue(result["stale_prepared_verdict"])
self.assertFalse(result["approval_valid_for_merge"])
def test_missing_heads_fail_closed(self):
result = assess_pr_sync_status(
**_base_kwargs(pr_head_sha="short", base_head_sha=None)
)
self.assertEqual(result["recommended_next_action"], ACTION_BLOCKED)
self.assertTrue(any("PR head" in r for r in result["reasons"]))
def test_closed_pr_blocked(self):
result = assess_pr_sync_status(**_base_kwargs(pr_state="closed"))
self.assertEqual(result["recommended_next_action"], ACTION_BLOCKED)
def test_failed_checks_block_merge_now(self):
result = assess_pr_sync_status(**_base_kwargs(checks_status="failure"))
self.assertEqual(result["recommended_next_action"], ACTION_BLOCKED)
def test_stale_approval_with_update_required_routes_update(self):
result = assess_pr_sync_status(
**_base_kwargs(
approval_at_current_head=False,
commits_behind=1,
branch_protection_requires_current_base=True,
mergeable=True,
has_conflicts=False,
)
)
self.assertEqual(
result["recommended_next_action"], ACTION_UPDATE_BRANCH_BY_MERGE
)
class TestUpdatePrBranchPreflight(unittest.TestCase):
def _ok_kwargs(self, **overrides):
data = {
"role_kind": "author",
"expected_pr_head_sha": PR_HEAD,
"live_pr_head_sha": PR_HEAD,
"expected_base_head_sha": BASE_HEAD,
"live_base_head_sha": BASE_HEAD,
"has_conflicts": False,
"mergeable": True,
"style": "merge",
"has_author_lock": True,
"worktree_path": "/Users/x/Development/Gitea-Tools/branches/issue-100",
"worktree_owns_source_branch": True,
"control_checkout_clean": True,
"master_parity_ok": True,
"force_push": False,
"rebase": False,
}
data.update(overrides)
return data
def test_author_allowed_when_preflight_clean(self):
result = assess_update_pr_branch_preflight(**self._ok_kwargs())
self.assertTrue(result["mutation_allowed"])
self.assertFalse(result["head_race"])
self.assertFalse(result["base_race"])
def test_head_race_fail_closed(self):
result = assess_update_pr_branch_preflight(
**self._ok_kwargs(live_pr_head_sha=NEW_HEAD)
)
self.assertFalse(result["mutation_allowed"])
self.assertTrue(result["head_race"])
self.assertTrue(any("PR head race" in r for r in result["reasons"]))
def test_base_race_fail_closed(self):
result = assess_update_pr_branch_preflight(
**self._ok_kwargs(live_base_head_sha=NEW_HEAD)
)
self.assertFalse(result["mutation_allowed"])
self.assertTrue(result["base_race"])
self.assertTrue(any("base head race" in r for r in result["reasons"]))
def test_conflicts_structured_handoff_no_mutation(self):
result = assess_update_pr_branch_preflight(
**self._ok_kwargs(has_conflicts=True, mergeable=False)
)
self.assertFalse(result["mutation_allowed"])
self.assertTrue(result["author_remediation_handoff"])
self.assertTrue(any("conflicts" in r.lower() for r in result["reasons"]))
def test_reviewer_denied(self):
result = assess_update_pr_branch_preflight(
**self._ok_kwargs(role_kind="reviewer")
)
self.assertFalse(result["mutation_allowed"])
self.assertTrue(any("author-only" in r for r in result["reasons"]))
def test_merger_denied(self):
result = assess_update_pr_branch_preflight(
**self._ok_kwargs(role_kind="merger")
)
self.assertFalse(result["mutation_allowed"])
self.assertTrue(any("author-only" in r for r in result["reasons"]))
def test_no_force_push(self):
result = assess_update_pr_branch_preflight(
**self._ok_kwargs(force_push=True)
)
self.assertFalse(result["mutation_allowed"])
self.assertTrue(any("force-push" in r for r in result["reasons"]))
def test_no_rebase(self):
result = assess_update_pr_branch_preflight(
**self._ok_kwargs(style="rebase", rebase=True)
)
self.assertFalse(result["mutation_allowed"])
self.assertTrue(any("rebase" in r for r in result["reasons"]))
def test_missing_lock_fail_closed(self):
result = assess_update_pr_branch_preflight(
**self._ok_kwargs(has_author_lock=False)
)
self.assertFalse(result["mutation_allowed"])
def test_worktree_not_under_branches(self):
result = assess_update_pr_branch_preflight(
**self._ok_kwargs(worktree_path="/tmp/not-a-branches-path")
)
self.assertFalse(result["mutation_allowed"])
self.assertTrue(any("branches/" in r for r in result["reasons"]))
class TestPostUpdateHeadTransition(unittest.TestCase):
def test_update_invalidates_approval_and_requires_fresh_review(self):
result = assess_post_update_head_transition(
former_pr_head_sha=PR_HEAD,
new_pr_head_sha=NEW_HEAD,
former_approval_head_sha=PR_HEAD,
former_reviewer_lease_head_sha=PR_HEAD,
former_merger_lease_head_sha=PR_HEAD,
prepared_verdict_head_sha=PR_HEAD,
)
self.assertTrue(result["head_changed"])
self.assertTrue(result["approval_invalidated"])
self.assertTrue(result["reviewer_lease_superseded"])
self.assertTrue(result["merger_lease_superseded"])
self.assertTrue(result["prepared_verdict_invalidated"])
self.assertEqual(
result["recommended_next_action"], ACTION_FRESH_REVIEW_REQUIRED
)
def test_same_head_unexpected(self):
result = assess_post_update_head_transition(
former_pr_head_sha=PR_HEAD,
new_pr_head_sha=PR_HEAD,
)
self.assertFalse(result["head_changed"])
self.assertEqual(result["recommended_next_action"], ACTION_BLOCKED)
class TestStaleArtifacts(unittest.TestCase):
def test_stale_approval_cannot_authorize_merge(self):
result = merge_approval_usable_at_head(
current_head_sha=NEW_HEAD,
approved_head_sha=PR_HEAD,
)
self.assertFalse(result["approval_usable"])
def test_fresh_approval_usable(self):
result = merge_approval_usable_at_head(
current_head_sha=NEW_HEAD,
approved_head_sha=NEW_HEAD,
)
self.assertTrue(result["approval_usable"])
def test_stale_reviewer_lease_cannot_cross_heads(self):
result = lease_usable_at_head(
lease_kind="reviewer",
lease_head_sha=PR_HEAD,
current_head_sha=NEW_HEAD,
)
self.assertFalse(result["lease_usable"])
def test_stale_merger_lease_cannot_cross_heads(self):
result = lease_usable_at_head(
lease_kind="merger",
lease_head_sha=PR_HEAD,
current_head_sha=NEW_HEAD,
)
self.assertFalse(result["lease_usable"])
class TestSequentialQueue(unittest.TestCase):
def test_reassess_after_merge_and_master_refresh(self):
result = assess_sequential_queue_step(
just_merged=True,
master_refreshed=True,
next_pr_number=101,
)
self.assertTrue(result["reassess_allowed"])
self.assertTrue(result["process_next"])
self.assertTrue(result["post_merge_cleanup_handoff"])
self.assertEqual(result["next_pr_number"], 101)
def test_block_reassess_without_master_refresh(self):
result = assess_sequential_queue_step(
just_merged=True,
master_refreshed=False,
next_pr_number=101,
)
self.assertFalse(result["reassess_allowed"])
self.assertTrue(any("master must be refreshed" in r for r in result["reasons"]))
if __name__ == "__main__":
unittest.main()