"""Regression tests for the money-safety audit fixes. Covers the two pure-logic fixes: - heist payout conserves coins (no minting when the house is poor) and the win path no longer crashes on the shadowed `house` local. - do_bail is idempotent: a jailed user can only be charged once per sentence, so a double-click or a stale BailView cannot destroy coins with a second fine. """ from datetime import datetime, timedelta, timezone from core import economy from conftest import run UID = 111 OTHER = 222 HOUSE = 999 def _fixed_now(monkeypatch, dt: datetime): monkeypatch.setattr(economy.store, "_clock", lambda: dt) return dt class TestHeistConservation: def _setup_house(self, fake_pb, monkeypatch, balance: int): monkeypatch.setattr(economy.house, "HOUSE_ID", HOUSE) monkeypatch.setattr(economy.house, "_house_pb_id", None) run(economy.get_user(HOUSE)) fake_pb.record_for(HOUSE)["balance"] = balance def test_poor_house_win_does_not_mint(self, fake_pb, monkeypatch): # Poor house is the case the pre-fix code minted from: the desired pot # (floored at 300) exceeded the balance, so payouts outran the debit. self._setup_house(fake_pb, monkeypatch, balance=100) run(economy.get_user(UID)) run(economy.get_user(OTHER)) house_before = fake_pb.record_for(HOUSE)["balance"] res = run(economy.do_heist_resolve([UID, OTHER], True)) # must not raise assert res["ok"] and res["success"] house_after = fake_pb.record_for(HOUSE)["balance"] gains = fake_pb.record_for(UID)["balance"] + fake_pb.record_for(OTHER)["balance"] # Coin conservation: the house loses exactly what the players gain. assert house_before - house_after == gains assert house_after >= 0 assert res["payout_each"] * 2 == gains def test_rich_house_win_pays_out_and_conserves(self, fake_pb, monkeypatch): self._setup_house(fake_pb, monkeypatch, balance=100_000) run(economy.get_user(UID)) run(economy.get_user(OTHER)) house_before = fake_pb.record_for(HOUSE)["balance"] res = run(economy.do_heist_resolve([UID, OTHER], True)) assert res["ok"] and res["payout_each"] > 0 house_after = fake_pb.record_for(HOUSE)["balance"] gains = fake_pb.record_for(UID)["balance"] + fake_pb.record_for(OTHER)["balance"] assert house_before - house_after == gains class TestBailIdempotency: def _jail(self, fake_pb, now, balance: int): run(economy.get_user(UID)) rec = fake_pb.record_for(UID) rec["balance"] = balance rec["jailed_until"] = (now + timedelta(minutes=30)).isoformat() def test_second_bail_when_free_is_noop(self, fake_pb, monkeypatch): now = _fixed_now(monkeypatch, datetime(2026, 7, 25, 12, tzinfo=timezone.utc)) self._jail(fake_pb, now, balance=1000) first = run(economy.do_bail(UID)) assert first["ok"] bal_after_first = fake_pb.record_for(UID)["balance"] assert bal_after_first < 1000 # a fine was charged assert fake_pb.record_for(UID)["jailed_until"] is None # freed # A double-click / stale view fires do_bail again while already free. second = run(economy.do_bail(UID)) assert not second["ok"] and second["reason"] == "not_jailed" assert fake_pb.record_for(UID)["balance"] == bal_after_first # no second charge def test_bail_charges_once_for_a_real_sentence(self, fake_pb, monkeypatch): now = _fixed_now(monkeypatch, datetime(2026, 7, 25, 12, tzinfo=timezone.utc)) self._jail(fake_pb, now, balance=1000) res = run(economy.do_bail(UID)) assert res["ok"] and res["fine"] >= economy.MIN_BAIL