تستی که بعد از implementation نوشته میشود و فوراً سبز است، نشان نمیدهد رفتار پیش از آن وجود نداشته است. در کار Agentمحور باید علت شکست را مشاهده کنیم و نقشها را جدا نگه داریم: یک نفر یا Agent تست را مینویسد، بازبین Oracle را میسنجد و implementer کد production را تغییر میدهد.
## اهداف یادگیری
میتوانی Red را به علت درست نسبت دهی، تستی را که به دلیل اشتباه میسبزد تشخیص دهی، نقش test author/reviewer/implementer را جدا کنی و برای هر نقش ورودی و خروجی قابلبررسی تعیین کنی.
## جایگاه در چرخه
این درس ادامهٔ `Test` در فصل ۵ و l20 است. ورودی آن acceptance example تأییدشده است و خروجیاش تستی است که قبل از کد production شکست معنادار دارد. authority همان قرارداد S2 است: پنجرهٔ بسته `409 RESCHEDULE_WINDOW_CLOSED`، بدون mutation و audit.
## Red به چه معناست؟
Red معتبر چهار شرط دارد: تست جدید واقعاً assertion رفتاری دارد؛ baseline با حذف implementation یا روی revision اصلی fail میشود؛ failure به نبود behavior مربوط است؛ و تست به fixture یا import شکسته وابسته نیست. «exit code غیرصفر» کافی نیست. خطای parse، database خاموش یا typo Red نیست.
مثال بد
ایجنت ابتدا این تابع را میسازد و سپس تست مینویسد:
expect(cancellationDecision(input).allowed).toBe(false);تست سبز است، اما معلوم نیست کد واقعاً policy را اعمال میکند یا تست به fixtureای اشاره دارد که همیشه false برمیگرداند. اگر implementation را حذف کنیم و تست همچنان سبز بماند، Red وجود نداشته است.
مثال اصلاحشده: سه نقش
**Test author** فقط تست و fixture را میسازد:
it("returns the closed-window contract", async () => {
const response = await reschedule(api, {
appointmentId: "apt-41",
startLocal: "2026-09-11T10:00:00",
endLocal: "2026-09-11T11:00:00",
});
expect(response.status).toBe(409);
expect(response.body).toMatchObject({ code: "RESCHEDULE_WINDOW_CLOSED" });
});**Reviewer** میپرسد آیا `appointmentId` متعلق به actor است، clock کنترل شده، status قبلی و عدم mutation assert شده است. **Implementer** فقط بعد از ثبت Red سرویس را تغییر میدهد. Reviewer میتواند همان test را روی revision اصلی یا با حذف patch اجرا کند.
جداسازی لزوماً سه انسان نیست. سه پنجرهٔ Agent یا سه commit نقش را مستقل میکند، به شرطی که implementer متن acceptance و test را نتواند بیصدا تغییر دهد و reviewer به diff واقعی دسترسی داشته باشد.
## تستی که غلط سبز میشود
این الگو خطرناک است:
const result = await service.reschedule(input);
expect(result).toBeDefined();هر objectی سبز است؛ status، body، version، audit و database state بررسی نمیشوند. نسخهٔ اصلاحشده response را با قرارداد مقایسه میکند و قبل و بعد storage را snapshot میگیرد:
expect(result).toEqual({
status: 409,
body: { code: "RESCHEDULE_WINDOW_CLOSED" },
});
expect(await repo.find("apt-41")).toEqual(before);
expect(await audit.count({ appointmentId: "apt-41" })).toBe(0);این assertionها Oracle هستند؛ reviewer باید ببیند که assertion حذف یا به `toBeTruthy()` تبدیل نشده است.
## سناریوی booking-platform
برای revision اصلی، baseline شامل مسیر قدیمی و appointment confirmed است. Test author باید fixture را با tenant و actor معتبر بسازد و زمان را با timezone کلینیک به UTC تبدیل کند. implementer حق ندارد در body به `tenantId` اعتماد کند. reviewer علاوه بر پاسخ، invariant را میسنجد: در deadline بسته، version، slot، audit و notification تغییر نمیکنند. اگر تست فقط unit باشد، این محدودیت را نمیتوان به integration تعمیم داد؛ سطح شواهد باید کنار تست نوشته شود.
## آزمایشگاه گامبهگام
**ورودی:** revision اصلی، acceptance test، قرارداد فصل ۵.
۱. وضعیت پاک و hash revision را ثبت کن:
git rev-parse HEAD
pnpm exec vitest run apps/api/src/modules/appointments/reschedule.int.test.ts۲. Test author تست را اضافه کند و آن را روی revision اصلی اجرا کند. **خروجی:** failure مورد انتظار مثل `expected 409, received 200`؛ نه `ECONNREFUSED` یا parse error.
۳. Reviewer با `git diff -- tests` بررسی کند که assertion status، code و no-mutation وجود دارد. در یک checkout جدا یا با apply نکردن implementation، Red را تکرار کن.
۴. Implementer فقط فایلهای production مجاز را تغییر دهد؛ خروجی Green و Diff تولید کند.
۵. Reviewer تازه، test و production diff را بخواند و همان فرمان را تکرار کند.
**Recovery:** اگر baseline سبز شد، تست به revision اصلی متصل نیست؛ fixture یا test name را بررسی و Red را دوباره مشاهده کن. اگر failure زیرساختی بود، محیط ephemeral را repair کن و آن log را با Red قاطی نکن. اگر implementer test را تغییر داده، تغییر test را reject و چرخه را از commit test-author ادامه بده.
## تشخیص علت شکست با یک درخت تصمیم
پیش از آنکه failure را Red بنامیم، باید از سطح پایین به سطح رفتار حرکت کنیم. نخست exit code و نوع exception را ثبت کن. اگر parser، import یا dependency fail شده است، هنوز به رفتار نرسیدهایم و باید آن را بهعنوان خطای setup گزارش کنیم. اگر سرویس بالا نمیآید، health check و اتصال به fixture را جداگانه اجرا کن. تنها وقتی این دو لایه سالماند، متن assertion را بررسی کن. برای نمونه، `expected 409, received 200` دربارهٔ policy اطلاعات میدهد، اما `ECONNREFUSED` هیچ چیز دربارهٔ window reschedule نمیگوید.
در مرحلهٔ بعد، تست را با یک ورودی کنترلشده کوچک کن. یک appointment، یک actor، یک tenant و یک timestamp ثابت کافی است. حذف fixtureهای غیرضروری کمک میکند بفهمیم failure از کدام invariant آمده است. سپس یک mutation دستی کوچک روی production انجام بده: مثلاً شرط `status === "CONFIRMED"` را موقتاً حذف کن یا پاسخ را به `200` تبدیل کن. اگر تست پس از mutation همچنان سبز است، Oracle ضعیف است. این کار باید در محیط disposable انجام شود و mutation نباید به branch مشترک برسد؛ هدف سنجش قدرت تست است، نه اصلاح محصول.
artifactهایی که استقلال را قابلسنجش میکنند
برای هر نقش، artifact متفاوت تعریف کن. test author باید acceptance example، fixture، clock و assertionها را در یک تغییر ثبت کند. reviewer باید checklist خود و پرسشهای unresolved را بدون دستکاری تست بنویسد. implementer باید فقط به commit یا patch تست دسترسی داشته باشد و خروجی فرمان را با hash revision ثبت کند. در پایان، verifier باید بتواند با checkout یا worktree تازه همان Red را بازتولید کند. اگر تنها artifact یک خلاصهٔ متنی از Agent است، بررسی مستقل ممکن نیست؛ خلاصه ممکن است omission داشته باشد و جای diff و log را نمیگیرد.
برای جلوگیری از تغییر پنهانی تست، مسیر فایلهای مجاز را قبل از اجرا مشخص کن و بعد از هر مرحله `git diff -- tests` بگیر. اگر framework فایل snapshot تولید میکند، snapshot هم بخشی از test surface است و باید در review دیده شود. تغییر fixture بهظاهر بیخطر میتواند status، timezone یا مالکیت tenant را عوض کند و علت Red را از بین ببرد. هر تغییر در test author phase باید با دلیل و یک اجرای دوبارهٔ baseline همراه باشد.
## سناریوی مرزی: Red ناقص اما مفید
گاهی تست در revision اصلی با دو failure همزمان روبهرو میشود: پاسخ `200` بهجای `409` و audit ناقص بهدلیل fixture اشتباه. نباید کل تست را دور ریخت یا اولین failure را به رفتار نسبت داد. assertionها را موقتاً به مراحل مستقل تقسیم کن، fixture را با یک integration smoke test تأیید کن و سپس فقط یک ادعای رفتاری را نگه دار. در Evidence بنویس که کدام بخش Red معتبر بود و کدام بخش blocked ماند. این تفکیک از ادعای بیشازحد جلوگیری میکند و به implementer اجازه نمیدهد یک fix همزمان چند علت را پنهان کند.
همین قاعده برای تست asynchronous مهم است. `await` ناقص، promise بدون return یا timeout کوتاه میتواند تستی بسازد که گاهی سبز و گاهی قرمز است. قبل از نسبت دادن flaky failure به production، زمانبندی را با clock و retry کنترلشده بررسی کن؛ اما retry را درمان ندان. اگر اجرای دوم سبز شد، evidence همچنان نشان میدهد که failure ناپایدار بوده و Red رفتاری اثبات نشده است. تست باید آنقدر deterministic باشد که یک verifier تازه بتواند علت را ببیند.
## قرارداد تحویل بین نقشها
تحویل از test author به reviewer شامل این موارد است: لینک acceptance، دامنهٔ رفتار، non-goal، ورودیهای ثابت، Oracleهای مثبت و منفی، و فرمان دقیق. تحویل از reviewer به implementer باید verdict روشن داشته باشد؛ «بهنظر خوب است» مجوز نیست. implementer پس از تغییر باید بگوید چه فایلهایی تغییر کرده، چرا کمینه است، کدام تستها اجرا شده و چه چیزی هنوز ثابت نشده است. verifier نیز باید verdict را به `ACCEPT`، `CHANGES_REQUESTED` یا `BLOCKED` محدود کند و برای هر مورد artifact بدهد.
اگر تیم کوچک است و یک نفر هر سه نقش را انجام میدهد، استقلال را با فاصلهٔ زمانی و artifact جبران کن: تست را در یک commit جدا ثبت کن، baseline را قبل از implementation اجرا کن، و review نهایی را در session تازه انجام بده. این کار نقشها را واقعاً مستقل نمیکند، اما نقطهٔ تغییر assertion و علت تصمیم را قابلردگیری میسازد. هدف، تشریفات بیشتر نیست؛ هدف این است که سبز شدن تست با تغییر معیار اشتباه نشود.
## خطاهای رایج و نشانهها
- نقش reviewer همان implementer است و تغییر assertion در همان diff پنهان میشود.
- تست به mockی وصل است که رفتار مورد آزمایش را خودش برمیگرداند.
- فقط خروجی `toBeDefined` سنجیده میشود؛ قرارداد شکسته اما سبز است.
- baseline revision ثبت نشده؛ کسی نمیداند Red از کجا آمده است.
- تست integration با clock واقعی به نیمهشب وابسته است و failure ناپایدار دارد.
## چکلیست Verification
- [ ] hash revision اصلی و baseline ثبت شده است.
- [ ] Red علت رفتاری، نه محیطی، دارد.
- [ ] test author، reviewer و implementer مرز و artifact جدا دارند.
- [ ] status، code، state و audit طبق Oracle سنجیده میشوند.
- [ ] reviewer تازه فرمان را تکرار کرده است.
## تمرین مستقل
یک تست بنویس که ابتدا به دلیل `200` بهجای `409` fail شود و سپس نشان بدهد پس از اصلاح production، slot و audit تغییر نکردهاند.
**Rubric پاسخ:** fixture معتبر، assertion روی status/code، snapshot پیش/پس، عدم mock رفتار اصلی، ثبت baseline و توضیح Recovery لازم است.
## مرز ادعا و مقایسهٔ ابزار
جداکردن نقشها یک کنترل فرایندی است، نه ویژگی تضمینشدهٔ Cursor، Claude Code، Copilot یا Codex. میتوان از هرکدام برای author یا implementer استفاده کرد، اما reviewer باید به commit، Diff و فرمان واقعی دسترسی داشته باشد. یک پنجرهٔ تازه بدون artifact مستقل نیست؛ اگر همان خلاصه و همان test تغییرکرده را ببیند، استقلال فقط ظاهری است. مستند رسمی ابزار دربارهٔ workflow صحبت میکند، ولی صحت Oracle را به تیم واگذار میکند.
برای کاهش سوگیری، test author باید acceptance example و limitation را ثبت کند و implementer فقط مجوزهای همان Slice را داشته باشد. اگر نقشها بهدلیل اندازهٔ تیم یکی شدند، این استثنا را شفاف بنویس و یک verifier بعدی را اجباری کن. هدف حذف همکاری نیست؛ هدف قابلردگیری کردن نقطهای است که assertion میتوانست برای سبز شدن تغییر کند.
## جمعبندی
Red یک رنگ در خروجی نیست؛ ادعایی دربارهٔ علت شکست است. جداسازی نقشها و اجرای تست روی revision اصلی، احتمال self-approval و تست نمایشی را کم میکند.
## منابع مستقیم
- [Martin Fowler — Test-Driven Development](https://martinfowler.com/bliki/TestDrivenDevelopment.html) — چرخه و بازخورد کوتاه.
- [Vitest — Mocking](https://vitest.dev/guide/mocking.html) — مستند رسمی؛ mock باید scope و محدودیت داشته باشد.
- [Git documentation — git-diff](https://git-scm.com/docs/git-diff) — مبنای بررسی تغییر واقعی.
- [Anthropic — Claude Code best practices](https://www.anthropic.com/engineering/claude-code-best-practices) — توصیهٔ گزارش میدانی برای تست و بازبینی؛ استاندارد مستقل نیست.
به زبان ساده
Red واقعی باید به نبود behavior مربوط باشد، نه syntax، import شکسته یا database خاموش. جداکردن test author، reviewer و implementer احتمال تغییر پنهانی assertion برای سبزشدن را کم میکند.
مثال واقعی
در سناریوی فرضی دوره، reviewer باید روی revision اصلی `200` بهجای `409` را ببیند و علاوه بر response، ثابت کند slot، version و audit تغییر نکردهاند.