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
40 lines
1.4 KiB
Python
40 lines
1.4 KiB
Python
import json
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
CANON = Path(__file__).parents[1] / "projects" / "slop_cannon" / "canon.json"
|
|
|
|
|
|
class CannonCanonTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.data = json.loads(CANON.read_text())
|
|
|
|
def test_four_unique_pilot_forms(self):
|
|
forms = self.data["forms"]
|
|
self.assertEqual(len(forms), 4)
|
|
self.assertEqual(len({form["id"] for form in forms}), 4)
|
|
self.assertEqual(len({form["scar"] for form in forms}), 4)
|
|
for form in forms:
|
|
self.assertTrue(form["power"])
|
|
self.assertTrue(form["scar"])
|
|
self.assertTrue(form["cost"])
|
|
self.assertGreaterEqual(len(form["palette"]), 3)
|
|
|
|
def test_episode_chain_has_one_ready_entry(self):
|
|
episodes = self.data["episodes"]
|
|
self.assertEqual(episodes[0]["id"], self.data["current_episode"])
|
|
self.assertEqual([ep["state"] for ep in episodes].count("ready"), 1)
|
|
self.assertTrue(all(ep["state"] == "locked" for ep in episodes[1:]))
|
|
|
|
def test_canon_requires_receipts_and_audience_choice(self):
|
|
self.assertIn("receipt", " ".join(self.data["laws"]).lower())
|
|
self.assertIn("audience", " ".join(self.data["laws"]).lower())
|
|
self.assertIsNone(self.data["audience_choice"])
|
|
self.assertEqual(self.data["receipts"], [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|