""" 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" )