stackchain-lab-loop/tests/test_canon_validator.py
vincent b8d5e686c2 [Vincent] #19: Executable canon lifecycle validator with daily receipt fixtures
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
2026-08-14 14:10:09 -04:00

265 lines
9.4 KiB
Python

"""
Validator tests for canon lifecycle schemas.
Covers:
- Valid canon structure (from #13 fix)
- Negative tests: malformed receipts, illegal transitions
- Daily Lab receipt fixture with provenance binding
"""
import json
import sys
import unittest
from pathlib import Path
from copy import deepcopy
# Add project root to path for imports
sys.path.insert(0, str(Path(__file__).parents[1]))
CANON = Path(__file__).parents[1] / "projects" / "slop_cannon" / "canon.json"
from projects.slop_cannon.canon_validator import (
validate_canon,
validate_transition,
)
class CanonValidatorTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.data = json.loads(CANON.read_text())
# --- Positive tests ---
def test_current_canon_is_valid(self):
"""canon.json as committed should pass validation."""
result = validate_canon(self.data)
self.assertTrue(result["valid"], f"Errors: {result['errors']}")
def test_all_forms_have_valid_state(self):
"""Every form state must be in the valid set."""
result = validate_canon(self.data)
self.assertTrue(result["valid"], result["errors"])
# --- Negative: malformed receipts ---
def test_missing_signed_by_fails(self):
"""Receipt without signed_by is rejected."""
data = deepcopy(self.data)
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "wizard",
"provenance": "#13",
}
]
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("signed_by" in e for e in result["errors"]))
def test_invalid_signer_fails(self):
"""Signed_by must match agent: or human: pattern."""
data = deepcopy(self.data)
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "wizard",
"provenance": "#13",
"signed_by": "unknown:bot",
}
]
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("invalid signed_by" in e for e in result["errors"]))
def test_empty_provenance_fails(self):
"""Receipt with empty provenance is rejected."""
data = deepcopy(self.data)
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "wizard",
"provenance": "",
"signed_by": "agent:vincent",
}
]
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("provenance" in e for e in result["errors"]))
def test_form_id_must_exist(self):
"""Receipt referencing nonexistent form is rejected."""
data = deepcopy(self.data)
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "nonexistent",
"provenance": "#13",
"signed_by": "agent:vincent",
}
]
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("not found in forms" in e for e in result["errors"]))
# --- Negative: illegal state transitions ---
def test_survivor_cannot_transition(self):
"""Survivor state is terminal."""
form = {"id": "wizard", "state": "survivor"}
result = validate_transition(form, "archive_ghost")
self.assertFalse(result["valid"])
self.assertTrue(any("Illegal transition" in e for e in result["errors"]))
def test_ghost_cannot_transition(self):
"""Archive_ghost state is terminal."""
form = {"id": "wizard", "state": "archive_ghost"}
result = validate_transition(form, "candidate")
self.assertFalse(result["valid"])
self.assertTrue(any("Illegal transition" in e for e in result["errors"]))
def test_candidate_to_survivor_valid(self):
"""Candidate -> survivor is the expected happy path."""
form = {"id": "wizard", "state": "candidate"}
result = validate_transition(form, "survivor")
self.assertTrue(result["valid"])
def test_candidate_to_ghost_valid(self):
"""Candidate -> archive_ghost is valid (rejected form)."""
form = {"id": "wizard", "state": "candidate"}
result = validate_transition(form, "archive_ghost")
self.assertTrue(result["valid"])
# --- Negative: audience choice ---
def test_two_survivors_fails(self):
"""Only one survivor allowed."""
data = deepcopy(self.data)
for f in data["forms"][:2]:
f["state"] = "survivor"
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("survivor" in e.lower() for e in result["errors"]))
def test_choice_without_survivor_fails(self):
"""Setting audience_choice requires a survivor in forms."""
data = deepcopy(self.data)
data["audience_choice"] = {
"survivor_id": "wizard",
"stolen_trait": "Proves the execution path",
"source_episode": "01-pilot",
"source_issue": "7",
"committed_at": "2026-08-11T19:00:00Z",
}
# forms all stay candidate -> no survivor
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("survivor" in e.lower() for e in result["errors"]))
def test_invalid_committed_at_fails(self):
"""committed_at must be valid ISO timestamp."""
data = deepcopy(self.data)
data["forms"][0]["state"] = "survivor" # satisfy survivor req
data["audience_choice"] = {
"survivor_id": "wizard",
"stolen_trait": "Negotiates visible covenants",
"source_episode": "01-pilot",
"source_issue": "7",
"committed_at": "not-a-date",
}
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("ISO" in e for e in result["errors"]))
# --- Negative: structure ---
def test_duplicate_form_id_fails(self):
"""Duplicate form IDs are rejected."""
data = deepcopy(self.data)
data["forms"].append(deepcopy(data["forms"][0]))
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("duplicate" in e for e in result["errors"]))
def test_short_palette_fails(self):
"""Palette needs at least 3 colors."""
data = deepcopy(self.data)
data["forms"][0]["palette"] = ["red"]
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("palette" in e for e in result["errors"]))
class DailyReceiptFixtureTests(unittest.TestCase):
"""Test the Daily Lab receipt fixture with Vincent's provenance."""
def test_vincent_receipt_fixture(self):
"""A valid Vincent contribution receipt passes validation."""
data = deepcopy(json.loads(CANON.read_text()))
# Build a receipt for the #13 contribution
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "wizard",
"action": "mutation",
"power_delta": "Added lifecycle state tracking",
"scar_delta": "Receipt schema now enforces provenance",
"cost_delta": "All future drops require signed receipts",
"provenance": "pull/18@562d0cf",
"signed_by": "agent:vincent",
}
]
result = validate_canon(data)
self.assertTrue(result["valid"], f"Fixture failed: {result['errors']}")
def test_mixed_agent_receipt(self):
"""Receipts from different agents can coexist."""
data = deepcopy(json.loads(CANON.read_text()))
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "wizard",
"action": "mutation",
"power_delta": "Lifecycle state",
"scar_delta": "Provenance enforcement",
"cost_delta": "Signed receipts required",
"provenance": "pull/18@562d0cf",
"signed_by": "agent:vincent",
},
{
"episode_id": "01-pilot",
"form_id": "machine",
"action": "graft",
"power_delta": "Execution path proof",
"scar_delta": "Red tallies",
"cost_delta": "Remembers failures",
"provenance": "pull/12@47ca557",
"signed_by": "agent:timmy",
},
]
result = validate_canon(data)
self.assertTrue(result["valid"], f"Mixed fixture failed: {result['errors']}")
def test_human_receipt(self):
"""Human-signed receipts are valid (for canon decisions)."""
data = deepcopy(json.loads(CANON.read_text()))
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "wizard",
"action": "mutation",
"power_delta": "x",
"scar_delta": "x",
"cost_delta": "x",
"provenance": "#7",
"signed_by": "human:grepples",
}
]
result = validate_canon(data)
self.assertTrue(result["valid"], f"Human fixture failed: {result['errors']}")
if __name__ == "__main__":
unittest.main()