الكود القديم لا يكذب

الكود القديم بلا توثيق. وإن وُجد، فهو من قبل ثلاث سنوات. الاختبارات إما غائبة، أو موجودة لكنها معطّلة ومُعلّمة بـ skip. التعليقات تناقض الكود. كاتبه الأصلي استقال، ومن يعرفه لم يترك سوى عبارة “لا تلمسه وإلا انفجر”.

ومع ذلك فهذا الكود يعمل في هذه اللحظة نفسها. يعالج المدفوعات، ويستقبل تسجيلات الدخول، ويسجّل الطلبات.

التوثيق يكذب. التعليقات تكذب. وذاكرة الإنسان تكذب أشد. الشيء الوحيد الذي لا يكذب هو حركة المرور التي تتدفق فعليًا.

إذن من أين نبحث عن المواصفة؟ ليس من الويكي. ولا من Confluence. بل من nginx access log.

الدجاجة والبيضة

لإعادة هيكلة الكود القديم تحتاج إلى شبكة أمان. حين تغيّر شيئًا، يجب أن تعرف فورًا إن كان السلوك قد تغيّر. وتلك الشبكة هي الاختبار نفسه.

لكن الكود القديم بلا اختبارات. ولكتابة اختبار يجب أن تعرف ماذا يفعل الكود. ولمعرفة ما يفعله الكود يجب أن تقرأه. وحين تقرأه تجد أنه بلا اختبار ولا توثيق.

أيهما أسبق، الدجاجة أم البيضة؟ مأزقٌ كلاسيكي سمّاه مايكل فيذرز في كتاب Working Effectively with Legacy Code. وقد قدّم جوابًا هو characterization test (اختبار التوصيف) — اختبارٌ لا يثبّت ما ينبغي أن يفعله الكود بشكل صحيح، بل يثبّت ما يفعله الكود حاليًا كما هو. الصواب والخطأ مسألة لاحقة. أولًا يجب تثبيت السلوك الحالي حتى تستطيع مدّ يدك إليه.

في زمن فيذرز كان الناس يكتبون هذا يدويًا. تستدعي الدالة، وتأخذ القيمة الخارجة فتكتبها كما هي في expected. عملٌ ممل وبطيء، ولذلك لم يكمله أحد إلى النهاية.

لكن على مستوى الـ API، فإن “نتيجة استدعاء الدالة” تلك متراكمة بالفعل في مكان ما. كل يوم، بعشرات الآلاف. داخل ملف السجل.

شهرٌ من السجلات هو المواصفة

إذا جمعت لمدة شهر، يمكنك التقاط السلوك الحالي لـ API القديم بأكمله تقريبًا.

nginx access log (شهر واحد):
  endpoint · HTTP method · status code · timing
  تكرار الاستدعاء ← الأولوية
  أنماط الأخطاء (401, 422, 500 …)

request/response body (مُلتقَط عبر middleware أو reverse proxy):
  أزواج طلب/استجابة سليمة ← السلوك الذي يجب أن يمرّ
  أزواج طلب/استجابة خاطئة ← حالات الحافة التي يجب ألا تنكسر

عند دمج هذين الخيطين، يُترجَمان حرفيًا إلى اختبار تكامل Hurl. فـ Hurl صيغة تكتب طلب HTTP والاستجابة المتوقَّعة كما هما نصًّا صريحًا. زوجٌ واحد من حركة المرور — “خرجت هذه الاستجابة لهذا الطلب” — هو بالضبط كتلة Hurl واحدة.

# POST /api/orders — تكرار الاستدعاء #3، اثنا عشر ألف مرة يوميًا
POST https://api.example.com/orders
Content-Type: application/json
{ "sku": "A-1024", "qty": 2 }

HTTP 201
[Asserts]
jsonpath "$.order_id" exists
jsonpath "$.status" == "pending"
jsonpath "$.total" == 49800

هذا الاختبار لا يعرف “كيف ينبغي أن يعمل API الطلبات”. لكنه يعرف “أنه يعمل هكذا الآن”. وهذا يكفي. في اللحظة التي تغيّر فيها إعادة الهيكلة هذه الاستجابة، يضيء الضوء الأحمر.

ما يُستخلَص تلقائيًا من السجلات:

  • أي endpoint يُستخدم فعلًا ← الـ endpoint الذي لم يُستدعَ ولا مرة طوال شهر هو كود ميت. مرشّح للحذف قبل إعادة الهيكلة.
  • أنماط الاستجابة السليمة ← اختبارات الانحدار الأساسية.
  • أنماط الأخطاء ← حالات الحافة الحقيقية التي لا يتخيّلها الإنسان. هي 422 و500 التي صنعها مستخدمون حقيقيون.
  • تكرار الاستدعاء ← أولوية الاختبار. تبدأ بما يُستدعى اثني عشر ألف مرة يوميًا.

البند الأخير مهم. حين يكتب الإنسان اختبارًا، يبدأ من المسار السعيد الذي يتذكره. حركة المرور لا تحمل هذا التحيّز. المسار الذي يتحمّل الحمل فعليًا هو الأولوية ذاتها.

شبكة أمان من طبقتين

هذا النهج لا يُستخدم منفردًا، بل هو طبقة من خط أنابيب الراتشيت الذي يرفع الكود القديم إلى agent-operable.

nginx log (شهر) → توليد Hurl تلقائيًا → تثبيت السلوك الحالي لـ API القديم
        ↓
tsma → شبكة أمان على مستوى الدالة (unit)
        ↓
filefunc → ترتيب بنية الكود (مفهوم واحد لكل ملف)
        ↓
إعادة الهيكلة → Hurl يتحقق من حفظ سلوك API (integration)

الجوهر أن شبكة الأمان من طبقتين.

  • tsma = شبكة أمان على مستوى الدالة. تلتقط ما إذا كانت المنطق الداخلي قد تغيّرت. لكن حتى لو بقي توقيع الدالة كما هو، قد يتغيّر سلوك الـ endpoint بأكمله.
  • Hurl from traffic = شبكة أمان على مستوى الـ API. تلتقط ما إذا كان العقد كما يُرى من الخارج محفوظًا. مهما قلبت الداخل، ما دام ما يدخل من الخارج ويخرج إلى الخارج هو نفسه، فإنه يمرّ.

إعادة الهيكلة هي بحكم التعريف “تغيير البنية الداخلية مع حفظ السلوك الخارجي”. إذن يجب أن يكون تعريف “السلوك الخارجي” الذي ينبغي حفظه مثبَّتًا في مكان ما. tsma تمسك الحدّ الداخلي، وHurl يمسك الحدّ الخارجي. وحين تجتمع الطبقتان معًا، عندها فقط تستطيع أن تقول للوكيل: “اقلب كل شيء كما تشاء، وأين ينكسر تراه الآلة”.

حَكَمٌ لا يستطيع التملّق

هذا يتعشّق تمامًا مع جوهر Symbolic Feedback Loop.

إذا سألت الوكيل “هل أحسنت إعادة الهيكلة؟” أجاب “نعم، رتّبتها بأناقة”. إن أعطيته رأيًا، تملّق. لكن إذا شغّلت Hurl، يخرج POST /orders → expected 201, got 500. الأرقام ورموز الحالة لا تستطيع التملّق. لأنها بلا مشاعر.

اختبار Hurl المستخرَج من حركة المرور مواصفة لم يتدخّل فيها حكم الإنسان. ليس “أظن أنه يجب أن يعمل هكذا” من أحدهم، بل “بالرصد عمل هكذا”. ليس ادّعاءً بل قياسًا. ولذلك يمكن أن تحكم الآلة لا الإنسان على صواب إعادة الهيكلة وخطئها. الـ LLM منفّذٌ لا حَكَم، والحُكم تتولّاه أداة حتمية.

الشرط الوحيد: سجلٌّ مكتوب جيدًا

لكي يستقيم هذا الأسلوب يلزم شيء واحد فقط. شهرٌ من السجلات المكتوبة جيدًا.

و"المكتوبة جيدًا" هنا هي كل شيء. الـ access log وحده لا يكفي. فهو يعطيك الـ endpoint وstatus code وtiming، لكنه لا يعطيك جوهر ما يجب تثبيته — زوج request body وresponse body. معرفة POST /orders → 201 فقط لا تتيح إعادة إنتاج “خرج هذا المخرج لهذا المدخل”. لتثبيت الوظيفة يجب أن تمسك بالداخل والخارج معًا.

ولذلك السؤال الحقيقي ليس “كيف أكتب الاختبار” بل “هل سجلّي مكتوب جيدًا بما يكفي ليصير مواصفة”.

  • هل يبقى request/response body، أم يبقى status code فقط؟
  • هل تبقى استجابات الأخطاء معه أيضًا؟ body الـ 422 والـ 500 هو بالذات حالة الحافة التي لا يتخيّلها الإنسان.
  • هل السجل مهيكَل بحيث تستطيع الآلة أن تربط الطلب والاستجابة في أزواج؟

إن توفّر هذا، فأنت تكتب المواصفة منذ شهر بالفعل. لا حاجة لكتابة اختبار منفصل. لأن خط أنابيب السجل كان يكتبه نيابة عنك. وإن لم يتوفّر، فيكفي أن تدسّ طبقة middleware واحدة الآن وتُبقيها شهرًا. بعد شهر يصل إلى يدك السلوك الحالي للكود القديم بأكمله.

ولماذا شهر لا يوم؟ يومٌ واحد يلتقط المسار السعيد فقط. شهرٌ كامل يلتقط دفعة آخر الشهر، وتدفّق حركة المرور قبيل التسوية، والـ endpoint الإداري الذي يُستدعى نادرًا، وحتى الـ cron الذي يدور مرة واحدة الساعة الثالثة فجرًا — يلتقط الذيل الطويل للنظام. المواصفة توزيعٌ لا متوسط.

ترجمة السجل إلى Hurl لتثبيت الوظيفة

متى توفّر السجل، فالباقي آليّ. تُدخِل أزواج request/response لشهر كامل إلى الأداة، فتترجم كل زوج إلى كتلة Hurl. ومئات ملفات Hurl المتدفّقة هكذا هي مجموعة characterization — شبكة أمان تثبّت السلوك الحالي للكود القديم بأكمله. لم تقرأ سطرًا واحدًا من الكود. قرأت حركة المرور المتدفّقة فقط.

ولنتوقّف مسبقًا عند نقطة كثيرًا ما يتردّد المرء عندها. “السجل يحوي بيانات شخصية ومدفوعات ورموزًا (tokens)، فهل يجوز تثبيت ذلك في اختبار؟”

يجوز. بل بدقة، لا حاجة لتثبيته. لأن هذه المنهجية أصلًا لا تحتاج إلى القيم. ما يثبّته الـ characterization test ليس القيمة بل السلوك.

HTTP 201
[Asserts]
jsonpath "$.order_id" exists
jsonpath "$.total" == 49800

ما يهمّ هنا كمواصفة ليس رقم 49800، بل البنية القائلة “حقل total موجود كعدد صحيح، ويُحسَب هكذا لمدخلٍ معطى”. حتى لو أخفيت القيمة أو استبدلتها ببيانات اصطناعية، فإن قيمة المواصفة تكاد لا تنقص. capture ← إخفاء ← توليد Hurl، هذا الخط بأكمله يدور داخل بنيتك التحتية. السجل الخام لا مكان يخرج إليه أبدًا. لا يبقى سوى مواصفة مُحجَّبة القيم، عقدٌ محفوظ البنية فقط. كون السجل لا يحتاج للخروج للخارج ليس تنازلًا أمنيًا بل جوهر هذا النهج — لأنك أصلًا تكتفي بتثبيت السلوك فقط.

إذا شغّلت Hurl المولَّد مرة على الـ staging، انفصل النجاح عن الفشل في مكانه. وإن أضاءت كل الأضواء خضراء، فالآن تستطيع أن تبدأ إعادة الهيكلة. قل للوكيل اقلب كل شيء كما تشاء، وأين ينكسر يراه Hurl.

سلّمٌ يُمدّ بلا كود

ولذلك فالقيمة الحقيقية لهذا النهج ليست “كتابة الاختبار بسرعة”. القيمة الحقيقية هي هذه.

  • يبدأ دون قراءة الكود — رحل الكاتب الأصلي ولا توثيق، لكن حركة المرور المتدفّقة وحدها تمدّ شبكة الأمان. تحصل على حق لمس الكود قبل أن تفهمه.
  • النتيجة قابلة للتحقق فورًا — إذا شغّلت Hurl المولَّد على الـ staging، يخرج pass/fail في مكانه. ليس “أظنه سيعمل” بل “الآن يمرّ 327 من أصل 327”.
  • البيانات لا تتجاوز السور — من capture إلى توليد Hurl، كل شيء ينتهي داخل بنيتي التحتية. كلما زادت رقابة الصناعة، صار كونك تستطيع البدء دون إخراج أي شيء للخارج أمرًا حاسمًا.

أول جرفة في تحديث الكود القديم تتوقّف عادةً عند جرفٍ اسمه “لا أحد يعرف ما هو السلوك الحالي”. حركة المرور ← Hurl تمدّ سلّمًا على ذلك الجرف. ومدّ السلّم لا يحتاج كودًا. تكفي حركة المرور المتدفّقة — وحتى تلك تبقى داخل السور كما هي.

التدفّق كان يكتب المواصفة بالفعل

نحن نجهد لنكتب المواصفة على حِدة. نكتب OpenAPI يدويًا، ونصف السلوك في الويكي، وحين تتباعد عن الكود نسمّيها drift ونتحسّر.

لكن النظام الحيّ كان يكتب مواصفته بنفسه كل لحظة. كلما دخل طلبٌ وخرجت استجابة، كان ذلك سطرًا من وصفٍ ذاتي يقول “أنا نظامٌ هكذا”. وملف السجل هو تلك السيرة الذاتية متراكمةً طوال شهر.

نحن فقط لم نقرأها.

الكود القديم ليس بلا توثيق. التوثيق موجود داخل access log، لكن صيغته مزعجة على قراءة الإنسان فحسب. وحين تترجمها إلى Hurl، تصير مواصفة قابلة للتشغيل، عقدًا تحكم فيه الآلة.

التوثيق يكذب. حركة المرور لا تكذب.


المصادر / الأسانيد

المفاهيم والأدوات الأساسية

  • Michael Feathers. Working Effectively with Legacy Code. Prentice Hall, 2004. — مصدر مفهوم characterization test. “تثبيت ما يفعله الكود حاليًا لا ما ينبغي أن يفعله بشكل صحيح.”
  • مشروع Hurl (hurl.dev) — صيغة اختبار طلب/استجابة HTTP بنص صريح. مدمَجة كأحد SSOT العشرة في yongol.
  • إثبات tsma على 527 دالة — راتشيت على مستوى الدالة (tsma).

استخراج الاختبارات من حركة المرور والتنفيذ (carving / record-replay)

  • Elbaum, Chin, Dwyer, Jorde (2009). “Carving and Replaying Differential Unit Test Cases from System Test Cases.” IEEE TSE 35(1). — الأساس الأكاديمي للـ differential unit test الذي يسجّل (record) تنفيذ النظام ثم يعيد تشغيله (replay) على مستوى الوحدة.
  • فريق هندسة Meta (2024). “Observation-based Unit Test Generation at Meta.” FSE 2024, arXiv:2402.06111. — carving للاختبارات من قيم مرصودة لتنفيذ التطبيق. 9.6 مليون تنفيذ في CI، واكتشاف 5,702 خللًا. إثباتٌ على المستوى الصناعي لـ “الرصد هو الاختبار”.

تثبيت السلوك الحالي (snapshot / golden master)

  • Fujita, Kashiwa, Lin, Iida (2023). “An Empirical Study on the Use of Snapshot Testing.” ICSME 2023. — إثبات تبنّي اختبار snapshot (= golden master/characterization). “تثبيت المخرج الحالي لا الصواب لاكتشاف التغيير.”

شبكة أمان إعادة الهيكلة

  • Kim, Zimmermann, Nagappan (2014). “An Empirical Study of Refactoring Challenges and Benefits at Microsoft.” IEEE TSE 40(7). — إثبات أن إعادة الهيكلة بلا اختبار يضمن حفظ السلوك تصير كلفة وخطرًا.
  • Yoo, Harman (2012). “Regression Testing Minimization, Selection and Prioritization: A Survey.” STVR 22(2). — اختبار الانحدار = التعريف القياسي لـ “الثقة بأن التغيير لا يضرّ بالسلوك القائم”.

توزيع الاستخدام الفعلي هو الأولوية

  • John D. Musa (1993). “Operational Profiles in Software-Reliability Engineering.” IEEE Software 10(2). — إذا وزّعت الاختبارات بترتيب تكرار الاستخدام، فحتى لو توقّفت لضيق الوقت، تكون أكثر الوظائف استخدامًا هي الأكثر تحقّقًا. الأساس الكلاسيكي لـ “توزيع حركة المرور بدل تحيّز المسار السعيد”.

لماذا يجب أن تحكم الآلة (الـ LLM منفّذ لا حَكَم)

  • Huang, Chen, Mishra, et al. (2024). “Large Language Models Cannot Self-Correct Reasoning Yet.” ICLR 2024, arXiv:2310.01798. — بلا تغذية راجعة خارجية، لا يستطيع الـ LLM تصحيح استدلاله. سبب الحاجة إلى مُحقِّق خارجي حتمي.

  • Sharma, Tong, et al. (2024). “Towards Understanding Sycophancy in Language Models.” ICLR 2024, arXiv:2310.13548. — الـ RLHF يعلّم المجاراة فيهدم موثوقية حُكم الـ LLM على نفسه.

  • صورة الغلاف: من إنشاء الذكاء الاصطناعي (Google Gemini)

قراءات مرافقة

  • Michael Feathers, “Characterization Testing” — مقالة مبتكِر المصطلح. “في اللحظة التي يدخل فيها البرنامج الإنتاج يصير هو نفسه المواصفة (it becomes its own specification).” أطروحةٌ تكاد تطابق عنوان هذه المقالة.
  • درس Hurl الرسمي، “Your First Hurl File” — من GET / HTTP 200 إلى وضع --test. مدخلٌ يضع في يدك مباشرةً أن سطرًا واحدًا من النص الصريح هو اختبار.
  • GitHub Engineering, “Scientist: Measure Twice, Cut Once” — مكتبةٌ تشغّل الكود القديم (control) والجديد (candidate) معًا في الإنتاج لمقارنة النتيجتين. “السلوك الفعلي وحده هو المواصفة الحقيقية.”
  • Twitter Diffy (ملخّص InfoQ) — وكيل (proxy) يرسل الطلب نفسه إلى الخدمة الجديدة والقديمة فيلتقط فروق الاستجابة فقط كانحدار. سابقةٌ كلاسيكية لـ “تثبيت السلوك دون كتابة اختبار”.
  • GoReplay — أداة تلتقط حركة مرور HTTP الحيّة من واجهة الشبكة وتعيد تشغيلها على الـ staging. التطبيق النموذجي لـ “حركة مرور الإنتاج كمدخل اختبار”.
  • Nicolas Carlo, “Characterization vs Approval Tests” — ترتيبٌ لثلاثة مصطلحات لتقنية واحدة فعليًا، وتأكيدٌ على دور الـ “Printer” في تنقية المعلومات الحسّاسة من المخرجات.
  • Pact — consumer-driven contract testing. نهج “العقد الصريح” مقابل تثبيت حركة المرور. النظر إلى الطريقتين معًا يحقّق التوازن.

مقالات ذات صلة