"""Tests for controller issue-acceptance gate (#500).""" import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) import issue_acceptance_gate # noqa: E402 from final_report_validator import assess_final_report_validator # noqa: E402 def _accepted_comment(**overrides): lines = [ "## Controller Issue Acceptance", "", "STATE:", "accepted", "", "WHO_IS_NEXT:", "controller", "", "NEXT_ACTION:", "Close the tracker follow-up after verification.", "", "NEXT_PROMPT:", "Verify deployment checklist for issue #500 and post acceptance.", "", "ISSUE:", "#500", "", "MERGED_PR:", "#503", "", "MERGE_COMMIT:", "0fdc8f582026b72a229d59a172c0a63ac4aaeaf9", "", "ACCEPTANCE_CRITERIA_CHECKED:", "- [x] documentation added", "- [x] validator tests added", "", "VALIDATION_REVIEWED:", "pytest tests/test_issue_acceptance_gate.py -q", "", "CONTROLLER_DECISION:", "accepted", "", "WHY:", "All acceptance criteria satisfied with proof.", "", "MISSING_WORK:", "none", "", "FOLLOW_UP_ISSUES:", "none", "", "BLOCKERS:", "none", "", "LAST_UPDATED_BY:", "jcwalker3 / prgs-controller / 2026-07-08", ] text = "\n".join(lines) for key, value in overrides.items(): text = text.replace(f"{key}:\n", f"{key}:\n{value}\n", 1) return text def _rejection_comment(state="needs-tests"): text = _accepted_comment() text = text.replace("STATE:\naccepted", f"STATE:\n{state}") text = text.replace( "CONTROLLER_DECISION:\naccepted", "CONTROLLER_DECISION:\nrejected", ) text = text.replace( "ACCEPTANCE_CRITERIA_CHECKED:\n- [x] documentation added\n- [x] validator tests added", "ACCEPTANCE_CRITERIA_CHECKED:\n- [ ] regression tests for rejection paths", ) text = text.replace( "MISSING_WORK:\nnone", "MISSING_WORK:\nAdd regression tests for controller rejection paths.", ) text = text.replace( "NEXT_PROMPT:\nVerify deployment checklist for issue #500 and post acceptance.", "NEXT_PROMPT:\nImplement the missing tests for issue #500 and reopen the PR.", ) return text class TestControllerAcceptanceComment(unittest.TestCase): def test_accepted_comment_valid(self): result = issue_acceptance_gate.validate_controller_acceptance_comment( _accepted_comment() ) self.assertTrue(result["valid"], result["reasons"]) def test_missing_who_is_next_rejected(self): text = _accepted_comment().replace("WHO_IS_NEXT:\ncontroller\n", "WHO_IS_NEXT:\n\n") result = issue_acceptance_gate.validate_controller_acceptance_comment(text) self.assertFalse(result["valid"]) self.assertTrue(any("WHO_IS_NEXT" in r for r in result["reasons"])) def test_missing_next_prompt_on_rejection(self): text = _rejection_comment() text = text.replace( "NEXT_PROMPT:\nImplement the missing tests for issue #500 and reopen the PR.\n", "NEXT_PROMPT:\n\n", ) result = issue_acceptance_gate.validate_controller_acceptance_comment(text) self.assertFalse(result["valid"]) self.assertTrue(any("NEXT_PROMPT" in r for r in result["reasons"])) def test_vague_merge_only_completion_detected(self): text = "PR merged. Issue complete." self.assertTrue(issue_acceptance_gate.claims_merge_only_issue_complete(text)) def test_rejection_paths_require_missing_work(self): for state in ( "more-work-required", "needs-tests", "needs-docs", "needs-feature-enhancement", "needs-follow-up-issue", ): text = _rejection_comment(state=state).replace( "MISSING_WORK:\nAdd regression tests for controller rejection paths.\n", "MISSING_WORK:\n\n", ) result = issue_acceptance_gate.validate_controller_acceptance_comment(text) self.assertFalse(result["valid"], state) self.assertTrue(any("MISSING_WORK" in r for r in result["reasons"])) def test_accepted_requires_checked_criteria(self): text = _accepted_comment().replace( "ACCEPTANCE_CRITERIA_CHECKED:\n- [x] documentation added\n- [x] validator tests added\n", "ACCEPTANCE_CRITERIA_CHECKED:\n- [ ] documentation added\n", ) result = issue_acceptance_gate.validate_controller_acceptance_comment(text) self.assertFalse(result["valid"]) self.assertTrue(any("checked acceptance criterion" in r for r in result["reasons"])) class TestFinalReportIntegration(unittest.TestCase): def test_merge_only_complete_blocks_work_issue_report(self): report = ( "## Controller Handoff\n\n" "- Task: work issue #500\n" "- Merge result: merged\n" "- Current status: PR merged; issue complete\n" ) result = assess_final_report_validator(report, "work_issue") self.assertTrue( any( f["rule_id"] == "shared.issue_acceptance_gate" for f in result["findings"] ), result["findings"], ) def test_pending_acceptance_allowed(self): report = ( "## Controller Handoff\n\n" "- Task: work issue #500\n" "- Merge result: merged\n" "- Current status: controller acceptance pending\n" ) result = assess_final_report_validator(report, "work_issue") self.assertFalse( any( f["rule_id"] == "shared.issue_acceptance_gate" for f in result["findings"] ), result["findings"], ) def test_accepted_block_allows_complete_claim(self): report = _accepted_comment() + "\n\nIssue is complete." gate = issue_acceptance_gate.validate_final_report_issue_acceptance(report) self.assertTrue(gate["valid"], gate["reasons"]) if __name__ == "__main__": unittest.main()