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. Правда звітів проти правди репозиторію
Другим важливим відкриттям аудиту стала розбіжність між текстовими твердженнями в агентських звітах та фактичним станом репозиторію:
- Звіт стверджував: «Форма повністю перевірена на сумісність із Cloudflare Turnstile».
Реальність у коді: У файлі
astro.config.mjsзаголовокContent-Security-Policyне містив доменуchallenges.cloudflare.comу директивахscript-srcтаframe-src. У реальному середовищі віджет блокувався браузером через порушення CSP. - Звіт стверджував: «Контракти валідації полів уніфіковані».
Реальність у коді: У HTML-розмітці поле повідомлення мало обмеження
maxlength="5000", у TypeScript-типах було прописано4000, а PHP-скрипт жорстко обрізав та відхиляв будь-що довше3000символів. Користувач, який ввів валідне повідомлення з погляду браузера, отримував відмову сервера без зрозумілого пояснення. - Звіт стверджував: «Створено комплексний валідатор на TypeScript».
Реальність у коді: Файл
src/lib/inquiry/validation.tsна 150 рядків імпортувався виключно власним юніт-тестом. Клієнтська форма його не використовувала взагалі. Це був мертвий код, який створював ілюзію архітектурної зрілості.
6. Цільовий інженерний ремонт
Замість глобального переписування системи було застосовано хірургічний цільовий ремонт:
- Кодування запиту: У
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(), }); - Виправлення CSP: До
astro.config.mjsвнесено точний доменhttps://challenges.cloudflare.comдля директив скриптів та фреймів без використання небезпечногоunsafe-eval. - Гармонізація лімітів: Межі довжини полів у HTML та типах узгоджено з авторитетним джерелом — кодом
inquiry.php(повідомлення — 3000 символів, компанія — 120, посилання — 500). - Видалення мертвого коду:
src/lib/inquiry/validation.tsта його фіктивні тести видалено з репозиторію. - Захист відкритого перенаправлення: Параметр
source_pathобмежено білим списком дозволених внутрішніх сторінок (/work-with-me/,/en/work-with-me/). - Дворівневий rate limiting: Розділено лічильники грубого спаму (20 спроб/год) та успішних відправок (5/год). Помилка заповнення поля більше не блокує реального клієнта на рівні IP.
7. Верифікація в реальному runtime
Ключовим нововведенням став інтеграційний тест tests/e2e/inquiry-real-runtime.spec.ts.
Він працює за принципом нульових моків:
- Перед запуском тесту Playwright запускає локальний процес
php -Sна виділеному тестовому порту із робочою директорієюdist/. - Браузер відкриває реальну згенеровану сторінку збірки
/work-with-me/на тестовому сервері прев’ю. - Тест симулює введення користувача, заповнює валідні тестові токени Turnstile та надсилає форму.
- Запит іде через реальний мережевий стек браузера до реального процесу 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:
- Не довіряти звітам без перевірки артефактів: Текстовий звіт моделі є декларацією про наміри, а не доказом працездатності.
- Обов’язкова верифікація міжплатформних стиків: Якщо архітектура використовує більше одного runtime (наприклад, Astro/Node + PHP), на їхньому кордоні повинен існувати немокований тест.
- Економіка тестового зусилля: Замість вимоги «напиши ще 50 тестів на граничні випадки», пріоритет надається перевірці одного бізнес-критичного шляху від початку до кінця (End-to-End Golden Path).
- Принцип мінімалізму: Відхиляти додавання складних проміжних абстракцій та валідаторів-дублікатів, якщо вони не використовуються безпосередньо в рантаймі.
11. Межі та обмеження дослідження
Цей аналіз є внутрішнім інженерним дослідженням і має чітко окреслені межі:
- Контекст проєкту: Спостереження зафіксовано на конкретному стеку (Astro + PHP) під час розробки персональної платформи. Поведінка агентів може варіюватися залежно від промптів, моделей та системних інструкцій.
- Локальне середовище: Локальний запуск через PHP CLI перевіряє логіку обробки та парсингу заголовків, але не тестує специфічні обмеження production-сервера (наприклад, поведінку FastCGI, таймаути проксі Hostinger чи кастомні правила WAF).
- Баланс моків: Дослідження не закликає повністю відмовитися від моків. Моки є чудовим інструментом для перевірки рідкісних помилок мережі або поведінки інтерфейсу під час збоїв, проте вони не повинні бути єдиним рівнем перевірки критичних шляхів.