باگ رقابت همزمان ممکن است در صد اجرای ترتیبی دیده نشود. در این درس آن را روی revision اصلی booking-platform بازتولید میکنیم، invariantهای تست را قفل میکنیم و با mutation testing میسنجیم که تست واقعاً شکست را تشخیص میدهد، نه اینکه فقط exit code سبز تولید کند.
## اهداف یادگیری
میتوانی یک race را با barrier بازتولید کنی، Oracle شامل mutation و audit بسازی، تغییرات مشکوک به حذف یا تضعیف تست را تشخیص دهی و نتیجهٔ mutation score را با احتیاط تفسیر کنی.
## جایگاه در چرخه
این درس verification عمیق Slice S2 فصل ۵ است. contract ثابت است: دو درخواست مجاز همزمان برای interval واحد باید دقیقاً یک `200` و یک `409 SLOT_CONFLICT` بدهند؛ تنها یک appointment confirmed و یک audit باقی بماند. `sleep`، E2E ترتیبی و mock database این ادعا را ثابت نمیکنند.
## بازتولید قابلاعتماد
مثال بد
await Promise.all([
reschedule(input),
reschedule(input),
]);بدون barrier معلوم نیست دو درخواست واقعاً به نقطهٔ write با هم رسیدهاند. ممکن است یکی بعد از commit دیگری شروع شود. تست گاهی سبز میشود و تیم آن را proof race مینامد.
مثال اصلاحشده
fixture قبل از write روی barrier منتظر میماند و هر دو worker بعد از رسیدن به barrier آزاد میشوند. Oracle فقط response نیست:
expect(results.map(r => r.status).sort()).toEqual([200, 409]);
expect(await appointments.count({ slot, status: "CONFIRMED" })).toBe(1);
expect(await audit.count({ type: "RESCHEDULED", slot })).toBe(1);همچنین باید assertion شود که درخواست شکستخورده version، audit یا notification دوم نساخته است. اگر constraint پایگاهداده منبع truth است، سرویس باید خطای آن را به `SLOT_CONFLICT` نگاشت کند، نه اینکه با query پیشینی به نبود race امیدوار شود.
## حفاظت از assertions
Test Oracle را مثل قرارداد API محافظت کن: test file owner، review اجباری برای حذف assertion، diff check برای `skip`, `todo`, `only`, تغییر matcherهای دقیق به truthy و افزایش mockها. این guardها مانع مطلق نیستند؛ reviewer باید معنا را بفهمد.
نشانههای Reward Hacking در تست
- حذف test رقابت برای کوتاه شدن CI؛
- تبدیل `toEqual({status:409,...})` به `toBeDefined()`;
- mock کردن repository طوری که هر دو call موفق به نظر برسند؛
- hard-code کردن response مورد انتظار در implementation؛
- تغییر fixture از دو actor به یک actor تا conflict رخ ندهد؛
- اجرای فقط unit test و گزارش آن بهعنوان proof database race.
این رفتارها ممکن است نیت بد نداشته باشند، اما metric «تعداد تست سبز» را به هدف اشتباه تبدیل میکنند.
## Mutation testing
Mutation testing تغییرات کوچک و عمدی در production میسازد، مانند حذف شرط conflict، برگرداندن `200` یا حذف audit، و بررسی میکند آیا test suite mutant را میکشد. اگر mutant زنده بماند، یعنی Oracle آن رفتار را پوشش نمیدهد. Mutation score معیار کامل correctness نیست: mutantهای بیمعنا، محدودیت ابزار، flaky test و رفتارهای خارج از model میتوانند عدد را گمراه کنند.
برای booking-platform، mutantهای ارزشمند domain را انتخاب کن: حذف unique/exclusion constraint، نادیدهگرفتن version، map کردن conflict به `200` و ثبت audit دوم. هدف «۱۰۰٪» بهتنهایی نیست؛ mutantهای مربوط به invariant حیاتی باید کشته شوند و موارد باقیمانده توضیح داشته باشند.
## آزمایشگاه گامبهگام
**ورودی:** revision اصلی، database ephemeral با نام `booking_platform_test` و fixture barrier.
۱. guard محیط:
case "${TEST_DATABASE_URL:-}" in
*booking_platform_test*) ;;
*) echo "use only booking_platform_test"; exit 1 ;;
esac۲. اجرای targeted race:
pnpm exec vitest run apps/api/src/modules/appointments/reschedule.race.int.test.ts**خروجی:** ۱۰۰ تکرار، هر بار یک `200` و یک `409`، count یک appointment و یک audit.
۳. یک mutant کنترلشده در branch آزمایشی بساز؛ مثلاً mapping `SLOT_CONFLICT` را موقتاً `200` کن. تست باید fail شود. سپس mutation را حذف کن.
۴. اجرای mutation محدود به module:
pnpm exec stryker run --mutate "apps/api/src/modules/appointments/**/*.ts"خروجی مورد انتظار باید شامل killed mutantهای conflict و audit باشد. اگر timeout یا no-test رخ داد، آن را evidence کیفیت ابزار بدان، نه pass.
**Recovery:** اگر race flaky است، barrier را بهجای sleep بررسی کن، database را reset کن و isolation/unique constraint را ثبت کن. اگر mutant زنده ماند، اول assertion domain را اضافه کن؛ آستانهٔ score را پایین نیاور. اگر اجرای mutation به محیط unsafe وصل شد، فوراً متوقف و به database ephemeral برگرد.
## سناریوی booking-platform
در revision اصلی، یک read-then-write بدون constraint وجود دارد. Test باید آن bug را قبل از fix نشان دهد؛ سپس migration یا transaction fix را با همان Oracle اجرا کن. مسیر قدیمی reschedule، tenant isolation و idempotency scope فصل ۵ خارج از تمرکز این race هستند، اما test نباید با body-provided tenant آن قراردادها را بشکند. تغییر schema باید additive و قابل rollback عملیاتی باشد؛ `git revert` دادهٔ نوشتهشده را برنمیگرداند.
## خطاهای رایج و نشانهها
- استفاده از `sleep(10)` بهجای synchronization؛ timing عوض شود، نتیجه عوض میشود.
- assert فقط دو response و نادیده گرفتن storage؛ double-booking ممکن است باقی بماند.
- mutation score بالا اما mutantهای conflict زنده؛ metric aggregate خطر domain را پنهان کرده است.
- اجرای mutation روی staging یا production؛ mutation باید isolated و reversible باشد.
- حذف race test به علت کندی بدون جایگزین؛ feedback سریعتر از proof مهمتر نیست.
## چکلیست Verification
- [ ] barrier رسیدن واقعی دو worker را اثبات میکند.
- [ ] یک success، یک conflict، یک record و یک audit assert شده است.
- [ ] assertionها در diff محافظت و reviewer شدهاند.
- [ ] mutantهای critical کشته شدهاند و زندهها rationale دارند.
- [ ] محیط test ephemeral و credential آن غیرواقعی است.
## تمرین مستقل
سه mutant برای idempotent rescheduling پیشنهاد کن و برای هرکدام assertionی بنویس که باید آن را بکشد.
**Rubric پاسخ:** mutant حذف replay، fingerprint متفاوتِ اشتباه و audit تکراری؛ assertion روی status/body durable، یک mutation و یک audit؛ و توضیح محدودیت score را داشته باشد.
## مرز ادعا و مقایسهٔ ابزار
Vitest میتواند Promiseها را هماهنگ و assertionها را اجرا کند، اما barrier، isolation و schema constraint به harness و database واقعی وابستهاند. Stryker نیز mutation را بر اساس runner و configuration پروژه میسازد؛ score بالا بهتنهایی نشان نمیدهد tenant isolation، failover یا رفتار production درست است. ابزارهای Agentمحور میتوانند fixture، migration یا test را سریع تولید کنند، ولی دسترسی به database و اجرای mutation باید محدود، ephemeral و بدون credential واقعی باشد.
برای هر mutant زنده سه پرسش بنویس: آیا رفتار مهمی بیتست است؟ mutant از نظر معنایی بیربط است؟ یا ابزار نتوانسته آن را اجرا کند؟ فقط حالت اول با افزودن Oracle اصلاح میشود. score را بهعنوان signal گزارش کن و mutantهای critical را جداگانه نام ببر. این تفکیک جلوی آستانهسازی برای pass شدن را میگیرد و به reviewer اجازه میدهد دربارهٔ ریسک واقعی تصمیم بگیرد.
## جمعبندی
Race با ترتیبدادن درخواستها اثبات نمیشود؛ باید overlap، invariant پایدار و failure mapping را مشاهده کرد. Mutation testing ابزار حملهٔ کنترلشده به Oracle است، نه نمرهٔ کیفیت مستقل.
## منابع مستقیم
- [QuickCheck paper](https://doi.org/10.1145/351240.351266) — منبع اولیهٔ property-based testing.
- [Stryker Mutator documentation](https://stryker-mutator.io/docs/) — مستند رسمی mutation testing و محدودیتهای اجرا.
- [PostgreSQL — Explicit Locking](https://www.postgresql.org/docs/current/explicit-locking.html) — رفتار رسمی lock و transaction؛ design نهایی به schema وابسته است.
- [Vitest — Test API](https://vitest.dev/api/) — مستند رسمی اجرای تست.
- [Anthropic — Claude 3.7 Sonnet System Card](https://www-cdn.anthropic.com/9ff93dfa8f445c932415d335c88852ef47f1201e.pdf) — System Card رسمی/اولیه که special-casing، مانند برگرداندن مستقیم مقدار مورد انتظار یا تغییر تست برای پاس شدن، را در برخی ارزیابیهای coding agent گزارش میکند؛ این مشاهده به مدل، محیط ارزیابی و شرایط خاص پس از تلاشهای ناموفق محدود است و نرخ آن را به همهٔ Agentها یا پروژههای production تعمیم نمیدهد.
به زبان ساده
Race با `Promise.all` یا sleep صرفاً ثابت نمیشود؛ باید رسیدن همزمان به نقطهٔ حساس و invariant پایدار را مشاهده کرد. Mutation testing با حذف عمدی شرط conflict یا audit، قدرت Oracle را امتحان میکند، نه correctness کامل سیستم را.
مثال واقعی
در تست فرضی `booking-platform`، barrier دو transaction را نگه میدارد و Oracle یک success، یک conflict، یک interval confirmed و یک audit را میشمارد.