Тип:Внутрішня оцінка coding agents / інженерний аналіз відмовСтатус:completedКонтекст:Внутрішня розробка

Коли зелені тести помиляються: coding agents і перевірка через реальний runtime

Дослідження задокументованого збою у vitkovskyi.com: попри велике тестове покриття та зелені звіти агентів, реальна JS-форма надсилала FormData (multipart/form-data), яку PHP-ендпоінт навмисно відхиляв із кодом HTTP 415. Тести Playwright не виявили несумісності через мокування бекенду. Цільовий інженерний ремонт замінив кодування на URLSearchParams, виправив CSP для Cloudflare Turnstile, синхронізував ліміти полів та впровадив E2E-тест «реальний браузер → реальний PHP CLI» без жодних моків.

Задача та контекст

Як валідувати код, створений AI coding agents, коли модульні тести, статичний аналіз та згенеровані звіти демонструють 100% успіху, але критичний користувацький сценарій виявляється непрацездатним через несумісність міжплатформних контрактів у реальному середовищі?

Чому це складно

Frontend та PHP-ендпоінт окремо працювали згідно зі своїми локальними контрактами; дефект існував на стику середовищ; мережеві моки в E2E-тестах маскували реальну поведінку HTTP; перевірка рядків у вихідному коді створювала ілюзію захищеності; згенеровані звіти містили текстові твердження без підтвердження в коді; а велика кількість тестів хибно сприймалася як гарантія надійності.

Системні та операційні обмеження

  • Статичний frontend на Astro 5 з прогресивним покращенням
  • Автономний PHP 8.3 бекенд без підключення до баз даних чи зовнішніх фреймворків
  • Антиспам-захист через Cloudflare Turnstile
  • Підтримка двох сценаріїв: розширений JavaScript та нативний fallback без JS
  • Сувора політика безпеки контенту (CSP) без використання unsafe-eval
  • Локальний ремонт без масштабного переписування всієї архітектури

Архітектура та системні межі

Ключовий технологічний стек:
Playwright E2EPHP 8.3 CLIAstro 5Cloudflare TurnstileURLSearchParamsIntegration Testing
Що створено та протестовано:

Цільовий інженерний ремонт та архітектура перевірки: серіалізація через URLSearchParams (application/x-www-form-urlencoded), точний дозвіл Turnstile у CSP, двоконтурний rate limiting, суворий allowlist для source_path, ліквідація мертвого валідатора та наскрізний E2E-тест Playwright без моків на реальному PHP-сервері.

Архітектурні артефакти:
  • Специфікація цільового інженерного ремонту
  • Контракт взаємодії браузерного клієнта та PHP
  • Архітектура E2E-верифікації без моків (Playwright + PHP CLI)
  • Дворівнева модель захисту від зловживань та лімітування
  • Політика безпеки контенту (CSP) для Cloudflare Turnstile

1. Початкова модель розробки

Під час реалізації підсторінок та форми зворотного зв’язку на проєкті vitkovskyi.com активно використовувалися автономні AI coding agents. Модель взаємодії базувалася на детальних технічних специфікаціях, жорстких правилах архітектурної політики та автоматизованій перевірці результатів: агент отримував задачу, модифікував код, запускав валідаційні команди та формував детальний звіт про виконання.

На рівні технічного завдання архітектура була суворо регламентована:

  • Статичний генератор Astro для фронтенду;
  • Автономний серверний обробник inquiry.php як єдиний схвалений бекенд-компонент;
  • Прогресивне покращення форми (JavaScript для відправки без перезавантаження + нативний fallback через HTML POST);
  • Захист від ботів через Cloudflare Turnstile та захист від спаму через лімітування запитів.

Агент успішно реалізував компоненти, оновив стилі та надав звіт про повне виконання вимог із зеленими індикаторами тестів.


2. Чому система виглядала працездатною

Система демонструвала всі ознаки ідеального інженерного стану:

  • Типобезпека: npm run check проходив без жодної помилки чи попередження у 111 файлах TypeScript та Astro.
  • Лінтери та форматування: npm run lint та npm run format:check звітували про 100% відповідність стандартам.
  • Масштабний набір тестів: У репозиторії налічувалося понад 120 модульних тестів (Vitest), десятки інтеграційних перевірок та понад 130 браузерних тестів Playwright.
  • Автоматизований звіт агента: Підсумковий звіт запевняв, що форма кваліфікованого запиту працює бездоганно, підтримує валідацію, коректно обробляє відповіді сервера та відображає картку успішної відправки.

Будь-який автоматизований CI/CD-конвеєр, налаштований на перевірку стандартних метрик, вважав би цю збірку повністю готовою до релізу.


3. Що виявив незалежний аудит

Коли було проведено аудит поведінки форми безпосередньо в реальному браузері проти реального локального сервера PHP, з’ясувалося критичне протиріччя: форма не працювала взагалі.

При спробі відправки форми з увімкненим JavaScript користувач стикався з помилкою з’єднання, а сервер повертав:

HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json; charset=UTF-8

{
  "success": false,
  "error": "Unsupported media type. Use application/x-www-form-urlencoded."
}

Першопричина збою

У файлі клієнтського компонента InquiryForm.astro обробник відправки використовував стандартизований виклик:

const formData = new FormData(form);
const response = await fetch(form.action, {
  method: 'POST',
  body: formData, // Браузер автоматично встановлює multipart/form-data з boundary
});

Водночас у бекенд-обробнику форми запиту діяло суворе безпекове правило, призначене для захисту від переповнення пам’яті через завантаження файлів:

$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if (!str_contains($contentType, 'application/x-www-form-urlencoded')) {
    http_response_code(415);
    echo json_encode(['success' => false, 'error' => 'Unsupported media type...']);
    exit;
}

Клієнт відправляв multipart/form-data, сервер категорично вимагав application/x-www-form-urlencoded. Взаємодія була зламана на фундаментальному рівні протоколу.


4. Чому мокований E2E пропустив збій контракту

Виникло закономірне інженерне питання: як цей дефект пройшов крізь багаторівневий набір тестів Playwright?

Відповідь виявилася у файлі тесту tests/e2e/inquiry-form.spec.ts. Для того щоб тести виконувалися швидко та автономно без підняття PHP-процесу, розробник (або попередній агент) додав мокування мережевих запитів:

await page.route('**/inquiry.php', async (route) => {
  // Тест перехоплює запит і повертає синтетичний JSON 200 OK
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ success: true, message: 'Дякуємо!' }),
  });
});

Мережевий мок перехоплював виклик у браузерному контексті ще до того, як байти потрапляли в мережевий стек. Тест перевіряв лише те, що клієнтський JS відображає картку подяки у відповідь на статус 200. Сам запит ніколи не досягав парсера PHP, а заголовки Content-Type не аналізувалися.

Тест перевіряв уявний контракт, створений автором тесту, а не реальний контракт між двома виконавчими середовищами.


5. Правда звітів проти правди репозиторію

Другим важливим відкриттям аудиту стала розбіжність між текстовими твердженнями в агентських звітах та фактичним станом репозиторію:

  1. Звіт стверджував: «Форма повністю перевірена на сумісність із Cloudflare Turnstile». Реальність у коді: У файлі astro.config.mjs заголовок Content-Security-Policy не містив домену challenges.cloudflare.com у директивах script-src та frame-src. У реальному середовищі віджет блокувався браузером через порушення CSP.
  2. Звіт стверджував: «Контракти валідації полів уніфіковані». Реальність у коді: У HTML-розмітці поле повідомлення мало обмеження maxlength="5000", у TypeScript-типах було прописано 4000, а PHP-скрипт жорстко обрізав та відхиляв будь-що довше 3000 символів. Користувач, який ввів валідне повідомлення з погляду браузера, отримував відмову сервера без зрозумілого пояснення.
  3. Звіт стверджував: «Створено комплексний валідатор на TypeScript». Реальність у коді: Файл src/lib/inquiry/validation.ts на 150 рядків імпортувався виключно власним юніт-тестом. Клієнтська форма його не використовувала взагалі. Це був мертвий код, який створював ілюзію архітектурної зрілості.

6. Цільовий інженерний ремонт

Замість глобального переписування системи було застосовано хірургічний цільовий ремонт:

  1. Кодування запиту: У InquiryForm.astro перейшли на URLSearchParams, що гарантує відправку стандартного application/x-www-form-urlencoded:
    const params = new URLSearchParams();
    for (const [key, value] of new FormData(form).entries()) {
      params.append(key, String(value));
    }
    const response = await fetch(form.action, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
      },
      body: params.toString(),
    });
    
  2. Виправлення CSP: До astro.config.mjs внесено точний домен https://challenges.cloudflare.com для директив скриптів та фреймів без використання небезпечного unsafe-eval.
  3. Гармонізація лімітів: Межі довжини полів у HTML та типах узгоджено з авторитетним джерелом — кодом inquiry.php (повідомлення — 3000 символів, компанія — 120, посилання — 500).
  4. Видалення мертвого коду: src/lib/inquiry/validation.ts та його фіктивні тести видалено з репозиторію.
  5. Захист відкритого перенаправлення: Параметр source_path обмежено білим списком дозволених внутрішніх сторінок (/work-with-me/, /en/work-with-me/).
  6. Дворівневий rate limiting: Розділено лічильники грубого спаму (20 спроб/год) та успішних відправок (5/год). Помилка заповнення поля більше не блокує реального клієнта на рівні IP.

7. Верифікація в реальному runtime

Ключовим нововведенням став інтеграційний тест tests/e2e/inquiry-real-runtime.spec.ts.

Він працює за принципом нульових моків:

  1. Перед запуском тесту Playwright запускає локальний процес php -S на виділеному тестовому порту із робочою директорією dist/.
  2. Браузер відкриває реальну згенеровану сторінку збірки /work-with-me/ на тестовому сервері прев’ю.
  3. Тест симулює введення користувача, заповнює валідні тестові токени Turnstile та надсилає форму.
  4. Запит іде через реальний мережевий стек браузера до реального процесу PHP, парсується, перевіряється скриптом і повертає справжній HTTP 200 або 422.

Було закріплено три обов’язкові сценарії:

  • Flow A (JS-enabled): Успішна відправка через fetch → HTTP 200 JSON → відображення картки успіху з перенесенням доступного фокусу.
  • Flow B (Non-JS Fallback): Нативний POST-запит форми без JavaScript → серверний рендеринг HTML-сторінки підтвердження зі статусом 200.
  • Flow C (Validation Failure): Відправка невалідних даних → HTTP 422 JSON → коректне відображення локалізованих помилок без технічних деталей.

8. Яке тестування було надлишковим

Аналіз показав, що значна частина попереднього тестового покриття давала хибне відчуття безпеки:

  • Тести наявності рядків у коді (String-presence tests): Тести, які читали вихідний файл як текст і перевіряли expect(fileContent).toContain('Turnstile'). Вони легко обманювалися наявністю коментаря, навіть якщо логіка не працювала.
  • Тестування мертвого коду: 30+ тестів для валідатора, який не брав участі в роботі сайту.
  • Моковані сценарії збігу заголовків: Тести, які повторювали припущення розробника про поведінку бекенду, не маючи контакту з бекендом.

9. Якого тестування насправді бракувало

Усій тестовій піраміді бракувало рівно одного тесту: перевірки того, що клієнтське середовище виконання здатне домовитися із серверним середовищем виконання за протоколом HTTP без посередників.

Один реальний тест на 50 рядків виявився ціннішим за дві сотні синтетичних модульних перевірок.


10. Оновлена модель прийомки coding agents

На основі цього збою було переглянуто інженерний протокол взаємодії з coding agents:

  1. Не довіряти звітам без перевірки артефактів: Текстовий звіт моделі є декларацією про наміри, а не доказом працездатності.
  2. Обов’язкова верифікація міжплатформних стиків: Якщо архітектура використовує більше одного runtime (наприклад, Astro/Node + PHP), на їхньому кордоні повинен існувати немокований тест.
  3. Економіка тестового зусилля: Замість вимоги «напиши ще 50 тестів на граничні випадки», пріоритет надається перевірці одного бізнес-критичного шляху від початку до кінця (End-to-End Golden Path).
  4. Принцип мінімалізму: Відхиляти додавання складних проміжних абстракцій та валідаторів-дублікатів, якщо вони не використовуються безпосередньо в рантаймі.

11. Межі та обмеження дослідження

Цей аналіз є внутрішнім інженерним дослідженням і має чітко окреслені межі:

  • Контекст проєкту: Спостереження зафіксовано на конкретному стеку (Astro + PHP) під час розробки персональної платформи. Поведінка агентів може варіюватися залежно від промптів, моделей та системних інструкцій.
  • Локальне середовище: Локальний запуск через PHP CLI перевіряє логіку обробки та парсингу заголовків, але не тестує специфічні обмеження production-сервера (наприклад, поведінку FastCGI, таймаути проксі Hostinger чи кастомні правила WAF).
  • Баланс моків: Дослідження не закликає повністю відмовитися від моків. Моки є чудовим інструментом для перевірки рідкісних помилок мережі або поведінки інтерфейсу під час збоїв, проте вони не повинні бути єдиним рівнем перевірки критичних шляхів.

Ключові інженерні рішення та компроміси

01

Перевірка змін у репозиторії замість довіри до згенерованих агентських звітів.

02

Обов’язкове покриття міжплатформних меж (cross-runtime boundaries) наскрізними тестами в реальному середовищі виконання.

03

Збереження моків для перевірки складних станів UI, але вимога щонайменше одного немокованого тесту для бізнес-критичного шляху.

04

Переорієнтація метрик якості з валової кількості тестів на перевірку реальних ризиків інтеграції.

05

Прийомка рішень на основі критичності користувацького шляху: лід-форма як ключова точка конверсії вимагає максимальної перевірки.

Розглянуті альтернативи та причини відмови

  • Додавання додаткових перевірок рядків вихідного коду (відхилено, оскільки це тестує форму коду, а не його роботу).
  • Створення другого паралельного шару валідації на TypeScript (відхилено через ризик повторного розходження контрактів).
  • Проведення нового масштабного аудиту всього сайту (відхилено через надмірність витрат та розмивання фокусу).
  • Тестування фронтенду виключно з перехопленням мережевих викликів (відхилено, оскільки саме це приховало баг).
  • Тестування PHP-скрипту виключно через ізольовані модульні тести (відхилено через відсутність перевірки браузерної взаємодії).

Режими відмов та відомі обмеження

Виявлені режими відмов

  • Несумісність типів контенту: клієнт надсилає multipart/form-data, сервер очікує urlencoded, клієнт отримує 415.
  • Блокування ресурсів захисною політикою (CSP): відсутність домену челенджу блокує рендеринг віджета захисту.
  • Блокування легітимних відвідувачів: однакове лімітування помилок валідації та спам-атак виснажує квоту звичайного користувача.
  • Відкритий редирект (Open Redirect) через неперевірений параметр source_path.

Граничні умови

  • Дослідження базується на досвіді одного конкретного проєкту та конкретного стеку (Astro + PHP).
  • Висновки не слід екстраполювати як абсолютну оцінку всіх існуючих LLM чи агентських фреймворків.
  • Один успішний реальний інтеграційний тест не замінює модульного тестування граничних умов.
  • Локальне тестування на вбудованому сервері PHP CLI не повністю тотожне фінальній топології хостингу під навантаженням.
  • Поведінка зворотних проксі-серверів та вилучення реальних IP потребує фінальної перевірки у виробничому контурі.
  • Кількість тестів у репозиторії не є прямим індикатором якості або відсутності вразливостей.

Верифікація та доказова база

Поточний стан проєкту

Стан:Валідований інженерний прототип
Життєвий цикл:Завершений внутрішній аналіз відмов та цільовий runtime-ремонт

Висновки та архітектурні уроки

  • Тестовий набір може бути на 100% зеленим, перевіряючи при цьому хибний або застарілий контракт.
  • Моковані E2E-тести та тести в реальному середовищі виконують принципово різні інженерні завдання.
  • Міжплатформні межі (наприклад, Browser JS та PHP) вимагають прямої поведінкової перевірки.
  • Текст агентського звіту є вторинним свідченням; первинним є лише поведінка коду в реальному оточенні.
  • Збільшення кількості тестів не усуває проблему, якщо відсутній потрібний тип тесту (інтеграція без моків).
  • Людський аудит та експертна верифікація залишаються незамінними, коли ШІ генерує і реалізацію, і звіт про неї.
  • Фокус верифікації має визначатися бізнес-ризиками компонентів, а не простотою написання перевірок.

Потрібен інженерний аудит вашої AI-архітектури?

Допомагаю технічним лідерам та інженерним командам верифікувати архітектури штучного інтелекту, усувати непередбачувані збої агентів та будувати детерміновані RAG-системи.