"""Tests for the consumables shop (repeatable, expiring coin sink).""" from datetime import datetime, timedelta, timezone from core import economy from conftest import run UID = 4242 def _fixed_now(monkeypatch, dt: datetime): # _clock is the time seam shared by every economy submodule monkeypatch.setattr(economy.store, "_clock", lambda: dt) return dt def _fund(fake_pb, amount: int) -> None: run(economy.get_user(UID)) fake_pb.record_for(UID)["balance"] = amount class TestBuy: def test_buy_earn_buff_deducts_and_activates(self, fake_pb): _fund(fake_pb, 1000) res = run(economy.do_buy_consumable(UID, "energy_xl")) assert res["ok"] and not res["instant"] assert res["balance"] == 500 # 1000 - 500 cost user = run(economy.get_user(UID)) assert economy.earn_mult(user) == 2.0 assert economy.exp_buff_mult(user) == 1.0 def test_insufficient_funds_rejected(self, fake_pb): _fund(fake_pb, 100) res = run(economy.do_buy_consumable(UID, "energy_xl")) assert not res["ok"] and res["reason"] == "insufficient" assert res["need"] == 400 assert run(economy.get_user(UID))["balance"] == 100 # unchanged def test_banned_rejected(self, fake_pb): _fund(fake_pb, 1000) fake_pb.record_for(UID)["eco_banned"] = True res = run(economy.do_buy_consumable(UID, "energy_xl")) assert not res["ok"] and res["reason"] == "banned" def test_unknown_consumable(self, fake_pb): _fund(fake_pb, 1000) res = run(economy.do_buy_consumable(UID, "nope")) assert not res["ok"] and res["reason"] == "not_found" class TestBuffLifecycle: def test_buff_expires(self, fake_pb, monkeypatch): t0 = _fixed_now(monkeypatch, datetime(2026, 8, 19, 12, tzinfo=timezone.utc)) _fund(fake_pb, 1000) run(economy.do_buy_consumable(UID, "energy_xl")) assert economy.earn_mult(run(economy.get_user(UID))) == 2.0 # 61 minutes later the 60-minute buff is gone _fixed_now(monkeypatch, t0 + timedelta(minutes=61)) assert economy.earn_mult(run(economy.get_user(UID))) == 1.0 def test_rebuy_extends_duration(self, fake_pb, monkeypatch): t0 = _fixed_now(monkeypatch, datetime(2026, 8, 19, 12, tzinfo=timezone.utc)) _fund(fake_pb, 2000) run(economy.do_buy_consumable(UID, "energy_xl")) # expiry = t0 + 60m _fixed_now(monkeypatch, t0 + timedelta(minutes=30)) res = run(economy.do_buy_consumable(UID, "energy_xl")) # extends, not resets assert res["extended"] is True # remaining should be ~90m (30m left + 60m added), not 60m assert res["remaining"] > timedelta(minutes=85) class TestEffects: def test_earn_buff_doubles_work(self, fake_pb, monkeypatch): import random _fixed_now(monkeypatch, datetime(2026, 8, 19, 12, tzinfo=timezone.utc)) _fund(fake_pb, 1000) monkeypatch.setattr(random, "randint", lambda a, b: 50) monkeypatch.setattr(random, "choice", lambda seq: seq[0]) monkeypatch.setattr(random, "random", lambda: 0.99) # no energiajook luck base = run(economy.do_work(UID))["earned"] fake_pb.record_for(UID)["last_work"] = None # clear cooldown run(economy.do_buy_consumable(UID, "energy_xl")) boosted = run(economy.do_work(UID))["earned"] assert boosted == base * 2 def test_exp_buff_doubles_award(self, fake_pb): _fund(fake_pb, 1000) run(economy.do_buy_consumable(UID, "xp_potion")) res = run(economy.award_exp(UID, 10)) assert res["gained"] == 20 # 10 * 2x exp buff def test_kohv_clears_cooldowns(self, fake_pb, monkeypatch): t0 = _fixed_now(monkeypatch, datetime(2026, 8, 19, 12, tzinfo=timezone.utc)) _fund(fake_pb, 1000) rec = fake_pb.record_for(UID) rec["last_work"] = t0.isoformat() # on cooldown res = run(economy.do_buy_consumable(UID, "kohv")) assert res["ok"] and res["instant"] user = run(economy.get_user(UID)) assert user["last_work"] is None # cooldown wiped -> /work is ready assert economy.store._cooldown_remaining(user, "work") is None def test_instant_reset_commands_match_cleared_cooldowns(self): # The Discord layer cancels reminder DMs for exactly these commands after a # kohv, so the list must stay in lockstep with the cooldown fields it wipes. expected = tuple(f.removeprefix("last_") for f in economy.consumables._COOLDOWN_FIELDS) assert economy.INSTANT_RESET_COMMANDS == expected assert economy.INSTANT_RESET_COMMANDS == ("work", "beg", "crime", "rob", "fish")