تصور کنید دهمین بار است که یک تستِ نامرتبط، مانع از ادغام کد شما در پروژه میشود و تنها راه نجات، حذف کامل آن تست است. این اقدام در لحظه جذاب است، اما در واقعیت، شما در حال پاک کردن حافظهٔ سیستمی خود هستید. حذف یک تست ناپایدار (Flaky) در خط لوله LLM، شاید ده دقیقه بعد از اینکه سومین Pull Request نامرتبط را مسدود کرد، تنها راه حل به نظر برسد، اما این یک شکست استراتژیک است. این تستها در واقع درکهای خاصی از سیستم را کدگذاری میکنند که بازیابی آنها بسیار دشوار است.
توسعهدهندگان اکنون با چالشی جدی روبروند: چگونه الگوهای معماری را تست کنند بدون اینکه خروجیهای غیرقطعی هوش مصنوعی زاینده (Generative AI) — شبیه به پیشبینی وضعیت آبوهوا که هر لحظه ممکن است تغییر کند — چرخه توسعه را فلج کند. همانطور که در تحلیل قبلی ما دربارهی الگوهای مهار توهم اشاره کردیم، ساختار درست مدلها تنها نیمی از راه است؛ نیمه دیگر، اعتبارسنجی آنهاست. طبق راهنمایی که در ۱۵ اوت ۲۰۲۶ در وبسایت dev.to منتشر شد، استفاده از دکوراتورهای «Skip» یا نادیده گرفتن تستها، جایگزینی خطرناک برای یک سامانهٔ رسمی قرنطینه است.
شکست دکوراتورهای Skip
استفاده از ابزارهایی مثل @pytest.mark.skip یا test.skip تست را بهطور کامل از چرخه اجرا حذف میکند. این کار مشکل مسدود شدن ادغام (Merge) را حل میکند، اما یک خلأ دادهای بحرانی ایجاد میکند. شما دیگر نمیدانید یک تست از هر ۵۰ بار اجرا یکبار شکست میخورد یا از هر ۲ بار.
به نقل از مستندات فنی این راهنما، بدون دادههای اجرایی، تشخیص این موضوع غیرممکن است که شکست ناشی از تغییر در پرامپت است، یا بهروزرسانی مدل در سمت ارائهدهنده، و یا یک خرابی کامل در قابلیتهای سیستم. در واقع، شما هیچ راهی ندارید که بفهمید آیا آن ویژگی که تست قرار بود از آن محافظت کند، سه هفته پیش بهطور کامل از کار افتاده است یا خیر.
یک تستِ Skip شده، پس از یک اسپرینت، عملاً با تست حذفشده فرقی نمیکند و لیستهای Skip بهطور نامحدود رشد میکنند بدون آنکه هرگز کوچک شوند.
مکانیزم قرنطینه
قرنطینه با Skip متفاوت است؛ در اینجا تست در هر Commit فعال میماند، اما قدرت مسدود کردن گیتِ ادغام را از دست میدهد. این یک تصمیم در سطح پیکربندی CI (یکپارچهسازی مداوم) است، نه تغییری در کدِ تست، بنابراین بهراحتی قابل بازگشت است.
این رویکرد هزینهٔ زمانی اضافی در CI را توجیه میکند، زیرا تست قرنطینه شده تنها ابزاری است که آن ویژگی خاص را رصد میکند. اگر یک قابلیت بهطور کامل خراب شود، تست از حالت «گاهی قرمز» به «همیشه قرمز» تغییر میکند؛ تغییری که تنها در صورت اجرای واقعی تست قابل شناسایی است.
یک Skip فارق نمیکند که ویژگی بهطور کامل کار کند یا کلاً حذف شده باشد؛ خروجی در هر دو حالت یکسان است. به همین دلیل است که لیست Skip یک نسخه سبکتر از قرنطینه نیست، بلکه ابزاری کاملاً متفاوت و بدتر است.
پیادهسازی فنی در Pytest
برای اجرای این مدل در pytest، توسعهدهندگان باید از یک مارکر ثبتشده استفاده کنند تا از خطاهای تایپی خاموش جلوگیری شود. پیکربندی در pyproject.toml باید شامل --strict-markers باشد تا هر مارکر ثبتنشده منجر به خطا شود، نه صرفاً یک هشدار.
# pyproject.toml
[tool.pytest.ini_options]
addopts = "--strict-markers"
markers = [
"quarantined(owner, since, reason): known-flaky; runs, but does not gate a merge",
]
مارکرها باید متادیتا را مستقیماً حمل کنند، زیرا کامنتها قابل کوئری گرفتن نیستند. یک مارکر استاندارد شامل موارد زیر است:
- مالک (Owner): نام یک تیم (نه فرد) برای تضمین تداوم، زیرا افراد شرکت را ترک میکنند.
- تاریخ (Since): تاریخ قرنطینه شدن (مثلاً ۲۰۲۶-۰۷-۱۴).
- دلیل (Reason): شرح دقیق ناپایداری، مثلاً «در ورودیهای طولانی، tool_choice=auto گاهی هیچ ابزاری فراخوانی نمیکند».
در این تستها، ادعاها (Assertions) باید روی ویژگیهای ساختاری متمرکز باشند نه متن دقیق. برای مثال، بررسی اینکه نام ابزار «lookup_order» است و تعداد فراخوانیها درست است، یک ادعای ساختاری است. اگر تست روی متن دقیق (Prose) متمرکز باشد، قرنطینه تنها درمان symptom است، نه علت.
تفکیک خط لوله CI
این سامانه به دو فراخوانی مجزا با انتخابگرهای مکمل نیاز دارد. اولین Job، گیتِ ادغام است و تستهای قرنطینه شده را حذف میکند: pytest -m "not quarantined" --junitxml=reports/gate.xml.
دومین Job فقط تستهای قرنطینه شده را اجرا کرده و نتایج را در یک فایل JUnit XML جداگانه ثبت میکند. این Job در پیکربندی CI به عنوان non-blocking علامتگذاری میشود تا وضعیت خروجی برای داشبوردها حفظ شود. در GitHub Actions از continue-on-error: true و در GitLab CI از allow_failure: true استفاده میشود. استفاده از || true توصیه نمیشود زیرا وضعیت خروجی (Exit Status) را دور میزند و دور میاندازد.
برای مدیریت ناپایداری، استفاده از پلاگین pytest-rerunfailures توصیه میشود تا بتوان از پرچمهای زیر استفاده کرد:
--reruns 3: تست را تا سه بار مجدداً اجرا میکند.--reruns-delay: یک تأخیر بین تلاشهای مجدد ایجاد میکند.--only-rerun: تلاش مجدد را به یک Exception خاص محدود میکند.
هر دو فایل JUnit XML باید به عنوان build artifacts آپلود شوند تا به عنوان ورودی برای داشبورد ناپایداری (Flake Dashboard) عمل کنند.
انطباق با Vitest
از آنجا که Vitest سیستم مارکر ندارد، رویکرد مبتنی بر مسیر (Path-based) پیشنهاد میشود. قرار دادن تستها در دایرکتوری __quarantine__ اجازه میدهد بهراحتی از طریق glob patterns در vitest.config.ts یا خط فرمان، آنها را مدیریت کرد.
// package.json
{
"scripts": {
"test:gate": "vitest run --exclude '**/__quarantine__/**'",
"test:quarantine": "vitest run --dir src --include '**/__quarantine__/**' --reporter=junit --outputFile=reports/quarantine.xml"
}
}
قراردادهای مسیر بر قراردادهای نامگذاری برتری دارند، زیرا با تغییر نام فایل از بین نمیروند و در لیست دایرکتوریها قابل مشاهدهاند. این دیدهشدن، فشار لازم را به تیمها وارد میکند تا لیست قرنطینه را پاکسازی کنند. نسخههای اخیر Vitest همچنین اجازه میدهند از طریق یک آرگومان شیء در test(name, options, fn)، منطق retry را برای هر تست بهطور مجزا تعریف کنید.
جلوگیری از «گورستان قرنطینه»
قرنطینه اگر مدیریت نشود، بهراحتی به یک گورستان برندشده تبدیل میشود. برای جلوگیری از این اتفاق، سه قانون لازم است:
۱. مالکیت تیمی: هر تست باید متعلق به یک تیم باشد، زیرا افراد شرکت را ترک میکنند.
۲. تاریخ انقضا: اگر مارکری قدیمیتر از بازه توافقشده باشد، Build باید شکست بخورد تا تیم مجبور شود تصمیم بگیرد: یا تست را اصلاح کند، یا ادعا را بازنویسی کند، یا تست را با یک پیام Commit شفاف حذف کند.
۳. سقف تعداد: محدود کردن تعداد تستهای قرنطینه شده (مثلاً ۱۵ مورد) تیمها را مجبور به اولویتبندی میکند. تیمها باید اصلاحات را بر اساس «امتیاز ناپایداری» رتبهبندی کنند، نه ترتیب فایلها.
برای جلوگیری از «پوسیدگی خاموش»، Job قرنطینه باید تضمین کند که واقعاً در حال اجرای تستها است. اگر کد فراخوانی شده بازنویسی (Refactor) شود، ممکن است Job در Import شکست بخورد و چون اجازه شکست دارد، نادیده گرفته شود. برای مقابله با این موضوع، کد خروجی ۵ در pytest (که نشاندهنده عدم جمعآوری هیچ تستی است) باید به عنوان یک خطای سخت (Hard Failure) در نظر گرفته شود.
تفکیک ناپایداری از رگرسیون
یک قانون حیاتی این است: تستی که در هر بار اجرا شکست میخورد، هرگز نباید قرنطینه شود. این یک رگرسیون (Regression) است، نه ناپایداری. این گرانترین اشتباه ممکن است، زیرا Job قرنطینه اجازه شکست دارد و بهندرت خوانده میشود.
توسعهدهندگان باید تست شکستخورده را چندین بار اجرا کنند تا مطمئن شوند گاهی پاس میشود و سپس مارکر قرنطینه را اعمال کنند. این فرآیند معمولاً دو دقیقه زمان میبرد و تنها راه تشخیص یک تست ناپایدار از یک خرابی واقعی است.
از آنجا که نام پرچمهای پلاگین و کلیدهای CI تغییر میکنند، توسعهدهندگان باید پیش از پیادهسازی، املای فعلی را در مستندات pytest-rerunfailures و CLI ویتست بررسی کنند.
گام بعدی شما
- بررسی لیست تستهای Skip شده در پروژه خود و تبدیل آنها به سیستم قرنطینه.
- تعریف یک مالک تیمی و تاریخ انقضا برای هر تست ناپایدار جهت جلوگیری از انباشت.
- پیادهسازی دو Job مجزا در CI برای تفکیک تستهای گیت از تستهای رصدگر.
اما مدیریت این تستها تنها بخشی از چالش است؛ برای درک اینکه چگونه میتوان دقت استنتاج را بدون افزایش هزینه بالا برد، به تحلیل ما دربارهی تکنیکهای Distillation مراجعه کنید.




گفتگو