Files
bench/tests/test_pr_conflicts.py
istosandClaude Fable 5 43e4319817 Surface PR merge conflicts on the review card
The poller now reads GitHub's mergeable field alongside reviews and
checks. A CONFLICTING PR drops any approved-green verdict (as
changes-needed-by-you, not a CI failure), wears an alarm-coloured
conflicts chip in the card's footer row, and narrates the flip in the
ticker. GitHub computes mergeability lazily, so UNKNOWN keeps the
previous reading instead of flapping the chip.

The poll fold is now a pure function (_fold), so the verdict logic is
testable without gh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 07:57:19 +02:00

96 lines
3.8 KiB
Python

"""Conflicted PRs become card state: the poller folds GitHub's
mergeable field into the PR snapshot, a conflict drops any
approved-green verdict as changes-needed-by-you (not a CI failure),
and GitHub's lazily-computed UNKNOWN keeps the previous reading so the
chip never flaps. The snapshot's `conflicts` key must reach the UI."""
import sys
import unittest
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO / "manager" / "core"))
import github # noqa: E402
def payload(**overrides):
"""A gh pr-view JSON payload with the fields _fold reads."""
data = {"reviews": [], "reviewRequests": [], "statusCheckRollup": []}
data.update(overrides)
return data
APPROVED = [{"state": "APPROVED"}]
class ConflictFolding(unittest.TestCase):
def test_conflict_drops_green_even_when_approved(self):
entry = github._fold(payload(reviews=APPROVED,
mergeable="CONFLICTING"), {})
self.assertTrue(entry["conflicts"])
self.assertEqual(entry["verdict"], "red")
self.assertIn("conflicts with main", entry["detail"])
def test_conflict_is_not_a_ci_failure(self):
entry = github._fold(payload(mergeable="CONFLICTING"), {})
self.assertIsNone(entry["ci"])
self.assertEqual(entry["verdict"], "red")
def test_mergeable_approved_pr_stays_green(self):
entry = github._fold(payload(reviews=APPROVED,
mergeable="MERGEABLE"), {})
self.assertFalse(entry["conflicts"])
self.assertEqual(entry["verdict"], "green")
def test_unknown_keeps_the_previous_reading_both_ways(self):
# GitHub computes mergeability lazily after a push: UNKNOWN means
# "not yet", never "fine" — the chip must not flap.
still = github._fold(payload(mergeable="UNKNOWN"),
{"conflicts": True})
self.assertTrue(still["conflicts"])
self.assertEqual(still["verdict"], "red")
clean = github._fold(payload(reviews=APPROVED, mergeable="UNKNOWN"),
{"conflicts": False})
self.assertFalse(clean["conflicts"])
self.assertEqual(clean["verdict"], "green")
def test_unknown_on_first_sight_alarms_nobody(self):
entry = github._fold(payload(mergeable="UNKNOWN"), {})
self.assertIsNone(entry["conflicts"])
self.assertEqual(entry["verdict"], "pending")
self.assertNotIn("conflicts", entry["detail"])
def test_resolution_lets_green_return(self):
entry = github._fold(payload(reviews=APPROVED, mergeable="MERGEABLE"),
{"conflicts": True, "verdict": "red"})
self.assertFalse(entry["conflicts"])
self.assertEqual(entry["verdict"], "green")
class SnapshotReachesTheUI(unittest.TestCase):
def test_public_state_carries_conflicts(self):
github.PR_STATE["x.md"] = {"verdict": "red", "ci": None,
"copilot": None, "conflicts": True,
"detail": "conflicts with main",
"url": "u", "ts": 1}
try:
self.assertTrue(github.public_state()["x.md"]["conflicts"])
finally:
github.PR_STATE.pop("x.md", None)
def test_the_card_wears_an_alarm_coloured_chip(self):
html = (REPO / "manager" / "core" / "board.html").read_text(
encoding="utf-8")
self.assertIn("prState.conflicts", html)
self.assertIn("label: 'conflicts', glyph: '✕', cls: 'bad'", html)
def test_poller_asks_github_for_mergeable(self):
source = (REPO / "manager" / "core" / "github.py").read_text(
encoding="utf-8")
self.assertIn("statusCheckRollup,state,mergeable", source)
if __name__ == "__main__":
unittest.main()