Architecture overview, dev/test commands, the dual-profile and re-export patterns, the economy locking/PocketBase-schema footguns, and the test harness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VbAVsrZuYesea99mPMmPT
5.6 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
Discord bot for the TipiLAN community (discord.py). Two responsibilities: member management (syncs nicknames/roles from a Google Sheet, birthday announcements) and the TipiCOIN economy (a large game economy stored in PocketBase). User-facing text is Estonian; only docs/PATCHNOTES.md content is English.
See README.md for the full feature/gameplay spec and docs/DEV_NOTES.md for the developer reference (add-a-command / add-a-shop-item checklists, constants).
Commands
# Run tests (pure logic + economy flows against an in-memory PocketBase fake)
python -m pytest tests/ -q
# Single file / single test
python -m pytest tests/test_economy_pure.py -q
python -m pytest tests/test_economy_flows.py::TestDaily::test_streak -q
# Run the bot (requires .env; PocketBase must already be running)
BOT_PROFILE=dev python bot.py # or BOT_PROFILE=economy
Deps: pip install -r requirements-dev.txt (dev = runtime + pytest). CI (.gitea/workflows/deploy.yml) runs pytest tests/ -q on push to master, then deploys to the host by restarting tipibot_dev (canary) and tipibot_eco systemd units. The bot runs on host tipilan-bots:/root/tipibot; the local checkout has no .env/live DB.
Architecture
Three layers, deliberately decoupled:
core/— domain logic, no Discord objects.core/economy/is the economy package.commands/— one Discord slash-command group per file. Each exposesregister_<group>_commands(tree, bot, ...).bot.py— thin wiring layer: Discord client, event handlers (on_ready,on_member_join), background tasks, shared helpers, and it calls everyregister_*_commands(...)on startup, passing shared helpers down (coin,cd_ts,award_exp,maybe_remind,parse_amount).
Dual-profile design (dev vs economy)
BOT_PROFILE env var (dev | economy) selects everything at import time in config.py: which Discord token, guild ID, birthday channel, and PocketBase collection (economy_users_dev vs economy_users_prod) are used. Legacy non-suffixed env vars act as fallbacks. Logs and data/ are also namespaced per profile (logs/<profile>/, data/<profile>/). /check, /member, /birthdays are dev-profile only. When adding config, follow the _DEV / _ECONOMY + legacy-fallback pattern.
The re-export pattern (important — two places)
Both strings/ and core/economy/ are packages split into submodules but re-exported flat through their __init__.py, so callers use import strings as S; S.NAME and from core import economy; economy.do_daily(...) unchanged.
- Edit the submodule, not the
__init__. Strings live instrings/{common,commands,member,economy,admin,games,fishing}.py; economy logic incore/economy/{store,income,gambling,fishing,jail,heist,prestige,shop,levels,quests,leaderboards,house,admin}.py. tests/test_strings.pyguards that every submodule name is re-exported — a new string that isn't re-exported fails CI.- Caveat (see
tests/conftest.py:93): the re-exportedeconomy.housemutable state is a snapshot; live house state is owned byeconomy.house. Patch the submodule, not the alias.
Economy persistence & concurrency (core/economy/store.py)
- Every mutation is a read-modify-write:
get_user(id)→ mutate theUserDatadict →await _commit(id, user). Money/EXP changes must also call_txn(...)for the transaction log. - Per-user async locks serialize commits. Public mutating functions are decorated with
@_locked_by(<arg positions of user ids>). Two hard rules to avoid deadlock/lost-writes:- A
@_locked_byfunction must never call another@_locked_byfunction. - House balance changes go through
_credit_house(an atomic PocketBase increment, no lock) so they're safe to call while holding user locks. Multi-user functions (do_give,do_rob) list both positions and acquire locks in sorted order.
- A
- PocketBase silently drops writes to fields not in the collection schema — this is the #1 footgun. Adding any new persisted field means adding it to
_default_user()and runningpython scripts/sync_pb_schema.py(reconciles both dev + prod collections).store.missing_schema_fields()//statussurface drift.
Command result convention
core.economy functions return a dict with a "reason" key describing the outcome (success, "cooldown", "jailed", "banned", insufficient funds, ...). The command handler in commands/ maps every possible res["reason"] to a string/embed. On success it calls award_exp(interaction, economy.EXP_REWARDS["<cmd>"]) and maybe_remind(...) where relevant.
Adding an economy command or shop item
Follow the exact ordered checklists in docs/DEV_NOTES.md ("Adding a New Economy Command", "Adding a New Shop Item"). They enumerate every touchpoint across core/economy/, strings/, commands/, and bot.py (cooldowns, EXP rewards, help embed, reminders, item-modified cooldown branches). Missing a step generally means a silently broken feature rather than an error.
Tests
No pytest-asyncio. Async tests wrap coroutines with the run(coro) helper from tests/conftest.py (which is asyncio.run). The fake_pb fixture monkeypatches core.pb_client with an in-memory FakePocketBase that mimics real PocketBase behaviour — including atomic field+/field- increments and silently dropping writes to fields missing from the schema (use fake_pb_without_quest_fields to simulate pre-migration schema drift). Prefer testing core/ logic directly; there's no Discord in the test path.