Files
tipibot/CLAUDE.md
Rene Arumetsa 7a28617657 docs: add CLAUDE.md guidance for Claude Code
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
2026-09-04 02:09:33 +03:00

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 exposes register_<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 every register_*_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 in strings/{common,commands,member,economy,admin,games,fishing}.py; economy logic in core/economy/{store,income,gambling,fishing,jail,heist,prestige,shop,levels,quests,leaderboards,house,admin}.py.
  • tests/test_strings.py guards 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-exported economy.house mutable state is a snapshot; live house state is owned by economy.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 the UserData dict → 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:
    1. A @_locked_by function must never call another @_locked_by function.
    2. 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.
  • 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 running python scripts/sync_pb_schema.py (reconciles both dev + prod collections). store.missing_schema_fields() / /status surface 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.