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>
96 lines
3.8 KiB
Python
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()
|