From b10b0813f3b9bc920bed87f0cdf59a5a1bc443e4 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sun, 19 Jul 2026 13:41:16 +0330 Subject: [PATCH] feat: enhance QA driver with detailed persona definitions for improved testing accuracy --- .claude/skills/qa-clinicpro/SKILL.md | 233 +++++++++++++++++++++++-- .claude/skills/qa-clinicpro/driver.mjs | 19 +- 2 files changed, 238 insertions(+), 14 deletions(-) diff --git a/.claude/skills/qa-clinicpro/SKILL.md b/.claude/skills/qa-clinicpro/SKILL.md index 50054ab1..9fcb3906 100644 --- a/.claude/skills/qa-clinicpro/SKILL.md +++ b/.claude/skills/qa-clinicpro/SKILL.md @@ -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/ +``` + +سه الگویی که باید در `/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/.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 diff --git a/.claude/skills/qa-clinicpro/driver.mjs b/.claude/skills/qa-clinicpro/driver.mjs index bcab1ca6..8ae7ed1c 100644 --- a/.claude/skills/qa-clinicpro/driver.mjs +++ b/.claude/skills/qa-clinicpro/driver.mjs @@ -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.