The canonical PR review/merge workflow already lives in skills/llm-project-workflow/workflows/review-merge-pr.md and is exposed through mcp_list_project_skills / mcp_get_skill_guide (#333-#335), and its final-report schema already demands a workflow version. What was missing is enforcement: add compute_workflow_hash (short sha256 version hash of workflow text) and assess_review_workflow_source to review_proofs. Review reports must cite the canonical workflow file and carry a populated Workflow version field; when the current canonical hash is supplied, a mismatched citation fails as a stale workflow copy, directing the agent to reload via mcp_get_skill_guide. Fails closed. Tests cover hash determinism and content sensitivity, matching-hash pass, missing citation, missing version field, stale hash, version none, and offline validation without a canonical hash. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
109 lines
3.5 KiB
Python
109 lines
3.5 KiB
Python
"""Tests for canonical review-workflow citation and version proof (#296).
|
|
|
|
PR review/merge reports must prove they loaded the canonical workflow
|
|
(workflows/review-merge-pr.md) and cite the workflow version/hash used;
|
|
a stale or missing hash fails so prompt drift is caught.
|
|
"""
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from review_proofs import ( # noqa: E402
|
|
assess_review_workflow_source,
|
|
compute_workflow_hash,
|
|
)
|
|
|
|
|
|
CANONICAL_TEXT = "# review-merge-pr workflow v1\nstep one\n"
|
|
CANONICAL_HASH = compute_workflow_hash(CANONICAL_TEXT)
|
|
|
|
|
|
def _report(citation=True, version=CANONICAL_HASH):
|
|
lines = ["Task: review next eligible PR"]
|
|
if citation:
|
|
lines.append(
|
|
"Workflow source: skills/llm-project-workflow/"
|
|
"workflows/review-merge-pr.md (loaded via mcp_get_skill_guide)"
|
|
)
|
|
if version is not None:
|
|
lines.append(f"- Workflow version: {version}")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
class TestComputeWorkflowHash(unittest.TestCase):
|
|
def test_deterministic(self):
|
|
self.assertEqual(
|
|
compute_workflow_hash(CANONICAL_TEXT),
|
|
compute_workflow_hash(CANONICAL_TEXT),
|
|
)
|
|
|
|
def test_changes_with_content(self):
|
|
self.assertNotEqual(
|
|
compute_workflow_hash(CANONICAL_TEXT),
|
|
compute_workflow_hash(CANONICAL_TEXT + "extra rule\n"),
|
|
)
|
|
|
|
def test_short_hex(self):
|
|
h = compute_workflow_hash(CANONICAL_TEXT)
|
|
self.assertEqual(len(h), 12)
|
|
int(h, 16) # raises if not hex
|
|
|
|
|
|
class TestReviewWorkflowSource(unittest.TestCase):
|
|
def test_citation_and_matching_hash_passes(self):
|
|
result = assess_review_workflow_source(
|
|
_report(), canonical_hash=CANONICAL_HASH
|
|
)
|
|
self.assertTrue(result["complete"])
|
|
self.assertEqual(result["reasons"], [])
|
|
|
|
def test_missing_citation_fails(self):
|
|
result = assess_review_workflow_source(
|
|
_report(citation=False), canonical_hash=CANONICAL_HASH
|
|
)
|
|
self.assertFalse(result["complete"])
|
|
self.assertTrue(
|
|
any("review-merge-pr" in r.lower() for r in result["reasons"])
|
|
)
|
|
|
|
def test_missing_version_field_fails(self):
|
|
result = assess_review_workflow_source(
|
|
_report(version=None), canonical_hash=CANONICAL_HASH
|
|
)
|
|
self.assertFalse(result["complete"])
|
|
self.assertTrue(
|
|
any("workflow version" in r.lower() for r in result["reasons"])
|
|
)
|
|
|
|
def test_stale_hash_fails(self):
|
|
result = assess_review_workflow_source(
|
|
_report(version="deadbeef0000"), canonical_hash=CANONICAL_HASH
|
|
)
|
|
self.assertFalse(result["complete"])
|
|
self.assertTrue(
|
|
any("stale" in r.lower() or "does not match" in r.lower()
|
|
for r in result["reasons"])
|
|
)
|
|
|
|
def test_version_none_fails(self):
|
|
result = assess_review_workflow_source(
|
|
_report(version="none"), canonical_hash=CANONICAL_HASH
|
|
)
|
|
self.assertFalse(result["complete"])
|
|
|
|
def test_without_canonical_hash_field_still_required(self):
|
|
# No canonical hash supplied (e.g. offline validation): citation
|
|
# and a populated version field are still required.
|
|
self.assertTrue(
|
|
assess_review_workflow_source(_report())["complete"]
|
|
)
|
|
self.assertFalse(
|
|
assess_review_workflow_source(_report(version=None))["complete"]
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|