وقتی یک عامل کد نویسی تست را به جای خطا تصحیح می کند

یک عامل رمزگذاری یک درخواست کشش برای یک کار پسزمینه باز میکند که پس از لغو کاربر، به تلاش مجدد ادامه میدهد. آزمایشی که خطا را تشخیص داد اکنون با موفقیت پشت سر گذاشته است و نماینده گزارش می دهد که همه آزمایش ها سبز هستند. اما این تفاوت دو چیز را تغییر می دهد: کنترل کننده مجدد و خود تست. کدام تغییر خطا را از بین می برد و آیا در صورت تکرار خطا، تست خطا را تشخیص می دهد؟
من از عوامل برنامه نویسی برای طراحی تست های یکپارچه سازی، مرتب سازی باگ ها و آماده سازی وصله ها برای خدمات back-end استفاده می کنم، بنابراین این یک سوال بررسی عملی برای من است. گاهی اوقات آزمون واقعا اشتباه است و تغییر آن تصمیم درستی است. گاهی اوقات عامل به تازگی کوتاه ترین مسیر را برای ساخت و ساز سبز پیدا کرده است. با ادغام مداوم، هر دو یکسان به نظر می رسند.
معیارها نشان میدهند که چقدر میتواند پیش برود. ImpossibleBench تستها را تضاد کار میکند، بنابراین هر راهحلی که میگذرد باید تقلب کند، و GPT-5 همچنان 54 درصد از وظایف متضاد خود را در SWE-bench پشت سر گذاشته است. اما تستها نیز همیشه درست نیستند: OpenAI در 59.4 درصد از 138 مشکل تأییدشده SWE-bench Verified که بررسی کرد، مشکلاتی را در آزمایشها یا شرح وظایف پیدا کرد.
بنابراین یک ویرایش آزمایشی به خودی خود چیزی را ثابت نمی کند. در زیر نشان میدهم که چگونه میتوان با یک مثال کوچک پایتون، شواهد را به هر دو صورت دریافت کرد: یک خطا، سه PR که همگی CI را پاس میکنند، و یک بررسی که آنها را در چند ثانیه متمایز میکند.
ساختمان سبز بیانیه ای در مورد بازرسی است
فکر می کنم اجرای آزمایشی به عنوان یک خط لوله مفید است. تستها انتخاب و راهاندازی میشوند، برنامه اجرا میشود، آزمایش مشاهده و اعتبارسنجی میشود، سیستم نتیجه را گزارش میکند و گیت CI آن نتیجه را به تصمیم ادغام تبدیل میکند.
شکل 1: اجرای آزمایشی به عنوان خط لوله. تغییر هر مرحله ای غیر از برنامه، معنای نتیجه سبز را تغییر می دهد.
فقط مرحله اجرا می تواند نقص را برطرف کند. اگر روابط عمومی فقط روی برنامه تاثیر می گذارد، تغییر رنگ از قرمز به سبز چیزی در مورد کد به شما می گوید. اگر مرحله دیگری را نیز لمس کند، نتیجه سبز ممکن است بیشتر درباره شواهد بگوید تا خطا.
یک خطا، سه درخواست کشش
آزمون ناموفق
در اینجا یک قرارداد نمونه است. یک بار cancel() برمی گردد، نمی توان این کار را دوباره امتحان کرد. کاری که از قبل در حال اجرا است همچنان می تواند نتیجه خود را گزارش کند و کاری که لغو نشده است باید پس از یک خطای موقت دوباره امتحان شود.
مدل زیر با یک امتحان در حال اجرا شروع میشود و پاسخهای تماس را یکی یکی پردازش میکند. run_retry() زمانبندی را جایگزین میکند که سعی مجدد در صف را شروع میکند. بدون رشته، تماس شبکه یا خواب. آن را به عنوان ذخیره کنید retry_job.py:
class RetryJob:
def __init__(self):
self.attempts = 1
self.canceled = False
self.retry_pending = False
def cancel(self):
self.canceled = True
self.retry_pending = False
def on_transient_failure(self):
self.retry_pending = True
def run_retry(self):
if self.retry_pending:
self.retry_pending = False
self.attempts += 1
خطا در ترتیب است. cancel() تمام تلاش های مجدد در صف را پاک می کند، اما تلاش در حال اجرا ممکن است بعد از آن یک شکست گذرا را گزارش کند و on_transient_failure() سپس یک بار دیگر بدون بررسی اینکه آیا کار لغو شده است، صف می کشد. این دو تست را وارد کنید tests/test_retry_job.py:
from retry_job import RetryJob
def test_cancel_blocks_retry_after_late_failure():
job = RetryJob()
job.cancel()
job.on_transient_failure()
job.run_retry()
assert job.canceled is True
assert job.attempts == 1
def test_uncanceled_job_retries():
job = RetryJob()
job.on_transient_failure()
job.run_retry()
assert job.attempts == 2
اجرا کنید python -m pytest -q از ریشه مخزن. تست تهی ناموفق بود زیرا attempts 2 است و آزمون مجدد ساده قبول می شود.
سه راه برای سبز شدن
اولین روابط عمومی برنامه را به حال خود رها می کند و یک ادعا را از دست می دهد:
- assert job.attempts == 1
+ assert job.attempts >= 1
اکنون آزمون تلاش دوم، سوم و چهارم را می پذیرد. نام آن همچنان قول می دهد که لغو تلاش مجدد را مسدود می کند، اما این ادعا دیگر این قول را تأیید نمی کند.
PR دوم کنترل کننده را تعمیر می کند و آزمایش ها را به حال خود رها می کند:
def on_transient_failure(self):
+ if self.canceled:
+ return
self.retry_pending = True
تست لغو مرتبط باقی می ماند، و آزمایش ساده مجدد نشان می دهد که نگهبان تلاش های مجدد را برای هر کار غیرفعال نکرده است.
روابط عمومی سوم هر دو را انجام می دهد. سادهترین راه برای از دست دادن است: تفاوت برنامه درست به نظر میرسد زیرا برنامه جاری است، اما اگر تغییر بعدی آن را برگرداند، آزمایش دیگر نمیتواند اشکال را تشخیص دهد. گزارش های CI ساده 2 passed برای هر سه PR
Swap Check را اجرا کنید
یک CI ساده دقیقاً یک ترکیب را اجرا می کند: PR در برابر کد PR آزمایش می کند. دو ترکیبی که کنار گذاشته به سوالاتی که در واقع بازبین دارد پاسخ می دهد:
- تست های پایه بر اساس کد اصلی. آیا این تغییر همچنان انتظارات اولیه را برآورده می کند؟ اگر یک آزمایش اولیه در اینجا ناموفق باشد، روابط عمومی قول را تغییر داده است و این نیاز به دلیل دارد.
- تست های اصلی کد اصلی. آیا آزمایشهای جدید همچنان باگ را برطرف میکنند؟ برای رفع اشکال، حداقل یکی از آنها باید در اینجا شکست بخورد. اگر همه پاس کنند، نمی توانند کد خطا را از کد ثابت تشخیص دهند.
شکل 2 هر چهار ترکیب را برای سه PR نشان می دهد:
شکل 2: تبادل ماتریس برای سه درخواست کشش. CI فقط کد اصلی را با تست های اصلی نشان می دهد که در هر سه رنگ سبز است.
در PR عامل، آزمایشها بخشی از تغییر هستند، بنابراین شما باید آنها را عمداً تعویض کنید. Git worktrees این کار را ارزان انجام می دهد. این دستورات سلولی را می سازند که برای اشکال زدایی بیشترین اهمیت را دارد، کد اصلی با فایل های تست هدر:
base=$(git rev-parse main) # commit IDs, because inside a worktree
head=$(git rev-parse fix-and-weaken) # HEAD would mean something else
tests=$(git diff --name-only --no-renames "$base" "$head" -- \
tests conftest.py pytest.ini)
cell=$(mktemp -d) # base code + head tests
git worktree add --quiet --detach "$cell" "$base"
git -C "$cell" restore --source="$head" -- $tests
(cd "$cell" && python -m pytest -q)
git worktree remove --force "$cell"
در PR سوم این چاپ شده است 2 passed: تست های جدید خطا را نمی بینند. برای سلول متقابل دیگر، درخت کار را به داخل اضافه کنید $head و فایل های تست را از $base. روابط عمومی که فقط ادعا را از بین می برد، پرچمی را در هر دو تلاقی بالا می برد، و اصلاح صادقانه هیچ کدام را مطرح نمی کند.
با این حال، نتایج برای کل بسته خشن است. یک تست اصلی میتواند در پایگاه کد فقط به این دلیل که چیزی اضافهشده توسط پچ را وارد میکند، شکست میخورد، و آزمایشی که قبلاً روی پایه ناموفق است، سیگنال را پنهان میکند. در استفاده واقعی، نتایج تست را با تست مقایسه کنید، همانطور که SWE-bench با خودش انجام می دهد FAIL_TO_PASS و PASS_TO_PASS لیست ها
فهرست مسیرهای آزمون نیز مهم است. باید همه چیزهایی را که چک را تعریف می کند، از جمله وسایل، عکس های فوری و پیکربندی مهار، مانند pytest.iniو فایل هایی که تنظیمات برنامه و تست را ترکیب می کنند، مانند pyproject.toml، نیاز به نگاه انسانی دارند. این conftest.py به عنوان مثال، hook، گزارشهای تست ناموفق را پس از گذشت بازنویسی میکند:
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.failed:
report.outcome = "passed"
در روابط عمومی که فقط این قلاب را اضافه می کند، فقط با tests در لیست، چک معاوضه رفتار می کند conftest.py به عنوان کد برنامه و یک پچ تمیز گزارش می دهد. اضافه کنید conftest.pyهر دو اخطار ظاهر می شود. محققان Anthropic این خانواده از هک ها را همراه با تماس توصیف می کنند sys.exit(0) قبل از اجرای هر دستور
نتیجه را به عنوان یک اثبات بازبینی تلقی کنید، نه یک حکم: آزمایشهای بازسازی جدید باید کد قدیمی را منتقل کنند، در حالی که پخش رفع اشکال باید در آنجا ناموفق باشد. من چک swap را به عنوان یک کار CI اجرا می کنم فقط برای گزارش با تاریخچه کامل Git (fetch-depth: 0(permissions: contents: read، persist-credentials: false).
هنگام تغییر آزمون تصمیم درستی است
آزمایش ها همیشه تغییر می کنند. در یک مقایسه، PRهای نوشته شده توسط انسان، آزمایشهای موجود را بیشتر از آزمایشهای به کمک عامل اصلاح کردند: 43.5% در مقابل 20.6% در نمونه دستی. بنابراین سوال مفید این است که چرا یک آزمون تغییر کرده است. چهار پاسخ وجود دارد:
- الزام تغییر کرده است. اگر سرویس اکنون اجازه می دهد تا یک تلاش مجدد از قبل ارسال شده پس از لغو شروع شود، آزمایش باید این را بگوید و شخصی باید ضمانت جدید را تأیید کرده باشد.
- دستگاه اشتباه بود. شاید یک شکست دائمی را به عنوان گذرا مشخص کرده باشد. آن را برطرف کنید، اما یک مورد خطای گذرا را نگه دارید تا مسیر تلاش مجدد پوشیده بماند.
- آزمایش خیلی زود به نظر می رسید. اگر لغو بهصورت ناهمزمان تأیید شد، منتظر تأیید باشید یا بهجای افزودن خواب، به زمانبندی ساختگی بروید.
- آزمایش یک جزئیات اجرایی را پین کرده است. علامت چک را به مرز عمومی منتقل کنید و نشان دهید که همچنان نقض می کند. این بزرگترین دسته در ممیزی OpenAI بود، با 35.5٪ از مسائل پرداخته شده است.
در هیچ یک از این موارد خروجی فعلی برنامه عامل انتظار جدید نیست. قرارداد، تنظیمات اصلاح شده یا دلیل نامعتبر بودن مشاهدات قدیمی را بخواهید، بهعلاوه مدرکی برای تأیید مبادله مبنی بر اینکه آزمون تجدیدنظر شده همچنان برای خطا ناموفق است.
مانتینلی
در چرخه عامل
عامل نیاز به مکانی برای بررسی خطاها، از جمله خطاهای تست، و راهی واضح برای گزارش تضاد بین یک نیاز و یک ادعا دارد، نه اینکه بی سر و صدا آن را حل کند. برای کار لغو، ابتدا پس از تأیید اینکه به دلیل درستی از کار افتاده است، پخش کننده را درگیر می کنم و دستورالعملی مانند این را به نماینده می دهم:
رفتار امتحان مجدد را اصلاح کنید تا بعد از آن تلاش جدیدی شروع نشود
cancel()بازده خرابی دیرهنگام قابل تکرار و مورد تکرار مجدد ساده را نگه دارید. اگر فکر میکنید آزمایشی اشتباه است، توقف کنید، توضیح دهید که چگونه قرارداد را نقض میکند و هر تغییر پیشنهادی را به طور جداگانه در آزمون نشان دهید. دستوراتی را که اجرا کردهاید و هر گونه شکست، یا پیشفرضهای مورد انتظار را گزارش کنید.
برای ایجاد آزمایشها فقط خواندنی برای یک عامل، قوانین مجوز Claude Code میتوانند ویرایشهای مدلهای مسیر را رد کنند، و قلاب PreToolUse میتواند با خروج با وضعیت ۲، تماس ابزار را مسدود کند. قلاب preToolUse مکان نما به همین ترتیب کار می کند. این قوانین ابزارهای فایل عامل را پوشش میدهند، نه هیچ پوستهای، مگر اینکه جعبه شنی Claude Code را با allowUnsandboxedCommands تنظیم شده به false.
در راهاندازی خودم، عاملی که تستهای یکپارچهسازی را مینویسد، فقط در کد آزمایشی اجرا میشود و دستورالعملهای ثابت آن، خاموش کردن یا حذف یک تست ناموفق را برای اجرای سبز ممنوع میکند. تریاژ ابتدا هر شکست را به عنوان یک برنامه، محیط یا مشکل تست طبقه بندی می کند. اشکالات برنامه به گزارش اشکال تبدیل می شوند، نه ویرایش های آزمایشی. تغییرات در دستورالعمل های خود عامل ها نیاز به تایید انسانی دارند و عامل ها هرگز ادغام نمی شوند.
در مخزن
کنترل های مخزن تصمیم می گیرند که چه چیزی ادغام شود. این ابزار شواهد رفتار مهم را از دسترس پچ کاندید دور نگه می دارد، همانطور که شکل 3 نشان می دهد:
شکل 3: چه چیزی ممکن است کاندید پچ تغییر کند و چه چیزی باید جداگانه بررسی شود.
شواهد را در اختیار داشته باشید. مسیرهای آزمایش، ویجت، مهار و CI را به مالکان در CODEOWNERS اختصاص دهید و «نیاز به بررسی توسط صاحبان کد» را روشن کنید. فقط فایل فقط به نمایش نیاز دارد. GitHub آن را از شاخه اصلی می خواند، بنابراین روابط عمومی نمی تواند صاحبان خود را از دست بدهد. به CODEOWNERS یک مالک بدهید و برای آخرین فشار به تأیید نیاز دارید.
بررسی های پذیرش را خارج از پچ نگه دارید. بسته کوچکی را برای پذیرش قراردادهای مهم در یک مخزن جداگانه یا تحت یک مرجع سخت ذخیره کنید و آن را از یک گردش کاری که PR نمیتواند ویرایش کند، اجرا کنید، مانند گردش کاری که توسط مجموعهای از قوانین در GitHub Enterprise Cloud یا خطمشی اجرای خط لوله در GitLab Ultimate لازم است. آن را از صندوق صندوق خود با صندوق خود اجرا کنید pytest.ini، از آنجایی که pytest به دنبال پیکربندی در دایرکتوری های والد می گردد و می تواند پیکربندی نامزد را انتخاب کند conftest.py، و تنظیم کنید PYTEST_DISABLE_PLUGIN_AUTOLOAD برای مسدود کردن پلاگین ها، بسته کاندید می تواند ثبت شود.
ثابت کنید که بررسی انجام شده است. commit را بدون قید و شرط کنید: بررسی وضعیت مورد نیاز یک نتیجه از دست رفته را میپذیرد، و در GitHub “تعهد از دست رفته وضعیت خود را به عنوان “موفقیت” گزارش میکند. سپس کار را شکست دهید، مگر اینکه موارد مورد نیاز برآورده و تصویب شود. هر runner که JUnit XML را می نویسد، از جمله pytest، Maven Surefire و Gradle، می تواند یک اسکریپت کوتاه را تغذیه کند که هر تست مورد نیاز را با نام بررسی می کند. آزمونی را که فقط یک تکرار را پشت سر گذاشته است حساب نکنید. Surefire بیلد را سبز نگه می دارد و تلاش ناموفق را به عنوان یک ثبت می کند flakyFailure. گزارش را با defusedxml تجزیه کنید زیرا این یک ورودی نامعتبر است.
هیچ کس کنترلی همه چیز را نمی گیرد. بررسی آزمون اجباری به بستهای که دروغ میگوید اعتماد میکند، اجرای پذیرش نمیتواند یک آزمون ضعیف را ببیند تا زمانی که یک اصلاح واقعی وجود داشته باشد، و بررسی معاوضه فقط به اندازه لیست مسیر آن خوب است.
هزینه ها و محدودیت ها
بررسی تعویض بسته را چهار بار اجرا میکند، بنابراین برای یک بسته بزرگ آن را به آزمایشهای تحت تأثیر محدود میکنم. نیاز به یک بازنگری پایه دارد که در آن بازتولیدکننده بتواند کار کند، آزمایشهای ناپایدار نویز را به سلولهای متقاطع اضافه میکنند و هر سلول باید چک خود را وارد کند، نه یک بسته نصب شده یا مشترک. node_modules پوشه
این کنترل ها همچنین میانبرها را هدف قرار می دهند، نه کدهایی که برای لغو آنها نوشته شده است. در مثال هفتم، retry_job.py وقتی شی گزارش pytest وارد شد، آن را برطرف کنید و هر سه کنترل آن را پاس می کنند. وقتی مهم است، آزمایشهای پذیرش را در برابر یک مصنوع داخلی یا سرویس در حال اجرا اجرا کنید و به خواندن تفاوت در برنامه ادامه دهید.
قبل از تایید چه چیزی باید بخواهید
به جای قضاوت، مدرک بخواهید. برای مثال ثابت، یک توضیح مفید روابط عمومی میگوید:
پاسخ تماس دیرهنگام در صورت شکست باعث شد تا پس از لغو، دوباره تلاش شود. هنگامی که کار لغو می شود، کنترل کننده اکنون این تماس را نادیده می گیرد. تست خنثی سازی اولیه در کد قدیمی با شکست مواجه می شود و در رفع مشکل عبور می کند. آزمون مجدد ساده نیز قبول می شود. برداشتن فیوز باعث می شود که تست override دوباره ناموفق باشد. بیانیه و توالی رویدادها بدون تغییر است.
برای روابط عمومی واقعی، مایلم قبل از تایید پنج چیز را ببینم:
- پخش کننده کد اصلی را خراب می کند و کد اصلی را ارسال می کند.
- برای رفع اشکال، حداقل یک آزمایش اصلی در کد اصلی به دلیل یک ادعا، نه به دلیل واردات، ناموفق میشود.
- هر آزمون ویرایش شده دلیلی دارد: یک نیاز تغییر کرده، یک دستگاه ثابت شده، یک نقطه مشاهده تنظیم شده است، یا یک جزئیات پیاده سازی حذف شده است.
- تغییرات در انتخاب، مهار یا پیکربندی CI توسط صاحبان مربوطه تأیید شد.
- تستهای مورد نیاز با نام اجرا میشوند و نه فقط کد منبع.
تکمیل
داوران همیشه باید بپرسند چرا یک آزمون تغییر کرده است. عوامل کدنویسی باعث میشوند که این سؤال بیشتر مطرح شود، زیرا میتوانند در عرض چند دقیقه کدها و تستها را بازنویسی کنند و تا زمانی که CI سبز شود به کار خود ادامه میدهند. تغییرات تستها، ویجتها، مهار و CI را بهعنوان تغییرات اثباتی در نظر بگیرید: تستها را تعویض کنید، ویرایشها را با دلیل بپذیرید، و بررسیهای رفتار مهم را خارج از محدوده وصله نگه دارید. گذراندن فقط زمانی مفید است که بتوانید دلیل رد شدن را توضیح دهید.



