پرش به محتویات

توسعه

راه‌اندازی

git clone https://github.com/Emadhabibnia1385/KasbBook.git
cd KasbBook
python3 -m venv venv
./venv/bin/pip install -r requirements-dev.txt
./venv/bin/pip install --no-deps -e .
./venv/bin/python -m pytest tests -q

نصب ویرایش‌پذیر همان چیزی است که kasbbook را قابل واردکردن می‌کند، چون نصب شده، نه چون چیزی اول sys.path را مرتب کرده. آن مرتب‌کردن سه بار شکسته — عیب‌یابی را ببین.

مجموعه روی SQLite و در همین پروسه اجرا می‌شود: بدون شبکه، بدون توکن ربات، بدون سرور دیتابیس، و بدون چیزی که بیرون از همین چک‌اوت دست دراز کند. یک کلون و وابستگی‌های توسعه کل تنظیمات لازم است، و CI همان است که می‌گوید پاس می‌شود.

اینجا عددی روی کرنومتر نوشته بود — «حدود چهل ثانیه» — و پوسیده بود. همان راهی را رفت که نشان شمار تست‌ها در README در 1100f63 رفت، و به همان دلیل: عددِ اندازه‌گیری‌شده در متن قولی است که تا ابد دوباره اندازه‌اش بگیری. زمان اجرا بی‌صداتر از شمارش می‌پوسد، چون اجرا کردنِ خود مجموعه یادآورش نمی‌شود.

شش تست مهاجرت رد می‌شوند مگر KASBBOOK_TEST_POSTGRES_URL تنظیم باشد — همین است که می‌گذارد لپ‌تاپی بدون پستگرس بقیه را اجرا کند.

export KASBBOOK_TEST_POSTGRES_URL=postgresql+asyncpg://kasbbook:pw@127.0.0.1:5435/kasbbook_test
./venv/bin/python -m pytest tests/test_migrations_postgres.py -q

بررسی کد

./venv/bin/python -m pyflakes src/kasbbook apps tests migrations/comparators.py

فقط pyflakes — نه قالب‌بند، نه مرتب‌کنندهٔ واردات. همان چیزهایی را می‌گیرد که واقعاً ایرادند: نام‌های تعریف‌نشده، واردات بلااستفاده، کد دست‌نیافتنی.

توجه کن که pyflakes به # noqa اعتنا نمی‌کند. وقتی واردات‌ها برای اثر جانبی هستند — مثل models.py که تولید خودکار Alembic فقط چیزی را می‌بیند که وارد شده — با __all__ بگو که خود لینتر می‌خواندش، نه با توضیحی که نمی‌خواند.

قراردادها

توضیح‌ها «چرا» را می‌گویند، نه «چه». # شمارنده را زیاد کن بالای count += 1 نویز است. # ارسال پیش از تلاش ثبت می‌شود: یادآور تکراری بدتر از یادآور ازدست‌رفته است دلیلی است که نفر بعدی «درستش» نمی‌کند.

تست‌ها به‌شکل جمله نام‌گذاری می‌شوند. test_reusing_a_spent_token_signs_the_whole_family_out فقط از خط شکست می‌گوید چه چیزی خراب شده.

تاریخ‌ها میخ‌کوب می‌شوند. DAY = date(2026, 8, 24)، هرگز date.today(). تستی که ساعت واقعی را می‌خواند یک سه‌شنبهٔ آینده به دلایلی مردود می‌شود که هیچ‌کس به کد ربطشان نمی‌دهد.

پول Decimal است. هرگز اعشاری ممیز شناور، هیچ‌جا، حتی در تست.

زبان رابط کاربری فارسی است. صفحه‌ها، خطاهایی که کاربر می‌بیند. کد، توضیح‌ها، موضوع کامیت‌ها و همین مستندات انگلیسی‌اند.

ردِ خود سرویس، اگر فارسی باشد، متنی است که آدم می‌خواند. screens.user_message() پیامی را که از قبل فارسی دارد دست‌نخورده به آدم رد می‌کند، پس جمله‌ای که در modules/<area>/service.py بالا انداخته می‌شود به همان قواعد واژگان و رقم و نیم‌فاصلهٔ یک صفحه پایبند است. دو نگهبان این قواعد را اعمال می‌کنند: tests/test_bot_ux_fixes.py روی bot/، و tests/test_copy_rules.py روی modules/ و shared/، که اولین آرگومان هر خطایی را می‌خواند که آنجا بالا انداخته شده. راه دیگر این است که انگلیسی بالا بیندازی‌اش و در screens/common.py نقشه‌اش کنی، که جای خطایی است که API هم با آن جواب می‌دهد.

افزودن یک امکان

شکلی که بیشتر امکان‌ها می‌گیرند:

  1. مدل — modules/<area>/models.py، ثبت‌شده در src/kasbbook/models.py تا تولید خودکار ببیندش.
  2. مهاجرت — alembic revision --autogenerate. آنچه تولید کرده را بخوان؛ پیش‌نویس است نه تصمیم. برای هر ستون NOT NULL روی جدولی که از قبل سطر دارد server_default اضافه کن.
  3. سرویس — modules/<area>/service.py. هر قاعده اینجا زندگی می‌کند، و با await self.books.require(...) شروع می‌شود.
  4. صفحه‌ها — ماژول همان بخش در bot/screens/. صفحه‌ها توابع خالص‌اند. کد مشترک فقط در screens/common.py می‌رود. هر صفحهٔ تازه را صریحاً از screens/__init__.py export کن. پیشوند کال‌بکی بردار که گرفته نشده باشد؛ تست نگهبان می‌گوید اگر شده باشد. هر payload را حداکثر ۶۴ بایت نگه دار. بعد tests/test_screens_architecture.py را اجرا کن.

صفحه‌کلید را از back_row()، cancel_row() و rows_of() بساز، نه با نوشتن دستی نشانه‌ها و تودرتویی ردیف‌ها. این مرتب‌کاری نیست: ردیف‌های دست‌نویس همان چیزی بود که «انصراف» را بسته به اینکه در کدام بخش باشی به دو شکل نوشت. دستور زبان چیدمانی که این‌ها نگه می‌دارند — کار اصلی تنها، هم‌ترازها دوتایی، کار مخرب در ردیف خودش، بازگشت یا انصراف آخر، هرگز بیش از سه تا در یک ردیف — در طراحی گفتگو است. ۵. مسیریابی — بخشی در bot/features/ که صاحب آن حوزه است: پیشوند را به prefixes آن اضافه کن (و اگر متن می‌خواند، جریان پیش‌نویس را به flows آن). ۶. API — یک مسیریاب در api/routers/، که همان سرویس را صدا می‌زند. ۷. تست‌ها — دست‌کم: مسیر خوش، یک رد، و یک تست جداسازی که ثابت کند حساب دیگری به آن نمی‌رسد.

مهاجرت‌ها

export KASBBOOK_DATABASE_URL="sqlite+aiosqlite:///scratch.db"
./venv/bin/alembic upgrade head
./venv/bin/alembic revision --autogenerate -m "what changed"

تولید خودکار را در برابر دیتابیسی اجرا کن که از قبل روی سر است، وگرنه می‌خواهد همه‌چیز را بسازد.

مهاجرت‌ها هیچ واردات برنامه‌ای ندارند — render_item در env.py به‌جای kasbbook.shared.money.Money مقدار sa.Numeric(28, 4) را منتشر می‌کند، پس مهاجرت سال‌ها بعد هم که کلاس جابه‌جا شده باشد اعمال می‌شود. تستی این را اعمال می‌کند.

اگر تولید خودکار برای ستون‌های پولی تغییر نوع گزارش داد، compare_type در migrations/comparators.py خراب است. آن تفاوت‌ها ظاهری نیستند: روی پستگرس هر کدام یک ALTER COLUMN TYPE است که کل جدول را به همان نوعی که دارد بازنویسی می‌کند.

تست‌های نگهبان

بعضی تست‌ها هستند تا ردهٔ اشتباه را بگیرند نه نمونه‌اش را. ارزش دارد پیش از برخورد با یکی‌شان بدانی‌شان:

تست چه چیزی را رد می‌کند
گراف واردات آداپتور آداپتوری که SQLAlchemy یا سرویسی وارد کند
تصادم پیشوند کال‌بک دو امکان که با یک پیشوند مسیریابی شوند
دکمه‌های بن‌بست پیشوندی که صفحه‌ای منتشرش می‌کند و چیزی مسیریابی‌اش نمی‌کند
پوشش متدهای خروجی متد آداپتوری که موردی برای تست ندارد
واردات مهاجرت مهاجرتی که کد برنامه را وارد کند
انحراف مدل و مهاجرت مدلی که بدون مهاجرت عوض شده
نقطهٔ ورود اسکریپت شکست runner.py وقتی مثل systemd به‌شکل اسکریپت اجرا شود

آخری هست چون واردکردن یک ماژول و اجرایش به‌عنوان اسکریپت دو چیز متفاوت‌اند، و همان تفاوت یک بار ربات را خواباند.

سبک تست

از همان درزی تست کن که کاربر از آن رد می‌شود. تست‌های گفتگو یک IncomingEvent می‌سازند و روی پاسخ ادعا می‌کنند — همان مسیری که فشار دکمه می‌رود. تست‌های API از ASGITransport روی خود برنامهٔ واقعی استفاده می‌کنند. فقط تست‌های آداپتور ساختگی‌سازی می‌کنند، و آن هم فقط روی HTTP.

روی حساب‌وکتاب ادعا کن، نه فقط نبودن خطا. تست حقوقی که net_pay == Decimal("42000000") را بررسی می‌کند فرمول سهم غلط را می‌گیرد. آن که فقط وجود فیش را بررسی می‌کند نمی‌گیرد.

تست‌های جداسازی اختیاری نیستند. هر امکان یکی می‌گیرد که ثابت کند حساب دوم به دادهٔ حساب اول نمی‌رسد. این همان ردهٔ ایرادی است که بیشترین اهمیت و کمترین حضور را در تست دستی دارد.

چیدمان مخزن

src/kasbbook/     خود بسته: adapters، bot، api، modules، shared
apps/bot/         نقطهٔ ورود قابل‌اجرا و حلقهٔ یادآور
migrations/       محیط alembic، مقایسه‌گرها، نسخه‌ها
deploy/           فایل compose و دو یونیت systemd
scripts/          نصب، به‌روزرسانی، پشتیبان، بازگردانی، حذف
tests/            همه‌چیز
docs/             همین فایل‌ها

سایت مستندات

docs/ هم چیزی است که گیت‌هاب نشان می‌دهد و هم چیزی که سایت منتشرشده از رویش ساخته می‌شود، پس یک نسخه بیشتر نیست و نمی‌تواند در یک جا درست و در جای دیگر کهنه باشد.

./venv/bin/pip install "mkdocs-material>=9.5,<10" "mkdocs-static-i18n>=1.2"
./venv/bin/mkdocs serve      # http://127.0.0.1:8000، با ویرایش بازخوانی می‌شود
./venv/bin/mkdocs build --strict

--strict همان است که یکپارچه‌سازی پیوسته اجرا می‌کند: پیوند داخلی شکسته ساخت را می‌شکند به‌جای اینکه منتظر بماند خواننده‌ای پیدایش کند. افزودن صفحه یعنی افزودنش به nav در mkdocs.yml — فایلی که در nav نیست باز هم ساخته می‌شود، اما چیزی به آن پیوند نمی‌دهد.

سایت دوزبانه است: page.md انگلیسی و page.fa.md فارسی. صفحه‌ای که ترجمهٔ فارسی ندارد به انگلیسی برمی‌گردد، نه به ۴۰۴ — پس افزودن صفحهٔ تازه به انگلیسی هرگز سایت فارسی را نمی‌شکند.

کامیت‌ها

موضوع انگلیسی، به وجه امری، که می‌گوید برای کاربر یا اپراتور چه چیزی عوض شده. بدنه چرا را توضیح می‌دهد — به‌ویژه وقتی تغییر کوچک است و دلیلش آشکار نیست.

کامیتی که ایرادی را درست می‌کند باید بگوید آن ایراد چه شکلی بود. نیمی از عیب‌یابی از بدنهٔ کامیت‌ها نوشته شده.