Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/agent_merge_queue/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,12 +818,15 @@ def classify(
elif self.queue_state != "queued":
blocked.append("queue authorization was revoked")

required_checks_passed = bool(config.required_checks)
for name in config.required_checks:
status = self.checks.get(name)
if status == "failed":
blocked.append(f"{name} failed")
required_checks_passed = False
elif status != "passed":
waiting.append(f"{name} is not complete")
required_checks_passed = False

for verdict in self.review_verdicts:
if verdict.state == "blocked":
Expand All @@ -833,10 +836,14 @@ def classify(

if self.mergeable == "CONFLICTING" or self.merge_state == "DIRTY":
blocked.append("pull request conflicts with main")
elif self.merge_state == "BEHIND":
blocked.append("GitHub reports the pull request head ref is out of date")
elif self.merge_state in {"BLOCKED", "DRAFT"}:
blocked.append(
f"GitHub reports the pull request merge state as {self.merge_state}"
)
elif self.merge_state == "UNSTABLE" and not required_checks_passed:
waiting.append("GitHub reports non-passing commit status")
elif self.merge_state == "UNKNOWN" or self.mergeable != "MERGEABLE":
waiting.append("GitHub is still computing mergeability")

Expand Down
44 changes: 36 additions & 8 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1717,20 +1717,48 @@ def test_review_verdicts_are_classified_generically(self) -> None:
self.assertEqual(value.state, "blocked")
self.assertIn("one finding", value.reasons)

def test_github_blocked_state_fails_closed_but_mergeable_states_do_not(
self,
) -> None:
def test_clean_merge_state_is_ready_when_other_policy_passes(self) -> None:
clean = entry(1)
clean.merge_state = "CLEAN"
clean.classify(CONFIG)
self.assertEqual(clean.state, "ready")
self.assertEqual(clean.reasons, [])

def test_has_hooks_merge_state_is_ready_when_other_policy_passes(self) -> None:
has_hooks = entry(1)
has_hooks.merge_state = "HAS_HOOKS"
has_hooks.classify(CONFIG)
self.assertEqual(has_hooks.state, "ready")
self.assertEqual(has_hooks.reasons, [])

def test_github_blocked_state_fails_closed(self) -> None:
blocked = entry(1)
blocked.merge_state = "BLOCKED"
blocked.classify(CONFIG)
self.assertEqual(blocked.state, "blocked")
self.assertIn("merge state as BLOCKED", blocked.reasons[-1])

for index, state in enumerate(("BEHIND", "HAS_HOOKS", "UNSTABLE"), start=2):
value = entry(index)
value.merge_state = state
value.classify(CONFIG)
self.assertEqual(value.state, "ready", state)
def test_behind_merge_state_is_blocked(self) -> None:
behind = entry(2)
behind.merge_state = "BEHIND"
behind.classify(CONFIG)
self.assertEqual(behind.state, "blocked")
self.assertIn("head ref is out of date", behind.reasons[-1])

def test_unstable_merge_state_waits_without_required_check_proof(self) -> None:
unstable = entry(3)
unstable.merge_state = "UNSTABLE"
unstable.checks = {}
unstable.classify(CONFIG)
self.assertEqual(unstable.state, "waiting")
self.assertIn("non-passing commit status", unstable.reasons[-1])

def test_unstable_merge_state_allows_exact_required_check_success(self) -> None:
unstable = entry(4)
unstable.merge_state = "UNSTABLE"
unstable.classify(CONFIG)
self.assertEqual(unstable.state, "ready")
self.assertEqual(unstable.reasons, [])

def test_overlap_groups_are_connected_components(self) -> None:
groups = overlap_groups(
Expand Down