Files
clinicpro/.claude/skills/qa-clinicpro/SKILL.md
T

531 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: qa-clinicpro
description: تست QA اپلیکیشن ClinicPro مثل یک کاربر واقعی — ابتدا ساخت همهٔ نقش‌ها و پروفایل‌های کامل (پزشک مستقل، پزشک عضو کلینیک، کلینیک، منشی، نماینده، بیمار، …) و تعیین ماتریس سطح دسترسی، سپس تست ماتریس دسترسی با تک‌تک آن‌ها. هر مانعی سر راه تست را مثل یک دولوپر ارشد Symfony/React خودش رفع می‌کند و تست را ادامه می‌دهد. اجرای اپ، ورود با هر نقش، پیمایش صفحات پنل ادمین، اسکرین‌شات، کشف خطاهای کنسول و شبکه، تست UI/UX و RTL، تست دسترسی نقش‌ها (authz)، تست قرارداد API و اندازه‌گیری کارایی، و تولید Bug Report. Use when asked to QA, test, smoke-test, find bugs in, screenshot, or verify ClinicPro's admin panel or API — «تست کن»، «باگ پیدا کن»، «QA کن»، «این صفحه را بررسی کن».
---
# QA ClinicPro
ClinicPro = بک‌اند Symfony 7.4 + یک **SPA کلاینت‌ساید React 19** که از `/admin/*` سرو می‌شود.
یعنی `curl` و فلگ `--screenshot` کروم به درد نمی‌خورند — هر دو روی فرم لاگین می‌نشینند،
چون JWT در `localStorage['clinicpro-auth']` است.
درایور این skill آن کار را انجام می‌دهد: با API لاگین می‌کند، `localStorage` را seed
می‌کند، بعد ناوبری می‌کند و **خطاهای کنسول، درخواست‌های شکست‌خورده، مسیری که واقعاً روی آن
فرود آمده، و اسکرین‌شات** را گزارش می‌دهد — با CDP روی `WebSocket` نیتیو Node 22،
**بدون هیچ وابستگی npm** (نه playwright، نه puppeteer).
مسیرها نسبت به `clinicpro/` هستند.
## پیش‌نیازها
هیچ نصبی لازم نیست. فقط این دو:
```bash
ddev describe | head -3 # باید بالا باشد: https://clinic-pro.ddev.site
ls "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
```
کروم جای دیگری است؟ `CHROME_BIN` را ست کن. بک‌اند جای دیگری است؟ `CLINICPRO_BASE`.
## کاربران تست
**`TEST_USERS.md` منسوخ است** — هیچ‌کدام از کاربرانش (`09100000001`, `09100100000`, …)
در دیتابیس وجود ندارند و همه `ERR_AUTH_005` می‌گیرند. اسکریپت‌های `create_test_users.php`
و `seed_realistic_data.php` هم که آن فایل ارجاع می‌دهد در ریپو نیستند.
پرسوناهای QA در `ROLES` داخل درایور تعریف شده‌اند. **واحد کار «پرسونا» است، نه
`ROLE_*`** — پزشک مستقل و پزشک عضو کلینیک هر دو `ROLE_DOCTOR` دارند ولی دادهٔ متفاوتی
می‌بینند، پس هرکدام یک ردیف جداگانه‌اند.
| پرسونا | موبایل | پسورد | نقش‌ها | تمایز |
|---|---|---|---|---|
| `admin` | `09120671756` | `QaTest@1234` | `ROLE_ADMIN` | — |
| `clinic` | `09127000000` | `QaTest@1234` | `ROLE_CLINIC` | مالک کلینیک |
| `secretary` | `09123456778` | `QaTest@1234` | `ROLE_SECRETARY` | منشیِ یک پزشک |
| `doctor` | `09390039833` | `09390039833` | `ROLE_DOCTOR` | حساب قدیمی، وضعیت عضویتش نامعلوم |
| `representation` | `09124000001` | `09124000001` | `ROLE_REPRESENTATION` | نماینده شهر |
| `doctor_solo` | `09129000001` | `QaTest@1234` | `ROLE_DOCTOR` | **پزشک مستقل** — مطب شخصی، بدون کلینیک |
| `doctor_member` | `09129000002` | `QaTest@1234` | `ROLE_DOCTOR` | پزشک **عضو کلینیک** |
| `clinic_doctor` | `09129000003` | `QaTest@1234` | `ROLE_CLINIC`+`ROLE_DOCTOR` | چندنقشی |
| `secretary_clinic` | `09129000004` | `QaTest@1234` | `ROLE_SECRETARY` | منشیِ کلینیک (نه پزشک) |
| `unclaimed_doctor` | `09129000005` | `QaTest@1234` | `ROLE_UNCLAIMED_DOCTOR` | پروفایل ایمپورت‌شدهٔ تصاحب‌نشده |
| `patient` | `09129000006` | `QaTest@1234` | `ROLE_USER` | کاربر عادی سایت |
| `importer` | `09129000007` | `QaTest@1234` | `ROLE_IMPORTER` | — |
پنج ردیف اول موجودند. **هفت ردیف آخر تا وقتی Phase 0 اجرا نشده وجود ندارند** و
`driver.mjs roles` برایشان `✗` می‌دهد — این دقیقاً چک آمادگی است.
اگر DB ریست شد، پسورد پنج‌تای اول را دوباره ست کن:
```bash
ddev exec php bin/console security:hash-password 'QaTest@1234'
# هش خروجی را در این کوئری بگذار:
ddev mysql -e "UPDATE users SET password_hash='<هش>' \
WHERE mobile_number IN ('09120671756','09127000000','09123456778');"
```
اعتبارسنجی همه نقش‌ها:
```bash
node .claude/skills/qa-clinicpro/driver.mjs roles
```
خروجی واقعی:
```
admin 09120671756 ROLE_USER,ROLE_ADMIN token 15min
clinic 09127000000 ROLE_USER,ROLE_CLINIC token 15min
secretary 09123456778 ROLE_USER,ROLE_SECRETARY token 15min
doctor 09390039833 ROLE_USER,ROLE_DOCTOR token 15min
representation 09124000001 ROLE_USER,ROLE_REPRESENTATION token 15min
```
می‌توانی به‌جای نام نقش، `--as "0912xxxxxxx:password"` هم بدهی.
---
## مسیر اجرا (agent path)
### ۰. Phase 0 — ساخت نقش‌ها، پروفایل‌ها و ماتریس دسترسی (اجباری، قبل از هر تست)
هیچ تستی را قبل از تمام‌شدن این فاز شروع نکن. خروجی این فاز سه چیز است:
**همهٔ پرسوناها موجود** · **پروفایل هرکدام کامل** · **ماتریس دسترسی مکتوب**.
**۰.۱ — کشف نقش‌ها.** لیست بالا را دوباره از روی کد بساز، به آن استناد نکن؛ ممکن است
نقشی اضافه شده باشد:
```bash
grep -rhoE "ROLE_[A-Z_]+" src/ assets/admin/ config/ | sort -u
ddev mysql -e "SELECT roles, COUNT(*) c FROM users GROUP BY roles ORDER BY c DESC;"
grep -n "role_hierarchy" -A 10 config/packages/security.yaml
```
هر نقشی که در کد هست و در جدول پرسوناها نیست را به `ROLES` در `driver.mjs` اضافه کن.
**۰.۲ — چک آمادگی.** ببین کدام پرسونا هنوز نیست:
```bash
node .claude/skills/qa-clinicpro/driver.mjs roles
```
**۰.۳ — ساخت پرسوناهای ناموجود.** برای هرکدام، **اول مسیر واقعی ساخت را در خود اپ پیدا
کن** و از همان استفاده کن — دست‌کاری مستقیم SQL پروفایل ناقص می‌سازد و تست را دروغین
می‌کند. به این ترتیب بگرد:
```bash
ls src/*/Command/ # آیا کامند کنسولی برای ساخت کاربر هست؟
grep -rn "IsGranted" src/Admin/Controller/ # اندپوینت‌های ادمینِ ساخت کاربر
sed -n '1,80p' docs/api/admin.md
```
فقط برای چیزی که هیچ مسیر اپلیکیشنی ندارد (مثلاً ست‌کردن `ROLE_IMPORTER` یا ساختن
`ROLE_UNCLAIMED_DOCTOR`) به `ddev mysql` برگرد، و در گزارش بنویس که کدام پرسونا
دستی ساخته شد.
**ترتیب ساخت مهم است** — وابستگی دارند:
```
کلینیک → doctor_member (عضو همان کلینیک) → secretary_clinic (منشیِ همان کلینیک)
پزشک → secretary (منشیِ همان پزشک)
```
**۰.۴ — کامل‌کردن پروفایل.** یک حسابِ بدون پروفایل، صفحات را خالی نشان می‌دهد و
باگ‌های واقعی را پنهان می‌کند. برای هر پرسونا این‌ها باید پر باشند:
| پرسونا | حداقل پروفایل لازم |
|---|---|
| `doctor_solo` / `doctor_member` / `clinic_doctor` | نام، تخصص، آدرس مطب، برنامهٔ کاری هفتگی، حداقل یک خدمت با تعرفه، حداقل یک بیمه |
| `clinic` | نام کلینیک، شهر، آدرس، حداقل یک پزشک عضو، حداقل یک خدمت |
| `secretary` / `secretary_clinic` | اتصال به پزشک/کلینیک + سطح دسترسی‌اش |
| `representation` | شهر تخصیص‌یافته |
| `patient` | نام، و حداقل یک نوبت رزروشده (برای اینکه صفحات خالی نباشند) |
| `unclaimed_doctor` | پروفایل پزشک بدون کاربرِ تصاحب‌کننده |
بعد از ساخت، پرشدن را تأیید کن — نه با حدس، با درخواست:
```bash
node .claude/skills/qa-clinicpro/driver.mjs api GET /api/v1/doctor/profile --as doctor_solo
```
**۰.۵ — تعیین سطح دسترسی.** ماتریس را از کد دربیاور، نه از ذهنت:
```bash
grep -n "RoleRoute\|allowedRoles\|element=" assets/admin/App.tsx # مسیرهای فرانت
grep -rn "IsGranted" src/*/Controller/ | sed 's/.*IsGranted(//' # گاردهای بک‌اند
```
از این دو، جدول `مسیر → نقش‌های مجاز` را بساز و در گزارش بیاور. بعد برای هر اندپوینت
حساس با `authz` (بخش ۳) تأییدش کن. **اختلاف بین ماتریسِ کد و خروجی `authz` = باگ**،
حتی اگر خروجی `authz` سخت‌گیرانه‌تر باشد.
**۰.۶ — دروازهٔ خروج.** تا وقتی `roles` برای همهٔ پرسوناها توکن برمی‌گرداند و ماتریس
نوشته شده، به فاز بعد نرو. اگر پرسونایی ساخته نشد، طبق بخش «وقتی به مانع خوردی»
خودت رفعش کن؛ رها کردنش یعنی آن نقش اصلاً تست نشده.
### ۱. بازدید از صفحه — اسکرین‌شات + خطاها
```bash
node .claude/skills/qa-clinicpro/driver.mjs visit \
"https://clinic-pro.ddev.site/admin/dashboard" --as admin --out /tmp/qa-dash.png
```
```
✓ screenshot /tmp/qa-dash.png (1440x900, as admin)
LANDING
(none)
CONSOLE ERRORS
(none)
NETWORK FAILURES
(none)
```
**بعد حتماً تصویر را با ابزار Read باز کن و نگاه کن.** نیمی از باگ‌های UI فقط دیدنی‌اند،
نه لاگ‌شدنی — همان یک اسکرین‌شات داشبورد دو باگ i18n لو داد (پایین را ببین).
فلگ‌ها: `--w 1440 --h 900` (ویوپورت)، `--wait 4000` (ms صبر برای رندر)، `--full` (کل صفحه).
**موبایل را جدا تست کن** — پنل RTL و پرجدول است و بیشتر مشکلات آنجاست:
```bash
node .claude/skills/qa-clinicpro/driver.mjs visit \
"https://clinic-pro.ddev.site/admin/dashboard" --as admin --w 390 --h 844 --out /tmp/qa-m.png
```
بخش `LANDING` دو حالتی را می‌گیرد که اسکرین‌شات پنهان می‌کند:
```
⚠ WRONG PAGE: asked /admin/users, landed /admin/dashboard — role likely lacks access (RoleRoute in App.tsx)
```
### ۲. آدیت UI/UX و RTL
```bash
node .claude/skills/qa-clinicpro/driver.mjs ux \
"https://clinic-pro.ddev.site/admin/dashboard" --as admin --w 390 --h 844
```
```
UX FINDINGS (390x844, as admin)
5 tap target(s) under 36px on a mobile viewport
```
چک‌ها: RTL نبودن ریشه، `lang` غلط، سرریز افقی، رقم لاتین داخل متن فارسی، تارگت لمسی
زیر ۳۶px، `<img>` بدون alt، فیلد بدون label، `id` تکراری، جدول خالی بدون empty-state،
و `<select>` نیتیو (استاندارد پروژه `SearchableSelect` است).
### ۳. تست دسترسی نقش‌ها (Security)
همان درخواست با همه نقش‌ها + ناشناس:
```bash
node .claude/skills/qa-clinicpro/driver.mjs authz GET /api/v1/admin/users
```
```
AUTHZ GET /api/v1/admin/users
anonymous 401
admin 200
clinic 403
secretary 403
doctor 403
representation 403
200 for: admin
```
هر ۲۰۰ غیرمنتظره در این جدول = یک باگ Critical. اگر `anonymous` هم ۲۰۰ گرفت، درایور
هشدار می‌دهد.
#### ۳.۱ جاروی کامل ماتریس — اجباری، نه نمونه‌ای
`authz` خودش همهٔ پرسوناها را می‌زند، پس **تست دسترسی نباید روی چند اندپوینت منتخب
بماند**. لیست اندپوینت‌ها را از روتر بگیر و همه را جارو کن:
```bash
ddev exec php bin/console debug:router --format=json \
| node -e 'const r=JSON.parse(require("fs").readFileSync(0));
for (const [n,v] of Object.entries(r))
if (v.path.startsWith("/api/v1") && !v.path.includes("{"))
console.log(v.method.split("|")[0].replace("ANY","GET"), v.path);' \
| while read m p; do
node .claude/skills/qa-clinicpro/driver.mjs authz "$m" "$p"
done | tee /tmp/qa-authz-matrix.txt
```
روی این DB حدود **۱۶۸ مسیر بدون پارامتر** برمی‌گردد و `authz` برای هر مسیر به‌ازای هر
پرسونا دوباره لاگین می‌کند (≈۲۲۰۰ درخواست) — چند دقیقه طول می‌کشد، پس در پس‌زمینه
اجرایش کن و بعد فایل را بخوان. دو تله در خواندن خروجی:
- **۴۲۲ روی مسیرهای POST طبیعی است** (بدنه خالی فرستاده شده) و باگ نیست؛ چیزی که مهم
است تمایز ۴۰۱/۴۰۳ از بقیه است. اگر نقشی به‌جای ۴۰۳ یک ۴۲۲ گرفت، یعنی **گارد بعد از
اعتبارسنجی اجرا شده** — همان هم یافته است.
- **مسیرهای عمومی** (لاگین، ثبت‌نام، لیست شهرها) قاعدتاً برای `anonymous` هم ۲۰۰‌اند؛
اول با `config/packages/security.yaml` تطبیق بده، بعد ادعای نشت کن.
اندپوینت‌های پارامتردار (`{uuid}`) از این حلقه می‌افتند — آن‌ها را دستی و با
**شناسهٔ متعلق به پرسونای دیگر** بزن، چون همان‌جاست که IDOR پیدا می‌شود:
```bash
# uuid پزشکِ دیگری را به پرسونای doctor_solo بده — باید ۴۰۳/۴۰۴ بگیرد، نه ۲۰۰
node .claude/skills/qa-clinicpro/driver.mjs authz GET /api/v1/doctor/<uuid-of-another-doctor>
```
سه الگویی که باید در `/tmp/qa-authz-matrix.txt` دنبالشان بگردی:
| یافته | معنی |
|---|---|
| `anonymous` = ۲۰۰ روی مسیر غیرعمومی | نشت داده — Critical |
| نقشی ۲۰۰ می‌گیرد که در ماتریس ۰.۵ نبود | گارد جا افتاده — Critical |
| ۲۰۰ روی uuidِ مستأجر دیگر | IDOR — Critical |
| نقشی ۴۰۳ می‌گیرد که طبق ماتریس باید ۲۰۰ بگیرد | یا گارد سخت‌گیر است یا ماتریس غلط — بررسی کن |
| ۵۰۰ به‌جای ۴۰۳ | گارد کار می‌کند ولی خطا مدیریت نشده — High |
**بدون این جدولِ کامل، فاز دسترسی تمام‌شده نیست.** خروجی‌اش را در گزارش نهایی بیاور.
### ۴. تست قرارداد API
```bash
node .claude/skills/qa-clinicpro/driver.mjs api GET /api/v1/categorys/state --as admin
```
```
GET /api/v1/categorys/state → 301 12ms (as admin)
ENVELOPE
(none)
BODY
{
"success": false,
"data": null,
"errors": [
{ "code": "ERR_MOVED", "message": "این endpoint منتقل شده. لطفاً از /api/v1/provinces استفاده کنید." }
]
}
```
بخش `ENVELOPE` پاکت `BaseController` را چک می‌کند: نبودِ `success`، پاسخ خطای بدون
`errors`، و دام معروف **double/triple nesting** (`data.data.data`).
POST هم می‌شود: `--body '{"name":"x"}'`.
### ۵. کارایی
```bash
node .claude/skills/qa-clinicpro/driver.mjs perf "https://clinic-pro.ddev.site/admin/doctors" --as admin
```
```
PERF https://clinic-pro.ddev.site/admin/doctors (as admin)
ttfb 12ms
domContentLoaded 232ms
load 233ms
first-paint 180ms
first-contentful-paint 248ms
resources 24 · DOM nodes 1132
SLOWEST API CALLS
18ms 2kb v1/admin/doctors?page=1&limit=25
17ms 1kb v1/admin/doctors/stats
17ms 5kb v1/specialties
13ms 1kb v1/provinces
```
---
## نقش QA و روش کار
وقتی این skill فعال شد، مثل یک **مهندس ارشد تست** رفتار کن، نه فقط اجراکننده دستور:
0. **Phase 0 را تمام کن** (بالا). بدون پرسوناهای کامل، هر تستی نتیجهٔ بی‌معنی می‌دهد.
1. **اول سناریوی واقعی کاربر را بنویس**، بعد اجرا کن. مثال: ورود منشی → لیست نوبت‌ها →
تغییر وضعیت یک نوبت → خروج → ورود مجدد → آیا تغییر ماند؟
**پیمایش با هر پرسونا اجباری است.** بعد از Phase 0، برای *هر* پرسونا در جدول، وارد شو و
مسیرهای مجازش را طبق ماتریس ۰.۵ بگرد — نه فقط با `admin`. برای هر پرسونا حداقل:
```bash
for p in admin clinic doctor_solo doctor_member clinic_doctor \
secretary secretary_clinic representation patient; do
node .claude/skills/qa-clinicpro/driver.mjs visit \
"https://clinic-pro.ddev.site/admin/dashboard" --as "$p" --out "/tmp/qa-$p.png"
done
```
بعد **هر اسکرین‌شات را با Read باز کن و ببین** — و بخش `LANDING` را بخوان تا ریدایرکت
بی‌صدای نقش را نگیری. سه چیزی که فقط با مقایسهٔ بین پرسوناها پیدا می‌شوند:
- **نشت داده بین مستأجرها:** آیا `doctor_solo` دادهٔ بیمار پزشک دیگری را می‌بیند؟ آیا
`clinic` نوبت‌های پزشک غیرعضو را می‌بیند؟ این‌ها همیشه Critical‌اند.
- **صفحهٔ سفید به‌جای «دسترسی ندارید»:** نقشی که نباید ببیند، باید پیام روشن بگیرد.
- **منوی سایدبار در برابر دسترسی واقعی:** آیتمی که نمایش داده می‌شود ولی به ۴۰۳
می‌خورد (یا برعکس: مسیر باز است ولی در منو نیست) باگ است.
2. برای هر بخش این حالت‌ها را پوشش بده:
Happy Path · ورودی نامعتبر · داده خالی · داده خیلی زیاد (لیست ۱۰٬۹۳۲ کاربری) ·
شرایط مرزی · خطای شبکه · **همهٔ پرسوناها** · دسکتاپ ۱۴۴۰ و موبایل ۳۹۰.
3. **هیچ چیز را حدس نزن.** ادعای بدون خروجی دستور، ادعا نیست.
4. **قبل از گزارش، باگ را دوباره تکرار کن.** همان دستور را دوباره بزن؛ اگر تکرار نشد،
flaky بودنش را بنویس نه خودِ باگ را.
5. باگ‌های کوچک UI را هم گزارش کن، ولی باگ‌های Business Logic اولویت بالاترند.
### وقتی به مانع خوردی — رفعش کن، بعد برو تست بعدی
QA اینجا فقط گزارش‌نویس نیست. هر جا اجرای تست گیر کرد، **مثل یک دولوپر ارشد
Symfony/React خودت مشکل را حل کن**، تأیید کن که حل شده، و تست را از همان‌جا ادامه بده.
توقف روی اولین مانع یعنی بقیهٔ نقش‌ها هیچ‌وقت تست نمی‌شوند.
روال ثابت هر مانع:
```
بازتولید → ریشه‌یابی (نه علامت) → اصلاح → اثبات اصلاح → ثبت → ادامهٔ همان تست
```
1. **ریشه را پیدا کن، نه علامت را.** `visit` صفحهٔ سفید داد؟ اول `CONSOLE ERRORS` و
`NETWORK FAILURES`، بعد فایل سورس صفحه (`redesign-page/driver.mjs inspect`)، بعد
کنترلر مربوطه. اصلاح باید در همان لایه‌ای باشد که علت آنجاست.
2. **طبق قواعد پروژه اصلاح کن**، نه با وصلهٔ سریع:
- بک‌اند: `extends BaseController`، خطا با `AppException(ErrorCodes::…)`، کد SOLID،
تغییر entity ⟵ `doctrine:migrations:diff` + `migrate`.
- فرانت: TanStack Query برای دادهٔ سرور، کامپوننت‌های `components/ui/`، توکن‌های
`styles.css` (هیچ hex هاردکد)، رشته‌های فارسی.
- اندپوینت عوض شد ⟵ همان جلسه `docs/api/<domain>.md` را به‌روز کن (قاعدهٔ ثابت پروژه).
3. **اثبات کن.** همان دستوری که شکست خورده بود را دوباره بزن و خروجی سالمش را نشان بده.
بعد `ddev exec php bin/phpunit` و در صورت تغییر فرانت `npx tsc --noEmit` را اجرا کن
تا مطمئن شوی چیزی نشکسته‌ای.
4. **ثبت کن.** هر اصلاح یک ورودی در بخش «Fixes Applied» گزارش نهایی می‌گیرد:
مانع · ریشه · فایل‌های تغییریافته · دستور اثبات.
**مرزهایی که رد نمی‌کنی:**
- **باگ محصول را بی‌صدا رفع نکن.** اگر مانع خودش یک باگ واقعی محصول است، هم Bug Report
را بنویس هم اصلاح را — نه فقط اصلاح. گزارش، خروجی کار است.
- **هرگز برای سبزشدن تست، دسترسی را باز نکن.** اگر نقشی ۴۰۳ می‌گیرد و تو انتظار ۲۰۰
داری، پیش‌فرض این است که **انتظارت غلط است**. `IsGranted` یا `RoleRoute` را فقط وقتی
عوض کن که از روی کد ثابت کرده باشی آن نقش باید دسترسی داشته باشد، و دلیلش را بنویس.
همین قاعده برای حذف اعتبارسنجی ورودی هم هست.
- **دادهٔ تست را با تغییر محصول نساز.** کمبود دادهٔ پرسونا را با seed درست کن، نه با
نرم‌کردن یک قاعدهٔ کسب‌وکار.
- **مهاجرت مخرب نزن.** روی DB لوکالِ پر (۱۰٬۹۳۲ کاربر) `doctrine:schema:drop` یا
مهاجرتی که ستون پرداده را می‌اندازد، ممنوع.
- **اگر اصلاح از تست بزرگ‌تر شد** (بازطراحی معماری، تغییر شکست‌دهندهٔ قرارداد API که
`nobat724_front` و `clinic-pro-tauri` هم مصرفش می‌کنند)، دست نگه دار: باگ را با
اصلاح پیشنهادی گزارش کن، آن یک تست را `SKIPPED` علامت بزن، و **برو تست بعدی**.
### فرمت Bug Report
هر یافته را با این قالب بنویس (فارسی):
```markdown
## Title
<عنوان کوتاه و مشخص>
- **Severity:** Critical | High | Medium | Low
- **Priority:** فوری | مهم | معمولی | کم
- **Environment:** Chrome headless · macOS · ddev · نقش: <role> · ویوپورت: <w>x<h>
### Description
### Steps To Reproduce
1. `node .claude/skills/qa-clinicpro/driver.mjs …` ← دستور دقیق، نه توضیح
2.
### Expected Behavior
### Actual Behavior
### Evidence
<خروجی درایور، مسیر اسکرین‌شات، پاسخ API>
### Impact
### Suggested Fix
<فایل:خط اگر پیدا کردی>
```
برای پیدا کردن فایل سورس یک صفحه از روی URL، از skill خواهر استفاده کن:
```bash
node .claude/skills/redesign-page/driver.mjs inspect "https://clinic-pro.ddev.site/admin/doctors"
```
### گزارش نهایی
۱. خلاصه وضعیت کلی · ۲. تعداد باگ‌ها · ۳. لیست بر اساس Severity ·
۴. باگ‌هایی که باید فوری رفع شوند · ۵. پیشنهاد بهبود کیفیت.
به‌علاوه این سه بخش که از قواعد بالا می‌آیند:
**۶. Fixes Applied** — هر مانعی که خودت رفع کردی:
| مانع | ریشه | فایل‌های تغییریافته | دستور اثبات |
|---|---|---|---|
**۷. ماتریس دسترسی** — جدول کامل `مسیر × پرسونا` از بخش ۳.۱، با اختلاف‌های
ماتریسِ کد و رفتار واقعی مشخص‌شده.
**۸. پوشش** — کدام پرسونا چه چیزی تست شد، و هر `SKIPPED` با دلیلش. اگر نقشی تست نشد
باید اینجا صریح بیاید؛ گزارشِ ساکت بدتر از گزارش ناقص است.
---
## Gotchas
- **SPA است، پس `curl` صفحه نمی‌دهد.** `curl /admin/doctors` همیشه همان HTML پوسته را
برمی‌گرداند. هر ادعایی درباره محتوای صفحه باید از `visit` بیاید.
- **ریدایرکت بی‌صدای نقش.** `RoleRoute` در `App.tsx` کاربر بدون دسترسی را بی‌هیچ پیغامی
به `/dashboard` می‌فرستد — اسکرین‌شات کاملاً سالم به‌نظر می‌رسد ولی صفحهٔ اشتباهی است.
همیشه بخش `LANDING` را بخوان.
- **توکن فقط ۱۵ دقیقه اعتبار دارد.** درایور برای هر دستور دوباره لاگین می‌کند، پس مسئله‌ای
نیست؛ ولی اگر خودت توکن را جایی کش کردی، انتظار ۴۰۱ داشته باش.
- **`TEST_USERS.md` دروغ می‌گوید** (بالا). به آن استناد نکن.
- **`CLAUDE.md` هم روی `/api/v1/categorys/{bundle}` منسوخ است** — آن مسیر حالا ۳۰۱ با
`ERR_MOVED` می‌دهد و مسیر واقعی `/api/v1/provinces` است.
- **کد OTP در محیط dev همیشه `12345` است** (`OtpService::sendCode` — در غیر dev کد تصادفی
۵رقمی می‌سازد و SMS می‌کند). پس زنجیرهٔ کامل ورود بدون رمز اسکریپت‌پذیر است:
`POST /api/v1/user/send-code``POST /api/v1/user/verify-code` با `code=12345`
`grant``POST /api/v1/user/otp-login`.
- **`patient` و `unclaimed_doctor` با رمز وارد نمی‌شوند و این باگ نیست.**
`PasswordAuthenticator::onAuthenticationSuccess` هر کاربری که `User::isStaff()` نباشد را
با ۴۰۳ و `ERR_AUTH_006` رد می‌کند (staff = doctor/clinic/secretary/admin/representation/importer).
این دو پرسونا فقط OTP-only هستند؛ درایور خودش به زنجیرهٔ OTP بالا fallback می‌کند.
**گاردش را برای سبزشدن تست باز نکن.**
- **`send-code` سقف ۵ درخواست در ساعت به‌ازای هر IP دارد** (`config/packages/rate_limiter.yaml`).
یک جاروی کامل authz این سقف را می‌سوزاند و بعدش پرسوناهای OTP-only شکست می‌خورند
(`ERR_RATE_LIMIT_001`). راه‌حل بدون دست‌زدن به محصول: توکن را مستقیم با کامند خود اپ بساز —
```bash
ddev exec 'php bin/console lexik:jwt:generate-token 09129000006 --user-class="App\\Auth\\Entity\\User"'
```
همان کلید و همان claimها؛ فقط محدودیت نرخ را دور می‌زند.
- **برای جاروی ماتریس، درایور را در حلقه صدا نزن.** هر فراخوانی دوباره لاگین می‌کند
(۱۶۸ مسیر × ۱۳ پرسونا ≈ ۲۲۰۰ لاگین) — هم چند ده دقیقه طول می‌کشد هم rate limit را می‌سوزاند.
یک‌بار برای هر پرسونا توکن بگیر و همان را در همهٔ مسیرها استفاده کن.
- **گواهی TLS ddev را Node قبول نمی‌کند.** درایور فقط برای هاست‌های `*.ddev.site` /
`localhost` `NODE_TLS_REJECT_UNAUTHORIZED=0` می‌گذارد و وارنینگ نویزی‌اش را خفه می‌کند.
- **خطاهای صفحهٔ لاگین به حساب صفحهٔ تحت تست نوشته نشوند.** درایور بافر خطا را بعد از
seed کردن `localStorage` و قبل از ناوبری به URL هدف پاک می‌کند.
- **دیتای لوکال واقعی و بزرگ است** (۱۰٬۹۳۲ کاربر، ۲۰۲ کلینیک، ۱۷۹ پزشک) — برای تست
«داده زیاد» لازم نیست چیزی seed کنی.
- **CDP روی پورت ۹۴۴۴** است تا با درایور `redesign-page` (پورت ۹۳۳۳) تداخل نکند؛
می‌توانی هر دو را هم‌زمان اجرا کنی. `CDP_PORT` قابل تغییر است.
## Troubleshooting
| نشانه | علت / رفع |
|---|---|
| `login as admin failed: … ERR_AUTH_005` | DB ریست شده؛ پسورد QA را دوباره ست کن (بخش «کاربران تست») |
| `Chrome did not expose CDP on :9444` | `CHROME_BIN` غلط است، یا نمونهٔ قبلی کروم روی همان پورت مانده — `pkill -f clinicpro-qa` |
| `⚠ page text only N chars` | رندر SPA کرش کرده یا کند است؛ اول `--wait 8000` را امتحان کن، بعد `CONSOLE ERRORS` را بخوان |
| `⚠ redirected to /login` | توکن رد شده — با `driver.mjs login <role>` صحتش را چک کن |
| `fetch failed` / `ECONNREFUSED` | ddev بالا نیست: `ddev start` |
## باگ‌های شناخته‌شده (در همین اجرا پیدا شدند)
نمونه‌هایی از خروجی واقعی همین درایور، به‌عنوان مرجعِ اینکه گزارش چطور باشد:
1. **Medium** — در «وضعیت نوبت‌ها»ی داشبورد، برچسب‌های `confirmed` و `expired` انگلیسی
مانده‌اند در حالی که بقیه فارسی‌اند («تکمیل شده»، «لغو پزشک»).
بازتولید: `visit https://clinic-pro.ddev.site/admin/dashboard --as admin`، اسکرین‌شات.
2. **Low** — کارت «درآمد این ماه» کلمهٔ «تومان» را دو بار نشان می‌دهد (یک‌بار کنار عدد،
یک‌بار به‌عنوان زیرنویس کارت). همان اسکرین‌شات.
3. **Low** — در ویوپورت ۳۹۰px داشبورد، ۵ تارگت لمسی زیر ۳۶px هستند.
بازتولید: `ux … --w 390 --h 844`.
4. **Medium (مستندات)** — `TEST_USERS.md` و بخش Category در `CLAUDE.md` هر دو منسوخ‌اند.