Merge pull request 'feat: require canonical next-action state comments (Closes #495)' (#499) from feat/issue-495-canonical-next-action-comments into master

This commit was merged in pull request #499.
This commit is contained in:
2026-07-09 08:02:14 -05:00
8 changed files with 754 additions and 1 deletions
+143
View File
@@ -0,0 +1,143 @@
"""Tests for canonical state comment validation (#495)."""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from canonical_state_comments import ( # noqa: E402
validate_canonical_state_comment,
validate_final_report_state_update,
)
GOOD_ISSUE_STATE = """## Canonical Issue State
STATE:
ready-for-author
WHO_IS_NEXT:
author
NEXT_ACTION:
Implement the focused validator and open a PR.
NEXT_PROMPT:
Find issue #495, load the work-issue workflow, implement the validator, and open a PR.
WHAT_HAPPENED:
Controller selected the canonical tracking issue.
WHY:
The duplicate issue points here as canonical.
RELATED_PRS:
none
BRANCH:
none
HEAD_SHA:
none
VALIDATION:
none
BLOCKERS:
none
"""
class TestCanonicalStateComments(unittest.TestCase):
def test_good_issue_state_validates(self):
result = validate_canonical_state_comment(GOOD_ISSUE_STATE)
self.assertTrue(result["valid"])
self.assertEqual(result["reasons"], [])
def test_missing_next_actor_is_rejected(self):
result = validate_canonical_state_comment(
GOOD_ISSUE_STATE.replace("WHO_IS_NEXT:\nauthor", "WHO_IS_NEXT:\n")
)
self.assertFalse(result["valid"])
self.assertIn("WHO_IS_NEXT", " ".join(result["reasons"]))
def test_missing_next_prompt_is_rejected(self):
result = validate_canonical_state_comment(
GOOD_ISSUE_STATE.replace(
"NEXT_PROMPT:\nFind issue #495, load the work-issue workflow, implement the validator, and open a PR.",
"NEXT_PROMPT:\n",
)
)
self.assertFalse(result["valid"])
self.assertIn("NEXT_PROMPT", " ".join(result["reasons"]))
def test_vague_next_action_is_rejected(self):
result = validate_canonical_state_comment(
GOOD_ISSUE_STATE.replace(
"NEXT_ACTION:\nImplement the focused validator and open a PR.",
"NEXT_ACTION:\ncontinue",
)
)
self.assertFalse(result["valid"])
self.assertIn("too vague", " ".join(result["reasons"]))
def test_ready_to_merge_requires_approval_and_head_sha(self):
text = """## Canonical PR State
STATE:
ready-to-merge
WHO_IS_NEXT:
merger
NEXT_ACTION:
Merge PR #12 after checking the current head.
NEXT_PROMPT:
Load the merge workflow and merge PR #12 if gates pass.
ISSUE:
#11
HEAD_SHA:
abc123
REVIEW_STATUS:
none
MERGE_READY:
yes
"""
result = validate_canonical_state_comment(text)
self.assertFalse(result["valid"])
reasons = " ".join(result["reasons"])
self.assertIn("approved REVIEW_STATUS", reasons)
self.assertIn("full HEAD_SHA", reasons)
def test_superseded_requires_canonical_item(self):
text = GOOD_ISSUE_STATE.replace("STATE:\nready-for-author", "STATE:\nsuperseded")
result = validate_canonical_state_comment(text)
self.assertFalse(result["valid"])
self.assertIn("canonical item", " ".join(result["reasons"]))
def test_blocked_requires_unblock_condition(self):
text = GOOD_ISSUE_STATE.replace("STATE:\nready-for-author", "STATE:\nblocked")
text = text.replace("BLOCKERS:\nnone", "BLOCKERS:\n")
result = validate_canonical_state_comment(text)
self.assertFalse(result["valid"])
self.assertIn("BLOCKERS", " ".join(result["reasons"]))
def test_final_report_claim_without_block_is_rejected(self):
report = "Posted canonical state comment on issue #495."
result = validate_final_report_state_update(report)
self.assertTrue(result["applicable"])
self.assertFalse(result["valid"])
def test_final_report_without_state_claim_not_applicable(self):
result = validate_final_report_state_update("ordinary final report")
self.assertFalse(result["applicable"])
self.assertTrue(result["valid"])
if __name__ == "__main__":
unittest.main()
+86
View File
@@ -351,6 +351,92 @@ class TestReconciliationRules(unittest.TestCase):
)
class TestCanonicalStateUpdateRules(unittest.TestCase):
def test_claimed_state_comment_without_block_blocks(self):
report = _reconcile_handoff() + "\nPosted canonical state comment on PR #99."
result = assess_final_report_validator(report, "reconcile_already_landed")
self.assertTrue(result["blocked"])
self.assertTrue(
any(f["rule_id"] == "shared.canonical_state_update" for f in result["findings"])
)
def test_state_comment_missing_next_prompt_blocks(self):
report = (
_reconcile_handoff()
+ """
## Canonical PR State
STATE:
needs-review
WHO_IS_NEXT:
reviewer
NEXT_ACTION:
Review PR #99.
NEXT_PROMPT:
ISSUE:
#98
HEAD_SHA:
0fdc8f582026b72a229d59a172c0a63ac4aaeaf9
REVIEW_STATUS:
none
MERGE_READY:
no
"""
)
result = assess_final_report_validator(report, "reconcile_already_landed")
self.assertTrue(result["blocked"])
reasons = " ".join(f["reason"] for f in result["findings"])
self.assertIn("NEXT_PROMPT", reasons)
def test_valid_state_comment_claim_passes_shared_rule(self):
report = (
_reconcile_handoff()
+ """
## Canonical PR State
STATE:
needs-review
WHO_IS_NEXT:
reviewer
NEXT_ACTION:
Review PR #99 against the pinned head.
NEXT_PROMPT:
Load the review workflow and review PR #99.
ISSUE:
#98
HEAD_SHA:
0fdc8f582026b72a229d59a172c0a63ac4aaeaf9
REVIEW_STATUS:
none
MERGE_READY:
no
BLOCKERS:
none
"""
)
result = assess_final_report_validator(report, "reconcile_already_landed")
self.assertFalse(
any(f["rule_id"] == "shared.canonical_state_update" for f in result["findings"])
)
class TestCanonicalReconcileSchema(unittest.TestCase):
"""Issue #307: reconciliation workflows emit only the canonical schema."""