Add issue_acceptance_gate validation for Controller Issue Acceptance comments, final-report rules blocking merge-only issue completion claims, documentation/templates, and regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
183 lines
6.3 KiB
Python
183 lines
6.3 KiB
Python
"""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() |