یک تیک سبز در خط لولهی CI/CD لزوماً به معنای سلامت کد نیست و گاهی بزرگترین دروغ یک توسعهدهنده است. در تیم AI Flip Room که در حال ساخت ابزاری برای استیجینگ مجازی با هوش مصنوعی است، متوجه شدند ۴۴ مورد از ۷۰ حفاظ (Guards) آنها، هنگام تخریب عمدی کد هیچ واکنشی نشان ندادند.
این ابزار به کاربران اجازه میدهد عکسهای اتاق را برای بازطراحی با هوش مصنوعی آپلود کنند و از مجموعهای از بررسیها برای مقایسه نتایج با تصاویر اصلی استفاده میکند. برای محافظت از این بررسیها، قوانین پرداخت و منطق رضایت کاربر، تیم از «حفاظها» استفاده میکند؛ اسکریپتهای مستقل Node.js (با پسوندهای .mjs یا .mts) که یک رفتار خاص را تأیید میکنند و اگر آن رفتار از بین برود، با خروجی غیرصفر متوقف میشوند.
بسیاری از برنامهنویسان برای اطمینان از اینکه منطقهای حیاتی — مانند قوانین صورتحساب یا رضایت حریم خصوصی — در حین بازنویسی کد (Refactor) دچار رگرسیون (Regression) — شبیه به وقتی که تعمیر یک لولهی آب باعث نشتی در جای دیگری از خانه شود — نمیشوند، به تستهای خودکار تکیه میکنند. اما طبق گزارش تیم AI Flip Room، حفاظهای آنها بهجای بررسی عملکرد واقعی، صرفاً چک میکردند که آیا کلمات خاصی در سورسکد وجود دارند یا خیر. این شکاف باعث میشد خطاهای بحرانی به محیط تولید (Production) راه یابند، در حالی که داشبورد تست همچنان سبز بود. این وضعیت یادآور اهمیت متدولوژی «تست قرمز اول» است که تأکید میکند برای اطمینان از کارکرد تست، ابتدا باید سناریویی طراحی کرد که تست را با شکست مواجه کند.
همانطور که در تحلیلهای قبلی ما دربارهی امنیت مدلهای بازمتن اشاره کردیم، اعتماد کورکورانه به ابزارهای نظارتی بدون تست استرس، ریسکهای سیستمی ایجاد میکند.
زمینه و بستر شکست
قبل از این حسابرسی، تیم با هرجومرج شدیدی در اجرا روبرو بود. تا ۲۲ اوت ۲۰۲۴، هیچ سیستم رسمی برای اجرای حفاظها وجود نداشت؛ لیست حفاظها در یک فایل Markdown نگهداری میشد که تنها ۳۳ مورد از ۷۸ اسکریپت موجود در دیسک را نام برده بود. وقتی سرانجام یک Runner برای مقایسه این لیست با فایلهای موجود پیادهسازی شد، وضعیت فاجعهبار آشکار گشت: یک حفاظ بدون اینکه کسی متوجه شود در ۱۵ مورد از ۱۸ چک قرمز بود، ۲۷ حفاظ فعال ماهها اجرا نشده بودند و ۹ اسکریپت در دیسک موجود بودند اما در Git ثبت نشده بودند.
وقتی همه چیز اجرا شد و سبز گشت، تیم گمان کرد کیفیت تضمین شده است. اما آنها اشتباه میکردند. دو روز بعد، یک بررسی مستقل با تغییر نام یک متغیر محیطی (Environment Variable) که حامل DSN است، یک غلط تایپی کوچک در پیکربندی ردیاب خطا ایجاد کرد. این یک اشتباه رایج در هنگام تغییر نام متغیرهاست. با وجود این خطا، هر ۵۵ چکِ حفاظی که مراقب آن پیکربندی بود، سبز ماندند. در محیط تولید، این غلط تایپی به این معنا بود که ردیاب خطا به یک عملیات بیاثر (no-op) خاموش تبدیل شد؛ دقیقاً همان شکستی که حفاظ طراحی شده بود تا از آن جلوگیری کند. این نوع گزارشهای موفقیت کاذب، بهویژه در سیستمهای خودکار، میتواند منجر به فجایع عملیاتی شود؛ موضوعی که در بررسی کنترلهای ارزانقیمت برای جلوگیری از گزارشهای کاذب در عاملهای هوش مصنوعی به تفصیل به آن پرداختهایم.
شکست تستهای مبتنی بر متن
این شکست منجر به یک حسابرسی دستی شد. مهندسان فایلهای تولید را یکییکی خراب کردند، حفاظ را اجرا کردند، فایل را بازیابی کردند و نتایج را ثبت نمودند. یک «حفره» زمانی ثبت میشد که با وجود خرابی واقعی در کد، حفاظ همچنان سبز میماند. نتایج در مناطق مختلف سیستم به شرح زیر بود:
- پرداخت، رضایت، حریم خصوصی و SEO: ۲۱ حفره در ۲۸ حفاظ
- موتور تصویر و خط لولهی رندر: ۵ حفره در ۱۹ حفاظ
- زیرساخت و مشاهدهپذیری: ۷ حفره در ۱۶ حفاظ
- سایر موارد (مناطق قدیمیتر، ارجاعات، ویدیو و پینها): ۹ حفره در ۸ حفاظ
برخی یافتهها تکاندهنده بود. یک حفاظ هیچ چکی نداشت و صرفاً یک تیک سبز چاپ میکرد و با کد خروجی ۰ بسته میشد، فارغ از اینکه کد اصلی چه وضعیتی دارد. حفاظ دیگری قرار بود بررسی کند که فایلهای ذخیرهسازی (blobs) قبل از ردیفهای پایگاهداده حذف شوند تا فایلهای یتیم مشتریان باقی نمانند. این چک از indexOf برای مقایسه جایگاه دو رشته در یک فایل مسیر (route file) استفاده میکرد. اما در بازنویسی سه هفته قبل، رشته اول حذف شده بود. چون indexOf مقدار ۱- برمیگرداند و ۱- از هر جایگاه عددی دیگری کمتر است، تست در هر اجرا پاس میشد، در حالی که منطق کد کاملاً شکسته بود.
جهش «بزن و فراموش کن» (Fire-and-Forget)
یک شکست بحرانی دیگر در مسیر رضایت کاربر (Consent Route) رخ داد که انتخابهای حریم خصوصی کاربر را مدیریت میکند. این مسیر سه کار را به ترتیب انجام میدهد: ثبت انتخاب در ارائهدهنده احراز هویت، افزودن به لاگ حسابرسی و حذف رندرهای عمومی از ویترین نمایش. حفاظ قدیمی از indexOf روی متن سورسکد استفاده میکرد تا این ترتیب را تأیید کند و ۹ چک را سبز نشان میداد.
تیم یک تغییر (Mutation) ایجاد کرد و یک فراخوانی await حیاتی را به یک فراخوانی void تبدیل کرد: void client.users.updateUserMetadata(userId, { ... }).catch(() => {});. این کار باعث شد یک عملیات نوشتن اجباری به یک تسک پسزمینه تبدیل شود که خطاها را میبلعد. چون فراخوانی در همان جایگاه قبلی در فایل باقی مانده بود، حفاظها سبز ماندند. اما معنای کد از بین رفته بود: لاگ حسابرسی ثبت میکرد «لغو شد»، کاربر فکر میکرد خارج شده است، اما ویترین نمایش همچنان رندرها را منتشر میکرد چون ارائهدهنده هنوز وضعیت را «مجاز» میدید.
برای رفع این مشکل، حفاظ بازنویسی شد تا بهجای نام، خودِ عبارت را استخراج کند. حفاظ جدید رشتهی مسیر را برش میدهد تا عبارت را ایزوله کند و با استفاده از عبارات منظم (Regex) تأیید میکند که عبارت با await شروع شود، شامل .catch( نباشد و با void ساکت نشده باشد.
ساخت یک Runner برای تست جهش
برای حل این مشکلات سیستمی، AI Flip Room در ۲۸ اوت ۲۰۲۴ یک ابزار تست جهش (Mutation Testing) سفارشی ساخت. ابزارهای قبلی اسکریپتهای موقتی بودند که در تلههایی مثل تفاوتهای Line Ending (CRLF vs LF) یا قطعهکدهایی که چندین بار تکرار میشدند، میافتادند. ابزار جدید از جهشهای معنایی تعریفشده در JSON استفاده میکند. هر جهش به صورت جملهای نوشته میشود که توضیح میدهد چگونه یک مشتری آسیب میبیند و چرا آن حفاظ وجود دارد.
مثال از یک شغل جهش:
{
"guard": "node tmp/qa/geometry-escalation-proof.mjs",
"mutations": [
{
"file": "src/lib/draft-rank.ts",
"name": "a judge that errored no longer ranks last - an empty verdict beats a checked image",
"from": " if (input.errored) return Number.MAX_SAFE_INTEGER - 1;\n",
"to": " void input.errored;\n"
}
]
}
منطق پنجمرحلهای این Runner برای اعتبارسنجی حفاظها به این صورت است:
۱. پیشچک: تأیید اینکه حفاظ قبل از اعمال جهش سبز است. اگر از قبل قرمز باشد، تست هیچ چیزی را ثابت نمیکند.
۲. اعتبارسنجی: رد کردن جایگزینیهای خالی برای جلوگیری از حذفهای ساده و بیمعنی.
۳. یکتایی: تأیید اینکه قطعه کد هدف دقیقاً یکبار در فایل تکرار شده است (با استفاده از Regex که \r?\n را تحمل میکند).
۴. اجرا: اعمال جهش، اجرای حفاظ و ثبت شکست در صورتی که حفاظ با وجود تغییر، همچنان سبز بماند.
۵. بازگردانی: بازگرداندن فایل به حالت بایتبهبایت اصلی پس از تست.
یافتههای محیط تولید و بازنویسی
این رویکرد بلافاصله کدهای مرده را شناسایی کرد. در یک مورد، تابعی برای تشخیص قدیمی بودن URL بازگشت پرداخت داشت و یک شاخه صریح برای حالت «زمان پرداخت در آینده است» داشت. ۶ جهش باعث قرمز شدن حفاظ شد، اما هفتمین جهش — حذف آن شاخه شرطی — هیچ تغییری ایجاد نکرد. مشخص شد یک برچسب زمانی آینده صرفاً یک سن منفی ایجاد میکند که هرگز از آستانه تعیینشده بیشتر نمیشود. آن شاخه یک عصای بیفایده بود و تیم آن را حذف و دلیلش را مستند کرد.
آنها نقصهای بحرانی دیگر را نیز یافتند:
- پارسری که در اولین براکت بسته متوقف میشد و در اشیاء تو در تو میشکست (۴ مورد از ۶ جهش پاس شدند).
- چکی برای لاگ کردن بلوکهای catch که وقتی لاگ از یک شاخه حذف میشد، چون شاخه همسایه هنوز لاگ داشت، سبز میماند.
- اسکنری برای کلیدهای Prototype که اگر عبارت حفاظ در هر جای فایل ظاهر میشد، کل فایل را پاک میکرد.
در حال حاضر، مخزن کد شامل ۹۳ شغل جهش و ۸۶۷ جهش است. ۱۷۱ حفاظ در لیست Runner وجود دارد، که ۱۲ مورد آنها به دلیل هزینه بالا یا نیاز به بیلد جدید، در لیست Skip هستند. این لیست در هر Commit با دیسک مقایسه میشود. تیم اکنون قانون گذاشته است که هر حفاظ جدیدی که تست جهش متناظر نداشته باشد، «نوشته نشده» تلقی میشود.
محدودیتهای جهش دستی
با این حال، تیم به یک نقطه کور اعتراف میکند: جهشها توسط همان افرادی نوشته میشوند که حفاظها را نوشتهاند. در ۵ اکتبر ۲۰۲۴، یک جهش با نام «تنها هفت مقاله در بلوک لینکها باقی مانده» ۵ لینک از ۱۷ لینک را حذف کرد، اما حفاظ سبز ماند. در اینجا جهش بیش از حد ضعیف بود، نه حفاظ؛ و متعاقباً بهروزرسانی شد تا تمام لینکها را حذف کند.
علاوه بر این، بسیاری از حفاظها هنوز بر خواندن متن سورسکد متکی هستند. در حالی که حفاظهای اجرایی (که توابع واقعی را از طریق یک Loader بارگذاری میکنند) بهتر هستند، اما بخش زیادی از منطق در Route Handlerهایی قرار دارد که به احراز هویت و دیتابیس متصلاند. استخراج این منطق برای تست، بازنویسی گستردهای میطلبد که تیم هنوز برای آن زمان کافی ندارد.
در حال حاضر، حفاظها بهجای خط لوله CI، بهصورت دستی روی لپتاپها قبل از Merge اجرا میشوند. Hook پیش از کامیت تنها در ۰.۰۸ ثانیه رجیستری را چک میکند. اجرای کامل دقایقی زمان میبرد و به دسترسی شبکه و دیتابیس نیاز دارد؛ اگر در Hook قرار میگرفت، احتمالاً توسعهدهندگان در زمان اصلاحات فوری با --no-verify آن را دور میزدند، که بدتر از نداشتن Hook است.
این تجربه ثابت میکند که یک تست پاسشده، دلیلی بر صحت کد نیست، بلکه صرفاً یک ادعای تأییدنشده است تا زمانی که مجبور شود شکست بخورد.
گام بعدی شما
- بررسی کنید آیا تستهای شما صرفاً وجود کلمات را چک میکنند یا رفتار واقعی سیستم را.
- برای بخشهای حیاتی (پرداخت و حریم خصوصی)، تستهای جهش (Mutation Testing) را پیاده کنید تا نقاط کور حفاظها مشخص شود.
- هر تستی که هرگز قرمز نمیشود را به عنوان یک «ادعای مشکوک» علامتگذاری و بازبینی کنید.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو