feat: enhance QA driver with detailed persona definitions for improved testing accuracy

This commit is contained in:
hamed
2026-07-19 13:41:16 +03:30
parent 773f9d4d16
commit b10b0813f3
2 changed files with 238 additions and 14 deletions
+222 -11
View File
@@ -1,6 +1,6 @@
---
name: qa-clinicpro
description: تست QA اپلیکیشن ClinicPro مثل یک کاربر واقعی — اجرای اپ، ورود با هر نقش، پیمایش صفحات پنل ادمین، اسکرین‌شات، کشف خطاهای کنسول و شبکه، تست 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 کن»، «این صفحه را بررسی کن».
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
@@ -33,17 +33,29 @@ ls "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
در دیتابیس وجود ندارند و همه `ERR_AUTH_005` می‌گیرند. اسکریپت‌های `create_test_users.php`
و `seed_realistic_data.php` هم که آن فایل ارجاع می‌دهد در ریپو نیستند.
نقش‌های واقعیِ کارکننده در `ROLES` داخل درایور هاردکد شده‌اند:
پرسوناهای QA در `ROLES` داخل درایور تعریف شده‌اند. **واحد کار «پرسونا» است، نه
`ROLE_*`** — پزشک مستقل و پزشک عضو کلینیک هر دو `ROLE_DOCTOR` دارند ولی دادهٔ متفاوتی
می‌بینند، پس هرکدام یک ردیف جداگانه‌اند.
| نقش | موبایل | پسورد |
|---|---|---|
| `admin` | `09120671756` | `QaTest@1234` |
| `clinic` | `09127000000` | `QaTest@1234` |
| `secretary` | `09123456778` | `QaTest@1234` |
| `doctor` | `09390039833` | `09390039833` |
| `representation` | `09124000001` | `09124000001` |
| پرسونا | موبایل | پسورد | نقش‌ها | تمایز |
|---|---|---|---|---|
| `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` | — |
سه تای اول پسوردشان عمداً برای QA ست شده. اگر DB ریست شد، دوباره ست کن:
پنج ردیف اول موجودند. **هفت ردیف آخر تا وقتی Phase 0 اجرا نشده وجود ندارند** و
`driver.mjs roles` برایشان `✗` می‌دهد — این دقیقاً چک آمادگی است.
اگر DB ریست شد، پسورد پنج‌تای اول را دوباره ست کن:
```bash
ddev exec php bin/console security:hash-password 'QaTest@1234'
@@ -74,6 +86,82 @@ representation 09124000001 ROLE_USER,ROLE_REPRESENTATION token 15min
## مسیر اجرا (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
@@ -152,6 +240,52 @@ AUTHZ GET /api/v1/admin/users
هر ۲۰۰ غیرمنتظره در این جدول = یک باگ 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
@@ -207,16 +341,80 @@ SLOWEST API CALLS
وقتی این 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
هر یافته را با این قالب بنویس (فارسی):
@@ -253,6 +451,19 @@ node .claude/skills/redesign-page/driver.mjs inspect "https://clinic-pro.ddev.si
۱. خلاصه وضعیت کلی · ۲. تعداد باگ‌ها · ۳. لیست بر اساس Severity ·
۴. باگ‌هایی که باید فوری رفع شوند · ۵. پیشنهاد بهبود کیفیت.
به‌علاوه این سه بخش که از قواعد بالا می‌آیند:
**۶. Fixes Applied** — هر مانعی که خودت رفع کردی:
| مانع | ریشه | فایل‌های تغییریافته | دستور اثبات |
|---|---|---|---|
**۷. ماتریس دسترسی** — جدول کامل `مسیر × پرسونا` از بخش ۳.۱، با اختلاف‌های
ماتریسِ کد و رفتار واقعی مشخص‌شده.
**۸. پوشش** — کدام پرسونا چه چیزی تست شد، و هر `SKIPPED` با دلیلش. اگر نقشی تست نشد
باید اینجا صریح بیاید؛ گزارشِ ساکت بدتر از گزارش ناقص است.
---
## Gotchas
+16 -3
View File
@@ -25,9 +25,13 @@ const CHROME = process.env.CHROME_BIN
const PORT = Number(process.env.CDP_PORT ?? 9444);
/**
* Local QA accounts, one per role. Passwords were set deliberately for testing
* (see SKILL.md § Test accounts); the doctor account predates that and still
* uses mobile-as-password. TEST_USERS.md is stale — its accounts do not exist.
* Local QA personas — one per distinct authorization identity in the product,
* not merely one per ROLE_* constant: an independent doctor and a clinic-member
* doctor carry the same role but see different data, so each gets its own row.
*
* The first five predate this list and are known to exist; the rest are
* provisioned by SKILL.md § Phase 0 and report `✗` from `driver.mjs roles`
* until they are. TEST_USERS.md is stale — its accounts do not exist.
*/
const ROLES = {
admin: ['09120671756', 'QaTest@1234'],
@@ -35,6 +39,15 @@ const ROLES = {
secretary: ['09123456778', 'QaTest@1234'],
doctor: ['09390039833', '09390039833'],
representation: ['09124000001', '09124000001'],
// Provisioned by Phase 0. Reserved QA range 0912900000x, password QaTest@1234.
doctor_solo: ['09129000001', 'QaTest@1234'], // own office, no clinic
doctor_member: ['09129000002', 'QaTest@1234'], // member of a clinic
clinic_doctor: ['09129000003', 'QaTest@1234'], // ROLE_CLINIC + ROLE_DOCTOR
secretary_clinic: ['09129000004', 'QaTest@1234'], // secretary of a clinic
unclaimed_doctor: ['09129000005', 'QaTest@1234'], // imported, unclaimed profile
patient: ['09129000006', 'QaTest@1234'], // ROLE_USER only
importer: ['09129000007', 'QaTest@1234'],
};
// ddev serves a locally-signed cert Node's fetch refuses. Relax TLS only for it.