[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
This commit is contained in:
1 parent
562d0cfa9e
commit
b8d5e686c2
10 files changed
+506
-2
No files matched your search
@@ -3,7 +3,7 @@ import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
CANON = Path(__file__).parents[1] / "projects" / "slop-cannon" / "canon.json"
|
||||
CANON = Path(__file__).parents[1] / "projects" / "slop_cannon" / "canon.json"
|
||||
|
||||
|
||||
class CannonCanonTests(unittest.TestCase):
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
CANON = Path(__file__).parents[1] / "projects" / "slop-cannon" / "canon.json"
|
||||
CANON = Path(__file__).parents[1] / "projects" / "slop_cannon" / "canon.json"
|
||||
|
||||
|
||||
class CanonLifecycleTests(unittest.TestCase):
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
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()
|
||||
Reference in new issue
Block a user