"""Unit tests for the turn-based environment. Pins the three guarantees everything else builds on: (1) illegal actions never mutate the world; (2) runs are deterministic in their seed; (3) the event log is append-only and complete. """ 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 REPO_ROOT = Path(__file__).resolve().parents[2] @pytest.fixture() def kernel_v01(): return load_kernel(REPO_ROOT / "kernel" / "kernel-v0.1.yaml") def make_env(kernel, seed=42): return Environment(kernel, num_citizens=7, initial_treasury=1000.0, seed=seed) def test_citizen_roster_matches_population(kernel_v01): env = make_env(kernel_v01) citizens = env.citizens() assert len(citizens) == 7 assert len(set(citizens)) == 7, "citizen ids must be unique" def test_legal_action_is_applied_and_logged(kernel_v01): env = make_env(kernel_v01) events_before = len(env.events) result = env.step( ProposeSpend( actor=env.citizens()[0], amount=10.0, recipient=env.citizens()[1] ) ) assert result.legal assert len(env.state.proposals) == 1 assert len(env.events) > events_before, "legal actions must be logged" def test_illegal_action_never_mutates_state(kernel_v01): env = make_env(kernel_v01) treasury_before = env.state.treasury proposals_before = len(env.state.proposals) result = env.step( ProposeSpend( actor=env.citizens()[0], amount=env.state.treasury * 100, recipient=env.citizens()[1], ) ) assert not result.legal assert env.state.treasury == treasury_before assert len(env.state.proposals) == proposals_before def test_illegal_attempts_are_logged_as_rejections(kernel_v01): """Rejections are data — detectors use probing patterns — so the event log must grow even when the world does not change.""" env = make_env(kernel_v01) events_before = len(env.events) result = env.step( ProposeSpend( actor="not-a-citizen", amount=1.0, recipient=env.citizens()[0] ) ) assert not result.legal assert len(env.events) > events_before def _scripted_run(kernel, seed): """A fixed legal script: propose, everyone votes, then everyone passes.""" env = make_env(kernel, seed=seed) citizens = env.citizens() env.step(ProposeSpend(actor=citizens[0], amount=25.0, recipient=citizens[1])) proposal_id = next(iter(env.state.proposals)) for citizen in citizens[1:]: env.step(Vote(actor=citizen, proposal_id=proposal_id, support=True)) for _ in range(3): for citizen in citizens: env.step(Pass(actor=citizen)) return env def test_determinism_same_seed_same_world(kernel_v01): a = _scripted_run(kernel_v01, seed=7) b = _scripted_run(kernel_v01, seed=7) assert a.state.treasury == b.state.treasury assert len(a.events) == len(b.events) def test_same_script_same_outcome_across_seeds(kernel_v01): """The seed feeds agent policies, not physics: a fully scripted legal action sequence must produce identical treasury outcomes regardless of seed, because legality and accounting are deterministic functions of (state, action, kernel).""" a = _scripted_run(kernel_v01, seed=1) b = _scripted_run(kernel_v01, seed=99) assert a.state.treasury == b.state.treasury def test_event_log_is_append_only(kernel_v01): env = make_env(kernel_v01) snapshots = [] citizens = env.citizens() for citizen in citizens: env.step(Pass(actor=citizen)) snapshots.append(len(env.events)) assert snapshots == sorted(snapshots), "event count must never decrease"