Added tests, fix README.
This commit is contained in:
108
tests/conftest.py
Normal file
108
tests/conftest.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Shared fixtures: an in-memory PocketBase stand-in wired into core.economy."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from core import economy, pb_client # noqa: E402
|
||||
|
||||
|
||||
class FakePocketBase:
|
||||
"""In-memory stand-in for core.pb_client.
|
||||
|
||||
Mimics the behaviours the economy layer depends on:
|
||||
- records are returned as deep copies (like JSON over REST)
|
||||
- "field+" / "field-" body keys are atomic number modifiers
|
||||
- fields not in `schema_fields` are silently dropped, like PocketBase
|
||||
does for fields missing from the collection schema (schema_fields=None
|
||||
keeps everything)
|
||||
"""
|
||||
|
||||
def __init__(self, schema_fields: set[str] | None = None):
|
||||
self.records: dict[str, dict] = {}
|
||||
self.schema_fields = schema_fields
|
||||
self._next_id = 0
|
||||
|
||||
def _filter(self, data: dict) -> dict:
|
||||
if self.schema_fields is None:
|
||||
return dict(data)
|
||||
return {k: v for k, v in data.items() if k.rstrip("+-") in self.schema_fields}
|
||||
|
||||
def _apply(self, record: dict, data: dict) -> None:
|
||||
for key, value in self._filter(data).items():
|
||||
if key.endswith("+"):
|
||||
record[key[:-1]] = record.get(key[:-1], 0) + value
|
||||
elif key.endswith("-"):
|
||||
record[key[:-1]] = record.get(key[:-1], 0) - value
|
||||
else:
|
||||
record[key] = value
|
||||
|
||||
async def get_record(self, user_id: str) -> dict | None:
|
||||
await asyncio.sleep(0) # yield, so unserialized tasks would interleave
|
||||
for record in self.records.values():
|
||||
if record.get("user_id") == user_id:
|
||||
return copy.deepcopy(record)
|
||||
return None
|
||||
|
||||
async def create_record(self, record: dict) -> dict:
|
||||
await asyncio.sleep(0)
|
||||
self._next_id += 1
|
||||
stored = self._filter(record)
|
||||
stored["id"] = f"rec{self._next_id}"
|
||||
stored["user_id"] = record.get("user_id", "")
|
||||
self.records[stored["id"]] = stored
|
||||
return copy.deepcopy(stored)
|
||||
|
||||
async def update_record(self, record_id: str, data: dict) -> dict:
|
||||
await asyncio.sleep(0)
|
||||
record = self.records[record_id]
|
||||
self._apply(record, data)
|
||||
return copy.deepcopy(record)
|
||||
|
||||
async def list_all_records(self, page_size: int = 500) -> list[dict]:
|
||||
await asyncio.sleep(0)
|
||||
return copy.deepcopy(list(self.records.values()))
|
||||
|
||||
async def count_records(self) -> int:
|
||||
return len(self.records)
|
||||
|
||||
# -- test helpers -------------------------------------------------------
|
||||
def record_for(self, user_id: int) -> dict:
|
||||
for record in self.records.values():
|
||||
if record.get("user_id") == str(user_id):
|
||||
return record
|
||||
raise KeyError(user_id)
|
||||
|
||||
|
||||
def _install(monkeypatch, fake: FakePocketBase) -> FakePocketBase:
|
||||
for name in ("get_record", "create_record", "update_record",
|
||||
"list_all_records", "count_records"):
|
||||
monkeypatch.setattr(pb_client, name, getattr(fake, name))
|
||||
monkeypatch.setattr(economy, "HOUSE_ID", None)
|
||||
monkeypatch.setattr(economy, "_house_pb_id", None)
|
||||
economy._user_locks.clear()
|
||||
return fake
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_pb(monkeypatch) -> FakePocketBase:
|
||||
return _install(monkeypatch, FakePocketBase())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_pb_without_quest_fields(monkeypatch) -> FakePocketBase:
|
||||
"""A fake whose collection schema predates the quest migration."""
|
||||
schema = set(economy._default_user().keys()) | {"user_id"}
|
||||
schema -= {"quest_daily", "quest_weekly"}
|
||||
return _install(monkeypatch, FakePocketBase(schema_fields=schema))
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.run(coro)
|
||||
204
tests/test_economy_flows.py
Normal file
204
tests/test_economy_flows.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""Tests for the async economy flows against the in-memory PocketBase fake."""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
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, "_now", lambda: dt)
|
||||
return dt
|
||||
|
||||
|
||||
class TestGetUser:
|
||||
def test_creates_default_record(self, fake_pb):
|
||||
user = run(economy.get_user(UID))
|
||||
assert user["balance"] == 0
|
||||
assert fake_pb.record_for(UID)["user_id"] == str(UID)
|
||||
|
||||
def test_roundtrips_existing_data(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["balance"] = 1234
|
||||
assert run(economy.get_user(UID))["balance"] == 1234
|
||||
|
||||
|
||||
class TestDaily:
|
||||
def test_first_claim(self, fake_pb):
|
||||
res = run(economy.do_daily(UID))
|
||||
assert res["ok"] and res["streak"] == 1 and res["earned"] == 150
|
||||
|
||||
def test_cooldown_blocks_second_claim(self, fake_pb):
|
||||
run(economy.do_daily(UID))
|
||||
res = run(economy.do_daily(UID))
|
||||
assert not res["ok"] and res["reason"] == "cooldown"
|
||||
|
||||
def test_streak_increments_next_day(self, fake_pb, monkeypatch):
|
||||
t0 = _fixed_now(monkeypatch, datetime(2026, 7, 25, 12, tzinfo=timezone.utc))
|
||||
run(economy.do_daily(UID))
|
||||
_fixed_now(monkeypatch, t0 + timedelta(days=1))
|
||||
res = run(economy.do_daily(UID))
|
||||
assert res["ok"] and res["streak"] == 2
|
||||
|
||||
def test_streak_resets_after_missed_day(self, fake_pb, monkeypatch):
|
||||
t0 = _fixed_now(monkeypatch, datetime(2026, 7, 25, 12, tzinfo=timezone.utc))
|
||||
run(economy.do_daily(UID))
|
||||
_fixed_now(monkeypatch, t0 + timedelta(days=3))
|
||||
res = run(economy.do_daily(UID))
|
||||
assert res["ok"] and res["streak"] == 1
|
||||
|
||||
def test_karikas_preserves_streak(self, fake_pb, monkeypatch):
|
||||
t0 = _fixed_now(monkeypatch, datetime(2026, 7, 25, 12, tzinfo=timezone.utc))
|
||||
run(economy.do_daily(UID))
|
||||
fake_pb.record_for(UID)["items"] = ["karikas"]
|
||||
fake_pb.record_for(UID)["daily_streak"] = 5
|
||||
_fixed_now(monkeypatch, t0 + timedelta(days=3))
|
||||
res = run(economy.do_daily(UID))
|
||||
assert res["ok"] and res["streak"] == 5
|
||||
|
||||
def test_streak_multiplier_tiers(self, fake_pb, monkeypatch):
|
||||
t0 = _fixed_now(monkeypatch, datetime(2026, 7, 25, 12, tzinfo=timezone.utc))
|
||||
run(economy.get_user(UID))
|
||||
rec = fake_pb.record_for(UID)
|
||||
rec["daily_streak"] = 13
|
||||
rec["last_streak_date"] = (t0.date() - timedelta(days=1)).isoformat()
|
||||
res = run(economy.do_daily(UID))
|
||||
assert res["streak"] == 14 and res["streak_mult"] == 3.0 and res["earned"] == 450
|
||||
|
||||
|
||||
class TestBuy:
|
||||
def test_insufficient_funds(self, fake_pb):
|
||||
res = run(economy.do_buy(UID, "gaming_hiir"))
|
||||
assert not res["ok"] and res["reason"] == "insufficient"
|
||||
|
||||
def test_purchase_and_rebuy_blocked(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["balance"] = 1000
|
||||
res = run(economy.do_buy(UID, "gaming_hiir"))
|
||||
assert res["ok"] and res["balance"] == 500
|
||||
assert "gaming_hiir" in fake_pb.record_for(UID)["items"]
|
||||
res = run(economy.do_buy(UID, "gaming_hiir"))
|
||||
assert not res["ok"] and res["reason"] == "owned"
|
||||
|
||||
def test_tier2_requires_level(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["balance"] = 100_000
|
||||
res = run(economy.do_buy(UID, "jellyfin"))
|
||||
assert not res["ok"] and res["reason"] == "level_required"
|
||||
fake_pb.record_for(UID)["exp"] = economy.exp_for_level(10)
|
||||
assert run(economy.do_buy(UID, "jellyfin"))["ok"]
|
||||
|
||||
def test_anticheat_repurchase_after_depletion(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
rec = fake_pb.record_for(UID)
|
||||
rec["balance"] = 10_000
|
||||
assert run(economy.do_buy(UID, "anticheat"))["ok"]
|
||||
assert fake_pb.record_for(UID)["item_uses"]["anticheat"] == 2
|
||||
fake_pb.record_for(UID)["item_uses"]["anticheat"] = 0
|
||||
assert run(economy.do_buy(UID, "anticheat"))["ok"]
|
||||
assert fake_pb.record_for(UID)["item_uses"]["anticheat"] == 2
|
||||
|
||||
|
||||
class TestGive:
|
||||
def test_transfer(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["balance"] = 500
|
||||
res = run(economy.do_give(UID, OTHER, 200))
|
||||
assert res["ok"]
|
||||
assert fake_pb.record_for(UID)["balance"] == 300
|
||||
assert fake_pb.record_for(OTHER)["balance"] == 200
|
||||
|
||||
def test_insufficient(self, fake_pb):
|
||||
res = run(economy.do_give(UID, OTHER, 50))
|
||||
assert not res["ok"] and res["reason"] == "insufficient"
|
||||
|
||||
def test_opposite_transfers_do_not_deadlock(self, fake_pb):
|
||||
async def both():
|
||||
for uid in (UID, OTHER):
|
||||
await economy.get_user(uid)
|
||||
fake_pb.record_for(uid)["balance"] = 100
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(
|
||||
economy.do_give(UID, OTHER, 10),
|
||||
economy.do_give(OTHER, UID, 25),
|
||||
),
|
||||
timeout=5,
|
||||
)
|
||||
run(both())
|
||||
total = fake_pb.record_for(UID)["balance"] + fake_pb.record_for(OTHER)["balance"]
|
||||
assert total == 200
|
||||
|
||||
|
||||
class TestRob:
|
||||
def test_anticheat_blocks_and_depletes(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
run(economy.get_user(OTHER))
|
||||
economy.set_house(HOUSE)
|
||||
fake_pb.record_for(UID)["balance"] = 1000
|
||||
target = fake_pb.record_for(OTHER)
|
||||
target["balance"] = 1000
|
||||
target["items"] = ["anticheat"]
|
||||
target["item_uses"] = {"anticheat": 1}
|
||||
res = run(economy.do_rob(UID, OTHER))
|
||||
assert res["ok"] and not res["success"] and res["reason"] == "valvur"
|
||||
assert fake_pb.record_for(UID)["balance"] == 1000 - res["fine"]
|
||||
assert "anticheat" not in fake_pb.record_for(OTHER)["items"]
|
||||
# the fine flows to the house
|
||||
assert fake_pb.record_for(HOUSE)["balance"] == res["fine"]
|
||||
|
||||
|
||||
class TestGambling:
|
||||
def test_roulette_conserves_money_with_house(self, fake_pb):
|
||||
economy.set_house(HOUSE)
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["balance"] = 1000
|
||||
random.seed(3)
|
||||
res = run(economy.do_roulette(UID, 100, "punane"))
|
||||
assert res["ok"]
|
||||
user_bal = fake_pb.record_for(UID)["balance"]
|
||||
if res["won"]:
|
||||
assert user_bal == 1000 + res["change"]
|
||||
else:
|
||||
assert user_bal == 900
|
||||
assert fake_pb.record_for(HOUSE)["balance"] == 100
|
||||
|
||||
def test_bet_larger_than_balance_rejected(self, fake_pb):
|
||||
res = run(economy.do_slots(UID, 50))
|
||||
assert not res["ok"] and res["reason"] == "insufficient"
|
||||
|
||||
def test_blackjack_bet_and_payout(self, fake_pb):
|
||||
economy.set_house(HOUSE)
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["balance"] = 500
|
||||
assert run(economy.do_blackjack_bet(UID, 100))["ok"]
|
||||
assert fake_pb.record_for(UID)["balance"] == 400
|
||||
# player loses: payout 0 of 100 invested -> house gains the bet
|
||||
run(economy.do_blackjack_payout(UID, 0, total_invested=100))
|
||||
assert fake_pb.record_for(UID)["balance"] == 400
|
||||
assert fake_pb.record_for(HOUSE)["balance"] == 100
|
||||
|
||||
|
||||
class TestConcurrency:
|
||||
"""The per-user locks must serialize read-modify-write cycles."""
|
||||
|
||||
def test_concurrent_exp_awards_are_not_lost(self, fake_pb):
|
||||
async def hammer():
|
||||
await asyncio.gather(*(economy.award_exp(UID, 10) for _ in range(25)))
|
||||
run(hammer())
|
||||
assert fake_pb.record_for(UID)["exp"] == 250
|
||||
|
||||
def test_concurrent_credit_house_is_atomic(self, fake_pb):
|
||||
economy.set_house(HOUSE)
|
||||
|
||||
async def hammer():
|
||||
await economy.get_user(HOUSE)
|
||||
await asyncio.gather(*(economy._credit_house(7) for _ in range(30)))
|
||||
run(hammer())
|
||||
assert fake_pb.record_for(HOUSE)["balance"] == 210
|
||||
86
tests/test_economy_pure.py
Normal file
86
tests/test_economy_pure.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Tests for the pure (no-database) economy math."""
|
||||
|
||||
import random
|
||||
from datetime import timedelta
|
||||
|
||||
from core import economy
|
||||
|
||||
|
||||
class TestLevels:
|
||||
def test_milestones(self):
|
||||
assert economy.get_level(0) == 1
|
||||
assert economy.get_level(249) == 4
|
||||
assert economy.get_level(250) == 5
|
||||
assert economy.get_level(1000) == 10
|
||||
assert economy.get_level(4000) == 20
|
||||
assert economy.get_level(9000) == 30
|
||||
|
||||
def test_exp_for_level_is_inverse(self):
|
||||
for level in range(1, 41):
|
||||
exp = economy.exp_for_level(level)
|
||||
assert economy.get_level(exp) == level
|
||||
if level > 1:
|
||||
assert economy.get_level(exp - 1) == level - 1
|
||||
|
||||
def test_negative_exp_clamps_to_level_1(self):
|
||||
assert economy.get_level(-500) == 1
|
||||
|
||||
def test_role_names(self):
|
||||
assert economy.level_role_name(1) == "TipiNOOB"
|
||||
assert economy.level_role_name(4) == "TipiNOOB"
|
||||
assert economy.level_role_name(5) == "TipiGRINDER"
|
||||
assert economy.level_role_name(10) == "TipiHUSTLER"
|
||||
assert economy.level_role_name(20) == "TipiCHAD"
|
||||
assert economy.level_role_name(30) == "TipiLEGEND"
|
||||
assert economy.level_role_name(99) == "TipiLEGEND"
|
||||
|
||||
|
||||
class TestGambleExp:
|
||||
def test_tiers(self):
|
||||
assert economy.gamble_exp(0) == 0
|
||||
assert economy.gamble_exp(9) == 0
|
||||
assert economy.gamble_exp(10) == 5
|
||||
assert economy.gamble_exp(99) == 5
|
||||
assert economy.gamble_exp(100) == 10
|
||||
assert economy.gamble_exp(999) == 10
|
||||
assert economy.gamble_exp(1_000) == 15
|
||||
assert economy.gamble_exp(9_999) == 15
|
||||
assert economy.gamble_exp(10_000) == 20
|
||||
assert economy.gamble_exp(99_999) == 20
|
||||
assert economy.gamble_exp(100_000) == 25
|
||||
|
||||
def test_cap(self):
|
||||
assert economy.gamble_exp(10_000_000) == 25
|
||||
|
||||
|
||||
class TestFormatTd:
|
||||
def test_hours(self):
|
||||
assert economy.format_td(timedelta(hours=1, minutes=23, seconds=45)) == "1t 23m"
|
||||
|
||||
def test_minutes(self):
|
||||
assert economy.format_td(timedelta(minutes=45, seconds=12)) == "45m 12s"
|
||||
|
||||
def test_seconds(self):
|
||||
assert economy.format_td(timedelta(seconds=8)) == "8s"
|
||||
|
||||
|
||||
class TestRollFish:
|
||||
def test_rolls_are_valid(self):
|
||||
random.seed(42)
|
||||
for _ in range(500):
|
||||
fish_id, weight = economy.roll_fish()
|
||||
if fish_id == "junk":
|
||||
assert weight == 0
|
||||
else:
|
||||
fish = economy.FISH_CATALOGUE[fish_id]
|
||||
assert fish["weight"][0] <= weight <= fish["weight"][1]
|
||||
|
||||
def test_rarity_bump_shifts_every_catch_up_a_tier(self):
|
||||
random.seed(7)
|
||||
rarities = {
|
||||
economy.FISH_CATALOGUE[fid]["rarity"]
|
||||
for fid, _ in (economy.roll_fish(rarity_bump=True) for _ in range(1000))
|
||||
if fid != "junk"
|
||||
}
|
||||
assert "common" not in rarities
|
||||
assert "legendary" in rarities
|
||||
116
tests/test_quests.py
Normal file
116
tests/test_quests.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Tests for the quest system: rotation, progress, claiming, schema detection."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from core import economy
|
||||
|
||||
from conftest import run
|
||||
|
||||
UID = 111
|
||||
|
||||
|
||||
def _fixed_now(monkeypatch, dt: datetime):
|
||||
monkeypatch.setattr(economy, "_now", lambda: dt)
|
||||
return dt
|
||||
|
||||
|
||||
def _complete_all_active_quests(fake_pb, user_id: int) -> tuple[int, int]:
|
||||
"""Push every active quest's tracked counter past its goal directly in the
|
||||
store. Returns (expected_coins, expected_exp)."""
|
||||
rec = fake_pb.record_for(user_id)
|
||||
coins = exp = 0
|
||||
for pool, block in (
|
||||
(economy.QUESTS_DAILY, rec["quest_daily"]),
|
||||
(economy.QUESTS_WEEKLY, rec["quest_weekly"]),
|
||||
):
|
||||
for qid, state in block["quests"].items():
|
||||
stat = pool[qid]["stat"]
|
||||
rec[stat] = state["snap"] + pool[qid]["goal"]
|
||||
coins += pool[qid]["coins"]
|
||||
exp += pool[qid]["exp"]
|
||||
return coins, exp
|
||||
|
||||
|
||||
class TestRotation:
|
||||
def test_deterministic_per_seed(self):
|
||||
a = economy._pick_quests(economy.QUESTS_DAILY, 3, "1:date:2026-07-26")
|
||||
b = economy._pick_quests(economy.QUESTS_DAILY, 3, "1:date:2026-07-26")
|
||||
assert a == b
|
||||
|
||||
def test_users_get_different_sets(self):
|
||||
sets = {
|
||||
tuple(economy._pick_quests(economy.QUESTS_DAILY, 3, f"{uid}:date:2026-07-26"))
|
||||
for uid in range(50)
|
||||
}
|
||||
assert len(sets) > 1
|
||||
|
||||
def test_counts(self, fake_pb):
|
||||
data = run(economy.get_quests(UID))
|
||||
assert len(data["daily"]) == economy.DAILY_QUEST_COUNT
|
||||
assert len(data["weekly"]) == economy.WEEKLY_QUEST_COUNT
|
||||
|
||||
def test_daily_rolls_over_weekly_stays(self, fake_pb, monkeypatch):
|
||||
# a Tuesday, so day+1 stays inside the same ISO week
|
||||
t0 = _fixed_now(monkeypatch, datetime(2026, 7, 21, 12, tzinfo=timezone.utc))
|
||||
run(economy.get_quests(UID))
|
||||
rec = fake_pb.record_for(UID)
|
||||
daily_before, weekly_before = dict(rec["quest_daily"]), dict(rec["quest_weekly"])
|
||||
_fixed_now(monkeypatch, t0 + timedelta(days=1))
|
||||
run(economy.get_quests(UID))
|
||||
rec = fake_pb.record_for(UID)
|
||||
assert rec["quest_daily"]["date"] != daily_before["date"]
|
||||
assert rec["quest_weekly"] == weekly_before
|
||||
|
||||
|
||||
class TestProgress:
|
||||
def test_progress_tracks_counter_delta(self, fake_pb):
|
||||
run(economy.get_quests(UID))
|
||||
rec = fake_pb.record_for(UID)
|
||||
qid, state = next(iter(rec["quest_daily"]["quests"].items()))
|
||||
stat = economy.QUESTS_DAILY[qid]["stat"]
|
||||
rec[stat] = state["snap"] + 1
|
||||
data = run(economy.get_quests(UID))
|
||||
quest = next(q for q in data["daily"] if q["id"] == qid)
|
||||
assert quest["progress"] == 1
|
||||
|
||||
def test_pre_roll_stats_do_not_count(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["work_count"] = 500
|
||||
data = run(economy.get_quests(UID))
|
||||
assert all(q["progress"] == 0 for q in data["daily"] + data["weekly"])
|
||||
|
||||
|
||||
class TestClaim:
|
||||
def test_claim_pays_and_is_idempotent(self, fake_pb):
|
||||
run(economy.get_quests(UID))
|
||||
coins, exp = _complete_all_active_quests(fake_pb, UID)
|
||||
res = run(economy.claim_quests(UID))
|
||||
assert res["ok"]
|
||||
assert res["claimed"] == economy.DAILY_QUEST_COUNT + economy.WEEKLY_QUEST_COUNT
|
||||
assert res["coins"] == coins and res["exp"] == exp
|
||||
assert fake_pb.record_for(UID)["balance"] == coins
|
||||
res = run(economy.claim_quests(UID))
|
||||
assert not res["ok"] and res["reason"] == "nothing"
|
||||
|
||||
def test_claim_with_nothing_done(self, fake_pb):
|
||||
run(economy.get_quests(UID))
|
||||
res = run(economy.claim_quests(UID))
|
||||
assert not res["ok"]
|
||||
|
||||
|
||||
class TestSchemaDetection:
|
||||
def test_missing_quest_fields_warn_and_zero_progress(
|
||||
self, fake_pb_without_quest_fields, caplog
|
||||
):
|
||||
"""Reproduces the live 'quests never progress' symptom: when the
|
||||
collection schema lacks quest_daily/quest_weekly, PocketBase drops the
|
||||
rolled quest block, so every call re-rolls with a fresh snapshot."""
|
||||
fake = fake_pb_without_quest_fields
|
||||
with caplog.at_level(logging.WARNING):
|
||||
run(economy.get_quests(UID))
|
||||
assert any("quest fields" in r.message for r in caplog.records)
|
||||
# counters advance, but progress stays 0 because the snapshot re-rolls
|
||||
fake.record_for(UID)["work_count"] = 500
|
||||
data = run(economy.get_quests(UID))
|
||||
assert all(q["progress"] == 0 for q in data["daily"] + data["weekly"])
|
||||
Reference in New Issue
Block a user