"""Tests for enforced PR review/merge state machine (#290).""" import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) import review_merge_state_machine as rmsm # noqa: E402 FULL_REVIEW_COMPLETION = {state: True for state in rmsm.REVIEW_MERGE_STATES} def _completion_through(state_name: str) -> dict[str, bool]: idx = rmsm.state_index(state_name) return {state: i <= idx for i, state in enumerate(rmsm.REVIEW_MERGE_STATES)} class TestWorkflowBlockers(unittest.TestCase): def test_infra_stop_blocks_all_states(self): result = rmsm.assess_workflow_blockers(infra_stop=True) self.assertTrue(result["block"]) self.assertEqual(result["forbidden_states"], list(rmsm.REVIEW_MERGE_STATES)) def test_infra_stop_forbids_advancement(self): result = rmsm.assess_state_advancement( {}, target_state="INVENTORY", infra_stop=True, ) self.assertTrue(result["block"]) self.assertFalse(result["allowed"]) class TestStateAdvancement(unittest.TestCase): def test_cannot_skip_inventory(self): result = rmsm.assess_state_advancement( {"PRECHECK": True}, target_state="SELECT_PR", ) self.assertTrue(result["block"]) self.assertEqual(result["next_allowed_state"], "INVENTORY") def test_sequential_advance_allowed(self): completion = _completion_through("INVENTORY") result = rmsm.assess_state_advancement( completion, target_state="SELECT_PR", ) self.assertFalse(result["block"]) self.assertTrue(result["allowed"]) class TestApproveAndMergeGates(unittest.TestCase): def test_approve_blocked_without_validate(self): completion = _completion_through("PIN_HEAD_SHA") result = rmsm.can_approve(completion) self.assertTrue(result["block"]) self.assertIn("VALIDATE", ",".join(result["missing_states"])) def test_approve_allowed_when_review_path_complete(self): completion = _completion_through("REVIEW_DECISION") result = rmsm.can_approve(completion) self.assertFalse(result["block"]) self.assertTrue(result["allowed"]) def test_merge_blocked_without_pre_merge_gates(self): completion = _completion_through("APPROVE_OR_REQUEST_CHANGES") result = rmsm.can_merge(completion) self.assertTrue(result["block"]) self.assertTrue(result["missing_pre_merge_gates"]) def test_merge_allowed_with_pre_merge_gates(self): completion = _completion_through("PRE_MERGE_RECHECK") gates = {gate: True for gate in rmsm._PRE_MERGE_REQUIRED_GATES} result = rmsm.can_merge(completion, pre_merge_gates=gates) self.assertFalse(result["block"]) self.assertTrue(result["allowed"]) def test_merge_blocked_when_head_sha_gate_fails(self): completion = _completion_through("PRE_MERGE_RECHECK") gates = {gate: True for gate in rmsm._PRE_MERGE_REQUIRED_GATES} gates["reviewed_head_sha_unchanged"] = False result = rmsm.can_merge(completion, pre_merge_gates=gates) self.assertTrue(result["block"]) self.assertIn("reviewed_head_sha_unchanged", result["missing_pre_merge_gates"]) class TestRecoveryHandoff(unittest.TestCase): def test_blocked_handoff_without_replay_passes(self): report = ( "Blocked recovery handoff.\n" "Restart the full workflow after infra_stop clears.\n" "No approve/merge performed." ) result = rmsm.assess_blocked_recovery_handoff(report) self.assertFalse(result["block"]) def test_blocked_handoff_with_merge_replay_fails(self): report = "Please merge PR #12 now using gitea_merge_pr." result = rmsm.assess_blocked_recovery_handoff(report) self.assertTrue(result["block"]) class TestFinalReportClaims(unittest.TestCase): def test_ready_to_merge_claim_without_gates_fails(self): result = rmsm.assess_final_report_state_claims( "PR is ready to merge after validation.", state_completion=_completion_through("VALIDATE"), ) self.assertTrue(result["block"]) def test_reviewed_claim_without_validate_fails(self): result = rmsm.assess_final_report_state_claims( "PR reviewed and looks good.", state_completion={"PRECHECK": True}, ) self.assertTrue(result["block"]) if __name__ == "__main__": unittest.main()