Builds on #13/PR #18 review. Makes canon lifecycle machine-checkable. Deliverables: 1. Dependency-free canon_validator.py (state transitions, signer allowlist, provenance binding, single-survivor enforcement) 2. 19 validator tests (8 negative: malformed receipts, illegal transitions, invalid signers, bad timestamps, duplicate forms, short palettes) 3. Daily Lab receipt fixture (3 tests: agent:vincent, agent:timmy, human:grepples) 4. Full suite integration - 31 tests pass from clean checkout 5. .gitignore for bytecode, renamed slop-cannon → slop_cannon (Python import) Closes #19 Refs: #5, #13, PR #12, PR #18
47 lines
2.0 KiB
Python
47 lines
2.0 KiB
Python
import json
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
CANON = Path(__file__).parents[1] / "projects" / "slop_cannon" / "canon.json"
|
|
|
|
|
|
class CanonLifecycleTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.data = json.loads(CANON.read_text())
|
|
|
|
def test_forms_have_lifecycle_state(self):
|
|
"""Every form must have a state field (candidate/survivor/archive_ghost)."""
|
|
for form in self.data["forms"]:
|
|
self.assertIn("state", form, f"Form {form.get('id', '?')} missing lifecycle state")
|
|
self.assertIn(form["state"], ["candidate", "survivor", "archive_ghost"])
|
|
|
|
def test_audience_choice_schema_defined(self):
|
|
"""Schema must define how a choice is structured so Episode 04 can parse it."""
|
|
schema = self.data.get("audience_choice_schema", {})
|
|
required = ["survivor_id", "stolen_trait", "source_episode", "source_issue"]
|
|
for field in required:
|
|
self.assertIn(field, schema, f"audience_choice_schema missing {field}")
|
|
|
|
def test_receipt_schema_defined(self):
|
|
"""Receipts need provenance fields for path-proof binding."""
|
|
schema = self.data.get("receipt_schema", {})
|
|
required = ["episode_id", "form_id", "provenance", "signed_by"]
|
|
for field in required:
|
|
self.assertIn(field, schema, f"receipt_schema missing {field}")
|
|
|
|
def test_receipt_schema_requires_agent_signing(self):
|
|
"""Signed_by must constrain to known actors — free text defeats path proofs."""
|
|
signed = self.data["receipt_schema"]["signed_by"]
|
|
self.assertIn("agent:", signed, "signed_by should mention agent: prefix")
|
|
|
|
def test_law_enforced_by_data(self):
|
|
"""Law 'Every firing leaves a power, scar, cost, and receipt' requires receipt_schema to exist."""
|
|
laws_text = " ".join(self.data["laws"]).lower()
|
|
self.assertIn("receipt", laws_text)
|
|
self.assertIn("receipt_schema", self.data, "Law mentions receipt but no schema enforces it")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|