در TDD، تست فقط یک فایل کمکی نیست؛ قرارداد مشاهدهپذیر رفتار است. ایجنت میتواند نوشتن کد و اجرای فرمان را سریع کند، اما انسان باید رفتار مجاز، Oracle و مرز اختیار را تأیید کند. این درس چرخهٔ Red–Green–Refactor را با یک policy مستقل برای reschedule در `booking-platform` اجرا میکند.
## اهداف یادگیری
پس از درس میتوانی چرخه را با یک رفتار کوچک اجرا کنی، Red را پیش از پیادهسازی ببینی، Green را با کمترین کد بسازی، Refactor را زیر تست سبز انجام دهی و برای ایجنت Task Contract شامل Scope، ممنوعیت، فرمان و Evidence بنویسی.
## جایگاه در چرخهٔ سرتاسری
فصل ۵ رفتار reschedule را به Sliceهای کوچک رساند. اکنون در مرحلهٔ `Test` هستیم و پیش از `Implement`، مثال پذیرفتهشده را به تست executable تبدیل میکنیم. قرارداد فصل ۵ حفظ میشود: appointment باید `CONFIRMED` باشد، deadline دقیقاً ۲۴ ساعت است، زمان در timezone کلینیک تفسیر و در UTC ذخیره میشود، و درخواست بسته باید `409 RESCHEDULE_WINDOW_CLOSED` با body قراردادی بدهد، بدون mutation یا audit تازه.
*شکل: چرخهٔ TDD از رفتار reschedule تأییدشده تا Red، Green کمینه، Refactor و Verification کامل؛ اگر شاهد کافی نیست، چرخه به تست برمیگردد.*
## مفهوم عمیق
**Red** یعنی تستی که رفتار جدید را مطالبه میکند و به دلیل نبود آن رفتار شکست میخورد. **Green** یعنی کمترین پیادهسازی که همان Oracle را سبز میکند. **Refactor** تغییر ساختار بدون تغییر رفتار مشاهدهشده است. این سه واژه دستور «همیشه اول تست» نیستند؛ قرارداد بازخورد کوتاه و قابلتکرارند.
برای ایجنت، انسان باید پنج چیز را قبل از اجرا مشخص کند: رفتار، فایلهای مجاز، چیزهای ممنوع، فرمانهای Verification و نشانهٔ پایان. ایجنت میتواند factهای مخزن را بخواند و patch پیشنهاد کند، اما نباید policy reschedule را از خود اختراع کند. یک چرخه فقط یک رفتار باریک دارد؛ مثلاً «برای appointment تأییدشده، در پنجرهٔ بسته reschedule رد شود»، نه همزمان reminder، refund و calendar sync.
مثال بد
«قانون reschedule را پیاده کن و همهٔ تستها را سبز کن.»این درخواست چند policy حلنشده دارد: appointment باید confirmed باشد، مرز دقیق زمان، timezone، status/body پاسخ و اثر audit. معیار پایان نیز «سبز بودن» است و ایجنت میتواند assertion را حذف یا تست را mock کند.
مثال اصلاحشده: Vitest
import { describe, expect, it } from "vitest";
import { rescheduleDecision } from "./reschedule-policy";
describe("reschedule window", () => {
it("rejects reschedule at exactly 24 hours before start", () => {
const result = rescheduleDecision({
now: new Date("2026-09-10T10:00:00Z"),
startsAt: new Date("2026-09-11T10:00:00Z"),
status: "CONFIRMED",
});
expect(result).toEqual({
allowed: false,
status: 409,
body: { code: "RESCHEDULE_WINDOW_CLOSED" },
});
});
});در این مثال «دقیقاً ۲۴ ساعت»، `CONFIRMED` بودن appointment و همان status/body فصل ۵ صریح است. این policy مستقلِ reschedule است و cancellation تازهای به Canon اضافه نمیکند. تست policy pure است و بهتنهایی نبود mutation و audit را ثابت نمیکند؛ در Slice integration باید آن Oracleهای persistent نیز اضافه شوند.
## قرارداد همکاری انسان و Agent
Goal: تصمیم reschedule برای appointment CONFIRMED را در مرز 24 ساعت مشخص کن.
Context: packages/contracts و apps/api/src/modules/appointments را بخوان؛
contractهای فصل ۵ و timezone کلینیک authority هستند.
Allowed: فقط فایل policy و تست همان policy.
Forbidden: تغییر deadline، حذف assertion، mock کردن clock در تست integration،
migration، شبکه و ارسال notification.
Input: مثال acceptance A-04 برای `RESCHEDULE_WINDOW_CLOSED`.
Done: اول خروجی Red، سپس patch کمینه، خروجی Green و Diff خلاصهشده.
Recovery: اگر status یا timezone نامعلوم بود DECIDE بده و کد نزن.ایجنت باید هر مرحله را گزارش کند. انسان در Red میپرسد «چرا شکست خورد؟»، در Green «آیا بیش از حد پیادهسازی شده؟» و در Refactor «آیا Oracle یا contract تغییر نکرده است؟». این تقسیم کار، همکاری را به کنترل کیفیت قابلممیزی تبدیل میکند.
## سناریوی booking-platform
در Slice S2 از فصل ۵، eligibility پیش از protected write رخ میدهد. policy مستقل reschedule در این درس فقط مرز زمانی را به چرخه وصل میکند و cancellation یا reminder تازهای به Canon اضافه نمیکند. اگر `now` و `startsAt` در دو timezone متفاوت باشند، ورودی باید ابتدا به Instant معتبر تبدیل شود. policy نباید `tenantId` یا `actorId` را از body بپذیرد؛ آنها از credential میآیند. تصمیم unresolved دربارهٔ مرز inclusive/exclusive باید owner و deadline داشته باشد، نه حدس ایجنت.
## آزمایشگاه گامبهگام
**ورودی:** acceptance example A-04، appointment `CONFIRMED`، baseline reschedule policy، Node و Vitest محلی.
۱. فایل موجود policy و فرمان تست را کشف کن:
pnpm exec vitest run packages/contracts/src/reschedule-policy.test.ts**خروجی مورد انتظار:** baseline سبز یا فهرست شکستهای موجود که قبل از تغییر ثبت شده است.
۲. تست reschedule در مرز ۲۴ ساعت را بدون implementation اضافه کن. اجرا باید شکست معنادار بدهد:
FAIL expected allowed:false, received allowed:trueاین Red معتبر است چون assertion مربوط به رفتار جدید است، نه syntax یا import شکسته.
۳. کمترین implementation را اضافه کن و همان تست را اجرا کن. **خروجی:** `1 passed`.
۴. Refactor کوچک انجام بده؛ مثلاً نام `remainingMs` را به `remainingMilliseconds` تغییر بده و تست را دوباره اجرا کن.
**Recovery:** اگر baseline پیشاپیش fail است، به Red ساختگی دست نزن؛ commit یا log baseline را نگه دار، dependency را repair کن و چرخه را از یک محیط پاک تکرار کن. اگر ایجنت تست را تغییر داد، diff تست را revert و contract را دوباره تأیید کن.
## خطاهای رایج و نشانهها
- نوشتن implementation پیش از test؛ اولین اجرا هرگز Red واقعی ندارد.
- یک چرخهٔ بزرگ برای policy، repository و notification؛ علت شکست نامعلوم میشود.
- استفاده از `Date.now()` مستقیم؛ تست مرزی ناپایدار است.
- سبز کردن با تغییر expected؛ diff تست از diff production بزرگتر یا بیدلیل تغییر کرده است.
- مخلوط کردن unit policy با database؛ شکست شبکه یا fixture به Red نسبت داده میشود.
## چکلیست Verification
- [ ] acceptance example و مرز ۲۴ ساعت صریح است.
- [ ] Red قبل از implementation با علت رفتاری ثبت شده است.
- [ ] Green کمینه است و contract فصل ۵ را تغییر نمیدهد.
- [ ] Refactor فقط ساختار را تغییر داده و تستها دوباره اجرا شدهاند.
- [ ] فرمان، نسخهٔ محیط و خروجی در Evidence ثبت شدهاند.
## تمرین مستقل
برای «reschedule در ۲۴ ساعت و یک دقیقه مانده مجاز است» تست و قرارداد Agent بنویس.
**Rubric پاسخ:** باید Instantهای ثابت، status `CONFIRMED`، assertion روی `allowed:true` و body/status موفق فصل ۵، یک فرمان قابلتکرار، ممنوعیت تغییر deadline و Recovery برای timezone نامعلوم را داشته باشد.
## مرز ادعا و مقایسهٔ ابزار
Vitest در این درس runner و assertion فراهم میکند؛ سبز شدن آن دربارهٔ صحت timezone، امنیت endpoint یا نبود race ادعا نمیسازد. Cursor، Claude Code، Copilot و Codex ممکن است فرمان و سطح اختیار متفاوتی داشته باشند، اما قرارداد همکاری مستقل از نام ابزار است: Context محدود، مسیرهای مجاز، منع credential واقعی و خروجی قابلبررسی. هرچه ابزار اجرای خودکار بیشتری دارد، allowlist و توقف انسانی مهمتر میشود. قابلیت محصول را با روش TDD یکی ندان؛ مستند رسمی ابزار capability را توضیح میدهد، نه کیفیت این مخزن را تضمین کند.
در Evidence، تاریخ، نسخهٔ runner، commit و environment را بنویس. نتیجهٔ موفق روی laptop یا database mock را به staging تعمیم نده. اگر clock، timezone database یا locale فرق دارد، آن را limitation ثبت کن. این سطحبندی ادعا مانع میشود «تست داریم» بهاشتباه «رفتار production ثابت است» ترجمه شود.
## جمعبندی
TDD با ایجنت یعنی بازخورد را سریعتر و مرز اختیار را روشنتر کنیم، نه اینکه Oracle را واگذار کنیم. Red مشاهدهشده، Green کمینه و Refactor زیر تست سبز سه شاهد جدا هستند.
## منابع مستقیم
- [Martin Fowler — Test-Driven Development](https://martinfowler.com/bliki/TestDrivenDevelopment.html) — توضیح چرخه و محدودیت تعمیم آن به تیمها.
- [Kent Beck — Test-Driven Development: By Example](https://www.oreilly.com/library/view/test-driven-development/0321146530/) — منبع اولیهٔ روش.
- [Vitest — Guide](https://vitest.dev/guide/) — مستند رسمی فرمان و assertion؛ نسخهٔ پروژه باید ثبت شود.
- [Anthropic — Claude Code best practices](https://www.anthropic.com/engineering/claude-code-best-practices) — گزارش میدانی فروشنده، نه تضمین کیفیت Agent.
به زبان ساده
در TDD، Red شکست رفتاری پیش از implementation است، Green کمینهترین کد لازم و Refactor تغییر ساختار زیر همان Oracle است. Agent میتواند اجرای چرخه را سریع کند، اما deadline و policy را تعیین نمیکند.
مثال واقعی
تست مستقل دوره برای reschedule در دقیقاً ۲۴ ساعت مانده، `409 RESCHEDULE_WINDOW_CLOSED` و عدم تغییر state را میخواهد؛ این مثال آموزشی به implementation خاصی وابسته نیست.