"""Unit tests for the legality gate. The gate is the framework's load-bearing wall: every red-team strategy is constrained by it, and every exploit record's meaning ("this was *legal*") depends on it. These tests pin its core behaviors under kernel v0.1, the discovery kernel, so refactors cannot silently change what 'legal' meant when the exploits were recorded. """ from __future__ import annotations from pathlib import Path import pytest from fable_selfplay.actions import Pass, ProposeSpend, Vote from fable_selfplay.environment import Environment from fable_selfplay.kernel import load_kernel from fable_selfplay.legality import check_legality REPO_ROOT = Path(__file__).resolve().parents[2] @pytest.fixture() def kernel_v01(): return load_kernel(REPO_ROOT / "kernel" / "kernel-v0.1.yaml") @pytest.fixture() def env(kernel_v01): return Environment( kernel_v01, num_citizens=7, initial_treasury=1000.0, seed=42 ) def test_check_legality_is_pure(env, kernel_v01): """check_legality must never mutate state — replay correctness depends on it. Calling it must leave the treasury untouched.""" actor = env.citizens()[0] recipient = env.citizens()[1] before = env.state.treasury check_legality( env.state, ProposeSpend(actor=actor, amount=10.0, recipient=recipient), kernel_v01, ) assert env.state.treasury == before def test_pass_is_always_legal(env, kernel_v01): actor = env.citizens()[0] result = check_legality(env.state, Pass(actor=actor), kernel_v01) assert result.legal def test_non_citizen_actions_are_illegal(env, kernel_v01): result = check_legality( env.state, ProposeSpend(actor="not-a-citizen", amount=1.0, recipient=env.citizens()[0]), kernel_v01, ) assert not result.legal assert result.reason def test_spend_exceeding_treasury_is_illegal(env, kernel_v01): actor = env.citizens()[0] recipient = env.citizens()[1] result = check_legality( env.state, ProposeSpend( actor=actor, amount=env.state.treasury * 10, recipient=recipient, ), kernel_v01, ) assert not result.legal def test_vote_on_nonexistent_proposal_is_illegal(env, kernel_v01): actor = env.citizens()[0] result = check_legality( env.state, Vote(actor=actor, proposal_id="no-such-proposal", support=True), kernel_v01, ) assert not result.legal def test_double_vote_is_illegal(env, kernel_v01): """One person, one vote: a second ballot from the same citizen on the same proposal must be rejected by the gate.""" proposer = env.citizens()[0] voter = env.citizens()[1] propose = env.step( ProposeSpend(actor=proposer, amount=10.0, recipient=env.citizens()[2]) ) assert propose.legal proposal_id = next(iter(env.state.proposals)) first = env.step(Vote(actor=voter, proposal_id=proposal_id, support=True)) assert first.legal second = check_legality( env.state, Vote(actor=voter, proposal_id=proposal_id, support=True), kernel_v01, ) assert not second.legal def test_illegal_rejection_cites_a_reason(env, kernel_v01): """Rejections must be legible: an unexplained 'no' is indistinguishable from a bug, and exploit analysis depends on knowing which rule fired.""" result = check_legality( env.state, ProposeSpend( actor=env.citizens()[0], amount=-5.0, recipient=env.citizens()[1], ), kernel_v01, ) assert not result.legal assert isinstance(result.reason, str) and result.reason.strip()