---
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` هر دو منسوخ‌اند.
