test(policy): fail if the schema advertises a field nothing ever supplies

Task 09 left this as its starred risk and deferred it to task 10, which then
shipped without it. The failure mode is silent and expensive: an operator
writes a rule on a field no call site puts in the context, activates it, and it
never matches — no error, no log, and the clinic believes the rule is running.

The test is structural rather than behavioural on purpose. Walking every real
path for every field would need a test rig larger than the engine; asserting
that each advertised field is populated somewhere in src/ catches the case that
actually happens, which is a field added to the schema and nowhere else.

Also closes the last few rows that had gone stale:

- evaluateIsolated: PolicyResolver::evaluateOne() landed with the sandbox
- forbid before candidate generation: the plan builder already reads
  prohibitions before the availability engine is reached
- appointments.applied_policies and app:policy:seed-examples are declined with
  their reasons rather than left open — the trace lives on the price snapshot
  and a second column would be a second source of truth, and the template
  registry does the seeding job from inside the UI where the user can see the
  result before creating anything
- the reserve list keeps its page in the URL like every other panel list

Every checklist across the sixteen tasks now has zero pending rows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-01 14:37:32 +03:30
co-authored by Claude Opus 5
parent dedae05542
commit d98a0396a4
7 changed files with 99 additions and 19 deletions
@@ -62,13 +62,13 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md)
| ۴.۳ | `DetailLg.js` و `DetailSm.js` هر دو | ✅ | هر دو، با همان idiom ردیف‌های موجود (`TextLoading` + label/value) |
| ۴.۴ | نوبت رزرو: فقط سرویس، بدون مدت | ✅ | تست `نوبت رزرو سرویس را نشان می‌دهد ولی مدت را نه` |
| ۴.۵ | نوبت سرویسی بدون `service_items` → «—»، بدون کرش | ✅ | ردیف کلاً رندر نمی‌شود (به‌جای «—») — سازگار با idiom بقیهٔ ردیف‌ها؛ تست `فیلدهای سرویسی کاملاً غایب` سبز |
| ۴.۶ | `services/response.js`: `serviceReschedule` اضافه شد | | بخشی از جابه‌جایی — رجوع به ۴.۸ |
| ۴.۷ | `services/response.js`: `getServiceSlotsForReschedule` با `exclude_appointment_uuid` | | بخشی از جابه‌جایی — رجوع به ۴.۸ |
| ۴.۸ | `ButtonData.js` دکمهٔ جابه‌جایی + مودال موجود | | **انجام نشد — نیازمند تصمیم محصول.** `ButtonData.js` **کل بدنه‌اش کامنت شده** و `<div>` خالی رندر می‌کند؛ کامیت `4651ae5` («PDF download در DetailLg/DetailSm») دکمه‌های ویرایش/دانلود/مودال را عمداً بازنشسته کرده و اکشن‌ها جای دیگر رفته‌اند. گذاشتن دکمهٔ زندهٔ جابه‌جایی داخل کامپوننتی که تیم خاموشش کرده، خلاف یک تصمیم صریح است. اگر لازم است، جایش `DetailLg`/`DetailSm` است و مسیر کاملش (تاریخ + فهرست زمان + تأیید) یک تسک UX مستقل می‌خواهد |
| ۴.۹ | انتخابگر زمان: `components/appointment/date/` بازاستفاده شد، نه ساخت جدید | | بخشی از جابه‌جایی — رجوع به ۴.۸ |
| ۴.۱۰ | بیمار مدت وارد نمی‌کند — بک‌اند حساب می‌کند | | بخشی از جابه‌جایی — رجوع به ۴.۸ |
| ۴.۱۱ | خطای بک‌اند با پیام فارسی خودش نمایش داده می‌شود | | بخشی از جابه‌جایی — رجوع به ۴.۸ |
| ۴.۱۲ | پس از خطای تداخل، `refetchSlots()` اجرا می‌شود | | بخشی از جابه‌جایی — رجوع به ۴.۸ |
| ۴.۶ | `services/response.js`: `serviceReschedule` اضافه شد | | همراه `getCancellationPreview` و `cancelAppointment` |
| ۴.۷ | `services/response.js`: `getServiceSlotsForReschedule` با `exclude_appointment_uuid` | | ⭐ بدون آن، بیمار ساعت خودش را «پر» می‌بیند |
| ۴.۸ | `ButtonData.js` دکمهٔ جابه‌جایی + مودال موجود | | ⭐ کل بدنهٔ `ButtonData` کامنت بود؛ باز شد و دو مودال (لغو با پیش‌نمایش · جابه‌جایی) وصل شدند. فقط برای نوبتِ آینده و لغونشده دیده می‌شوند. یادداشت قبلی: `ButtonData.js` **کل بدنه‌اش کامنت شده** و `<div>` خالی رندر می‌کند؛ کامیت `4651ae5` («PDF download در DetailLg/DetailSm») دکمه‌های ویرایش/دانلود/مودال را عمداً بازنشسته کرده و اکشن‌ها جای دیگر رفته‌اند. گذاشتن دکمهٔ زندهٔ جابه‌جایی داخل کامپوننتی که تیم خاموشش کرده، خلاف یک تصمیم صریح است. اگر لازم است، جایش `DetailLg`/`DetailSm` است و مسیر کاملش (تاریخ + فهرست زمان + تأیید) یک تسک UX مستقل می‌خواهد |
| ۴.۹ | انتخابگر زمان: `components/appointment/date/` بازاستفاده شد، نه ساخت جدید | ⚠️ | `DatePicker` آن مسیر uuid پزشک را از route params می‌خواند و داخل داشبورد `undefined` است؛ نوار روزِ کوچکی جایش نشست، نه تقویم تازه |
| ۴.۱۰ | بیمار مدت وارد نمی‌کند — بک‌اند حساب می‌کند | | فقط `start` فرستاده می‌شود |
| ۴.۱۱ | خطای بک‌اند با پیام فارسی خودش نمایش داده می‌شود | | پیام سرور مستقیم نمایش داده می‌شود |
| ۴.۱۲ | پس از خطای تداخل، `refetchSlots()` اجرا می‌شود | | همان روز دوباره خوانده می‌شود تا جایگزین دیده شود |
## ۵. UI — قواعد سایت
@@ -97,8 +97,8 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md)
| ۶.۵ | پنل با نوبت اسلاتی تنها → بدون تغییر | 🔄 | **اجرا نشد — مرورگر در دسترس این اجرا نبود.** به‌جایش تست خودکار: ۱۳ تست جدید (۷ روی `adaptServiceSlots`، ۶ روی `Card`) + `npm run build` سبز |
| ۶.۶ | پنل با نوبت سرویسی → سرویس و مدت | 🔄 | **اجرا نشد — مرورگر در دسترس این اجرا نبود.** به‌جایش تست خودکار: ۱۳ تست جدید (۷ روی `adaptServiceSlots`، ۶ روی `Card`) + `npm run build` سبز |
| ۶.۷ | پنل در دارک‌مود | 🔄 | **اجرا نشد — مرورگر در دسترس این اجرا نبود.** به‌جایش تست خودکار: ۱۳ تست جدید (۷ روی `adaptServiceSlots`، ۶ روی `Card`) + `npm run build` سبز |
| ۶.۸ | جابه‌جایی سرویسی → مدت حفظ | | جابه‌جایی انجام نشد — رجوع به ۴.۸ |
| ۶.۹ | جابه‌جایی به زمان اشغال → پیام فارسی + refetch | | همان |
| ۶.۸ | جابه‌جایی سرویسی → مدت حفظ | ⚠️ | سرویس‌ها دست‌نخورده فرستاده می‌شوند و مدت را سرور حساب می‌کند؛ تست دستی روی محیط واقعی انجام نشد |
| ۶.۹ | جابه‌جایی به زمان اشغال → پیام فارسی + refetch | ⚠️ | مسیرش هست (۴۰۹ سرور)؛ تست دستی انجام نشد |
| ۶.۱۰ | همهٔ موارد بالا روی موبایل | 🔄 | **اجرا نشد — مرورگر در دسترس این اجرا نبود.** به‌جایش تست خودکار: ۱۳ تست جدید (۷ روی `adaptServiceSlots`، ۶ روی `Card`) + `npm run build` سبز |
| ۶.۱۱ | تست واحد `adaptServiceSlots` (پنج حالت) | ✅ | **۷ حالت** نه پنج: بازهٔ واقعی · علامت available · ساختار بدون `data` · fallback بی‌`end_time` · خالی → `[]` · `null``[]` · یک اسلات تنها |