"""Issue #309: dedicated reconciler capability to close already-landed PRs. The reconciler path must prove exact ``gitea.pr.close`` capability, must never imply review/approve/request-changes/merge capability, and may close a PR only after full already-landed proof. """ import sys import unittest sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent)) import role_session_router # noqa: E402 import task_capability_map # noqa: E402 from review_proofs import assess_reconciler_close_gate # noqa: E402 PINNED = "0fdc8f582026b72a229d59a172c0a63ac4aaeaf9" OTHER = "a4060c5de00f2b1c9e88f4f6f0f3f9a7b2c1d0e9" class TestReconcilerCapabilityMap(unittest.TestCase): def test_reconcile_close_pr_task_maps_to_pr_close(self): self.assertEqual( task_capability_map.required_permission( "reconcile_close_landed_pr"), "gitea.pr.close", ) self.assertEqual( task_capability_map.required_role("reconcile_close_landed_pr"), "reconciler", ) def test_reconcile_close_issue_task_maps_to_issue_close(self): self.assertEqual( task_capability_map.required_permission( "reconcile_close_landed_issue"), "gitea.issue.close", ) self.assertEqual( task_capability_map.required_role( "reconcile_close_landed_issue"), "reconciler", ) def test_reconciler_tasks_do_not_grant_review_permissions(self): for task in ("reconcile_close_landed_pr", "reconcile_close_landed_issue"): permission = task_capability_map.required_permission(task) self.assertNotIn("review", permission) self.assertNotIn("approve", permission) self.assertNotIn("merge", permission) self.assertNotIn("request_changes", permission) class TestReconcilerRouting(unittest.TestCase): def test_reconciler_task_requires_reconciler_role(self): self.assertEqual( role_session_router.required_role_for_task( "reconcile_close_landed_pr"), "reconciler", ) def test_reconciler_task_blocked_in_author_session(self): result = role_session_router.route_task_session( "reconcile_close_landed_pr", active_profile="prgs-author", active_role_kind="author", allowed_in_current_session=False, ) self.assertEqual(result["route_result"], "wrong_role_stop") self.assertFalse(result["downstream_allowed"]) def test_reconciler_task_allowed_in_matching_session(self): result = role_session_router.route_task_session( "reconcile_close_landed_pr", active_profile="prgs-reconciler", active_role_kind="reconciler", allowed_in_current_session=True, ) self.assertEqual(result["route_result"], "allowed_current_session") self.assertTrue(result["downstream_allowed"]) class TestReconcilerCloseGate(unittest.TestCase): def _gate(self, **overrides): kwargs = { "pr_number": 278, "pr_state": "open", "candidate_head_sha": PINNED, "live_head_sha": PINNED, "target_branch": "master", "target_branch_sha": OTHER, "head_is_ancestor_of_target": True, "close_pr_capability": True, "close_issue_capability": False, "linked_issue_state": "closed", } kwargs.update(overrides) return assess_reconciler_close_gate(**kwargs) def test_already_landed_open_pr_close_allowed(self): result = self._gate() self.assertEqual(result["outcome"], "CLOSE_ALLOWED") self.assertTrue(result["pr_close_allowed"]) def test_not_landed_pr_cannot_be_closed(self): result = self._gate(head_is_ancestor_of_target=False) self.assertEqual(result["outcome"], "NOT_LANDED_CLOSE_BLOCKED") self.assertFalse(result["pr_close_allowed"]) def test_unchecked_ancestry_blocks_close(self): result = self._gate(head_is_ancestor_of_target=None) self.assertEqual(result["outcome"], "GATE_NOT_PROVEN") self.assertFalse(result["pr_close_allowed"]) def test_stale_target_branch_sha_blocks_close(self): result = self._gate(target_branch_sha=None) self.assertEqual(result["outcome"], "GATE_NOT_PROVEN") self.assertFalse(result["pr_close_allowed"]) def test_closed_pr_state_blocks_close(self): result = self._gate(pr_state="closed") self.assertEqual(result["outcome"], "GATE_NOT_PROVEN") self.assertFalse(result["pr_close_allowed"]) def test_changed_head_blocks_close(self): result = self._gate(live_head_sha=OTHER) self.assertEqual(result["outcome"], "GATE_NOT_PROVEN") self.assertFalse(result["pr_close_allowed"]) def test_missing_close_capability_produces_recovery_handoff(self): result = self._gate(close_pr_capability=False) self.assertEqual(result["outcome"], "RECOVERY_HANDOFF_REQUIRED") self.assertFalse(result["pr_close_allowed"]) self.assertTrue(any( "gitea.pr.close" in reason for reason in result["reasons"] )) def test_linked_issue_already_closed_skips_issue_close(self): result = self._gate(linked_issue_state="closed", close_issue_capability=True) self.assertFalse(result["issue_close_allowed"]) def test_linked_open_issue_close_requires_capability(self): allowed = self._gate( linked_issue_state="open", issue_resolved_by_landing=True, close_issue_capability=True, ) self.assertTrue(allowed["issue_close_allowed"]) blocked = self._gate( linked_issue_state="open", issue_resolved_by_landing=True, close_issue_capability=False, ) self.assertFalse(blocked["issue_close_allowed"]) def test_gate_never_allows_review_mutations(self): for result in ( self._gate(), self._gate(head_is_ancestor_of_target=False), self._gate(close_pr_capability=False), ): self.assertFalse(result["review_allowed"]) self.assertFalse(result["approve_allowed"]) self.assertFalse(result["request_changes_allowed"]) self.assertFalse(result["merge_allowed"]) def test_report_fields_listed_for_close(self): result = self._gate() for field in ( "identity/profile", "close capability proof", "PR live state", "candidate head SHA", "target branch SHA", "ancestor proof", "linked issue status", "PR close result", "issue close result", "no review/merge confirmation", ): self.assertIn(field, result["required_report_fields"]) if __name__ == "__main__": unittest.main()