119 lines
4.8 KiB
Python
119 lines
4.8 KiB
Python
"""
|
|
Test: Constitution Hash Gate
|
|
|
|
Ensures the constitution has not been modified without proper amendment.
|
|
CI MUST fail if the constitution hash doesn't match the lock file.
|
|
"""
|
|
|
|
import pytest
|
|
import blake3
|
|
|
|
|
|
class TestConstitutionHash:
|
|
"""Constitution integrity tests - HARD GATE."""
|
|
|
|
def test_constitution_exists(self, constitution_path):
|
|
"""Constitution file must exist."""
|
|
assert constitution_path.exists(), "MCP-CONSTITUTION.md not found"
|
|
|
|
def test_lock_file_exists(self, constitution_lock_path):
|
|
"""Constitution lock file must exist."""
|
|
assert constitution_lock_path.exists(), "governance/constitution.lock not found"
|
|
|
|
def test_constitution_hash_matches_lock(self, constitution_path, parse_lock_file):
|
|
"""
|
|
HARD GATE: Constitution hash must match lock file.
|
|
|
|
If this fails, either:
|
|
1. Constitution was modified without amendment procedure
|
|
2. Lock file needs updating via proper amendment
|
|
"""
|
|
# Read constitution
|
|
content = constitution_path.read_text()
|
|
lines = content.split('\n')
|
|
|
|
# Hash excludes signature block (last 12 lines as per original ceremony)
|
|
# But after amendment protocol was added, we need to use the locked line count
|
|
hash_lines = int(parse_lock_file.get("hash_lines", 288))
|
|
hashable_content = '\n'.join(lines[:hash_lines])
|
|
|
|
computed_hash = f"blake3:{blake3.blake3(hashable_content.encode()).hexdigest()}"
|
|
locked_hash = parse_lock_file["hash"]
|
|
|
|
assert computed_hash == locked_hash, (
|
|
f"Constitution hash mismatch!\n"
|
|
f" Computed: {computed_hash}\n"
|
|
f" Locked: {locked_hash}\n"
|
|
f" If intentional, follow amendment procedure."
|
|
)
|
|
|
|
def test_version_not_decreased(self, parse_lock_file):
|
|
"""Version must not decrease (no rollbacks without amendment)."""
|
|
version = parse_lock_file["version"]
|
|
parts = [int(p) for p in version.split(".")]
|
|
|
|
# Version 1.0.0 is the minimum
|
|
assert parts >= [1, 0, 0], "Constitution version cannot be below 1.0.0"
|
|
|
|
def test_immutable_rules_count(self, parse_lock_file):
|
|
"""Immutable rules count must be exactly 5."""
|
|
immutable_count = int(parse_lock_file["immutable_rules"])
|
|
assert immutable_count == 5, (
|
|
f"Immutable rules count changed from 5 to {immutable_count}. "
|
|
"This violates immutability clause."
|
|
)
|
|
|
|
def test_cooldown_days_minimum(self, parse_lock_file):
|
|
"""Amendment cooldown must be at least 7 days."""
|
|
cooldown = int(parse_lock_file["cooldown_days"])
|
|
assert cooldown >= 7, (
|
|
f"Cooldown period reduced to {cooldown} days. "
|
|
"Minimum is 7 days per constitution."
|
|
)
|
|
|
|
def test_btc_anchor_required(self, parse_lock_file):
|
|
"""BTC anchor requirement must be true."""
|
|
requires_anchor = parse_lock_file["requires_btc_anchor"].lower() == "true"
|
|
assert requires_anchor, "BTC anchor requirement cannot be disabled"
|
|
|
|
def test_sovereign_key_present(self, parse_lock_file):
|
|
"""Sovereign key must be specified."""
|
|
sovereign_key = parse_lock_file.get("sovereign_key")
|
|
assert sovereign_key and sovereign_key.startswith("key_"), (
|
|
"Sovereign key must be specified in lock file"
|
|
)
|
|
|
|
|
|
class TestConstitutionContent:
|
|
"""Tests that verify constitution content invariants."""
|
|
|
|
def test_profiles_defined(self, constitution_path):
|
|
"""All five profiles must be defined."""
|
|
content = constitution_path.read_text()
|
|
profiles = ["OBSERVER", "OPERATOR", "GUARDIAN", "PHOENIX", "SOVEREIGN"]
|
|
|
|
for profile in profiles:
|
|
assert profile in content, f"Profile {profile} not found in constitution"
|
|
|
|
def test_immutable_clauses_present(self, constitution_path):
|
|
"""All immutable clauses must be present."""
|
|
content = constitution_path.read_text()
|
|
immutables = [
|
|
"SOVEREIGN profile requires human verification",
|
|
"No AI may grant itself SOVEREIGN authority",
|
|
"Every mutation emits a receipt",
|
|
"Authority collapses downward, never upward",
|
|
"This immutability clause itself",
|
|
]
|
|
|
|
for clause in immutables:
|
|
assert clause in content, f"Immutable clause missing: {clause}"
|
|
|
|
def test_amendment_protocol_exists(self, constitution_path):
|
|
"""Amendment protocol must be defined."""
|
|
content = constitution_path.read_text()
|
|
assert "Amendment Protocol" in content, "Amendment protocol section missing"
|
|
assert "Cooling Period" in content or "cooling" in content.lower(), (
|
|
"Cooling period not defined in amendment protocol"
|
|
)
|