· 1 мин

AI-агенты тестируют мой Telegram mini app: 85 QA-циклов на живом проде

AI-агент может тестировать Telegram mini app как живой игрок, не трогая серверы Telegram: initData подписывается локально токеном бота, тестовый пользователь живёт вне диапазона реальных ID, а в браузере подпись инжектится HTTP-заголовком через page.route. У меня так прошло 85 QA-циклов на живом проде: план сценариев, прогон, отчёт с логами и SQL-выборками, баги, фиксы, перепроверка.

Как AI-агенты тестируют Telegram mini app без риска бана: локальная подпись initData, обход non-configurable SDK через page.route, batched evaluate. 85 QA-циклов на живом проде с реальными багами.

AI-агенты тестируют мой Telegram mini app: 85 QA-циклов на живом проде

У моего DnD-бота с ИИ-мастером сейчас открытая бета: сотни живых игроков, реальные платежи, и каждый релиз может что-то им сломать. Ручное прокликивание всех веток я не вывозил уже к маю. Сейчас QA у меня гоняют AI-агенты: с конца апреля по начало августа в папке docs/qa/ накопилось 85 пронумерованных циклов - план сценариев, прогон на проде, отчёт с логами и SQL-выборками, баги в трекер, фиксы, повторная проверка. Ниже - как эта кухня устроена технически и что она находит на живых примерах.

Как агент заходит в бота и почему за это не банят

Самый частый вопрос к этой схеме: "ты гоняешь ботов по Telegram, тебя же забанят". Нет. Главное правило моего QA-контура - в серверы Telegram не ходить вообще.

Mini app авторизуется в бэкенде через initData - подписанную строку, которую обычно выдаёт Telegram-клиент. Фокус в том, что подпись - это обычный HMAC на токене бота, и его можно посчитать локально: у меня это делает скрипт gen_init_data.py, который знает BOT_TOKEN и собирает валидный initData для тестового пользователя. Тестовый юзер - telegram_id = 999000001, фиксированный ID вне диапазона реальных телеграмных, бэкенд создаёт его сам при первом POST /api/auth/validate. Запросы летят напрямую в мой бэкенд на Railway - Telegram в цепочке не участвует, банить некого. Единственная эксплуатационная деталь: initData живёт 24 часа, поэтому генерится заново перед каждым прогоном.

Дальше два канала тестирования. API-канал - агент дёргает бэкенд curl'ом с этим initData и проверяет логику игры без UI. UI-канал - Playwright открывает mini app как обычную веб-страницу, и вот тут начинается самое интересное.

Подмена initData в браузере: грабли с non-configurable getter

Наивный план "открыть mini app в Playwright и подсунуть initData" разбивается об SDK. @twa-dev/sdk объявляет initData как non-configurable getter и не парсит URL fragment. То есть не работает ничего из очевидного: ни навигация с #tgWebAppData=..., ни Object.defineProperty(WebApp, 'initData', ...) - свойство защищено, configurable: false. Страница честно показывает "Ошибка авторизации", а в консоли initData === "", platform === "unknown".

Рабочая связка, до которой мы докопались, - двухслойная:

// 1. Мок window.Telegram.WebApp ДО загрузки SDK -
//    из него UI читает user, тему, viewport
await page.addInitScript((initData) => {
  window.Telegram = { WebApp: {
    initData,
    initDataUnsafe: { user: parseUser(initData), /* ... */ },
    version: '7.10', platform: 'tdesktop', colorScheme: 'dark',
    ready: noop, expand: noop, /* + ~30 методов-заглушек */
  }};
}, INIT);
 
// 2. ГЛАВНОЕ: подписанный initData уезжает HTTP-заголовком
//    в каждый запрос к бэкенду - мимо SDK вообще
await page.route('**/bot-production-****.up.railway.app/**', (route) =>
  route.continue({ headers: {
    ...route.request().headers(),
    'x-telegram-init-data': INIT,
  }})
);

Смысл второго слоя: неважно, что SDK внутри своего замыкания думает про initData, - axios всё равно уйдёт на бэкенд с правильным заголовком, потому что перехват стоит на уровне сети. Внутри SDK initData так и останется пустым, и это нормально: авторизацию решает заголовок.

Вторая грабля - скорость. Каждый вызов инструмента Playwright MCP стоит 500-1500 мс накладных расходов на протокол. Если агент кликает "по одному клику на вызов", прогон становится в 5-7 раз медленнее. Поэтому правило из моего QA-рецепта: много шагов в одном evaluate с паузами под рендер React - весь чарген (раса, класс, характеристики, сохранение) укладывается в 2-3 вызова вместо пятнадцати. И никаких снапшотов DOM между шагами: вместо них последняя строчка каждого evaluate возвращает document.body.innerText.slice(0, 300) - этого хватает, чтобы агент понял, где он.

Что агент приносит вместо "тут сломалось"

Формат один и тот же 85 циклов подряд: QA_N.md - план сценариев с чек-листами, QA_N результаты dd.mm.md - отчёт прогона. В отчёте каждый вердикт подкреплён тем, что можно перепроверить: строками логов, SQL-выборками, номерами записей.

Вот реальный кусок из свежего цикла QA_85 (26 игровых ходов на проде, 9 августа). Сценарий S4, проверка "новый персонаж не затирает известного". Агент играет: "Прошу кузнеца позвать его сына и его жену" - и ловит FAIL с доказательствами:

| id   | было                        | стало                              |
|------|-----------------------------|------------------------------------|
| 2345 | Кузнец / is_companion=false | Жена кузнеца / is_companion=true   |
| 2349 | -                           | Сын кузнеца (новая запись)         |
 
20:06:56 | dnd-v85l: безымянный NPC 'кузнец' получил имя
           'Жена кузнеца' (id=2345) - обновлён

То есть кузнец из ростера кампании исчез: его запись в БД перезаписала жена, да ещё и с флагом спутника, хотя в спутники её никто не нанимал. Агент не остановился на "что-то не так с NPC" - он назвал канал, который это сделал (dnd-v85l, логика "дать имя безымянному"), и сформулировал гипотезу: ролевые имена вроде "Кузнец" эта логика считает безымянными и отдаёт под переименование. Мне как деву осталось проверить гипотезу, а не воспроизводить баг.

Ещё два улова из того же прогона, чтобы был виден спектр. Механическая вычистка запрещённых имён из текста рассказчика оставляет висячие местоимения: предложение с "Лейфом" удалили, а следующее начинается с "Он клянётся..." - и читатель уже не знает, кто "он". И одна заглушка на 26 ходов: валидатор идентичности упал с формулировкой NPC 'Бренн' назван как 'собака', но в state race='человек', игроку ушло "Ты двигаешься в нужную сторону." (31 символ) - зато мана за ход не списалась, деньги игрока защищены, и это агент тоже проверил отдельной строкой.

Отдельная секция в каждом отчёте - "Что НЕ проверено". В QA_85 агент честно записал: кап на создание NPC проверен на одном срабатывании, путь дропа заражённых фактов живыми данными не подтверждён, UI в этом прогоне не трогали. Это дисциплинирует не хуже самих находок: я знаю границу, за которой "всё зелёное" ничего не значит.

Чему конвейер научился об себя же

Скилл, по которому работает QA-агент, - это набор инструкций под мой проект, и дорабатывается он об каждый фейл. Два примера, во что превращаются грабли.

Первый. Агенты, копаясь в проде, любили угадывать имена колонок в SQL - "ну наверняка там users.mana_balance". Аудит июля насчитал 372 упавших запроса, из них 93% - именно угаданные колонки (правильно - mana_balances.balance; ещё из классики: characters.gold на самом деле copper). Лечение - шпаргалка схемы прямо в скилле и правило "колонки не угадывать": перед первым SELECT к таблице читается cheatsheet. Число фейлов упало до единичных.

Второй, совсем стыдный. Отчёты нумеруются QA_1, QA_2, ... - и однажды сессия взяла номер "из памяти" вместо сканирования папки, создав QA_43/44 там, где должны были быть QA_24/25. Следующая сессия слепо продолжила от максимума, и пришлось переименовывать три файла. Теперь в правилах проекта жёсткий протокол: перед новым отчётом - просканировать папку, сравнить номера числами (строковая сортировка врёт: QA_9 > QA_44), при дыре в серии - остановиться и спросить. Мелочь? Мелочь. Но конвейер на 85 циклов держится ровно на таких мелочах.

Что в этом человеческого

Решения - по-прежнему мои. Агент выносит вердикты PASS/FAIL по чек-листу, но что из этого чинить сейчас, что завести в трекер на потом, а что признать "так и задумано" - разбираю я. Гипотезы агента про причину бага помечены в отчётах как гипотезы ("механизм передан по логу - подтверждение за девом") - и это правильная скромность: пару раз красивая версия агента не подтверждалась.

Экономику я считаю просто. 85 циклов за три с небольшим месяца - это темп, который руками недостижим в принципе: один такой прогон с 26 ходами, проверкой логов по пяти параллельным кампаниям и SQL-сверкой ростера занял бы у меня вечер, а у агента занимает полчаса. При этом продукт живой, с реальными игроками и платежами - и большинство регрессий теперь умирает в QA-отчёте, а не в чате поддержки.

Это не серебряная пуля: чинить найденное и размечать приоритеты всё равно мне. Но самая тупая часть работы - прокликать одно и то же в восемьдесят пятый раз и не проглядеть строчку в логе - уехала к агентам насовсем. А я пойду разбираться с жалобами кузнеца на внезапную смену личности.

Частые вопросы

Можно ли автоматизировать тестирование Telegram mini-app без риска бана?

Да, если не ходить в серверы Telegram вообще: initData подписывается локально через BOT_TOKEN (это обычный HMAC), тестовый пользователь берётся с ID вне диапазона реальных, а запросы идут напрямую в свой бэкенд. Telegram в этой схеме не участвует, банить некого.

Почему нельзя просто подменить initData в window.Telegram.WebApp?

Потому что @twa-dev/sdk объявляет initData как non-configurable getter и не парсит URL fragment: ни навигация с #tgWebAppData, ни Object.defineProperty не работают. Рабочая связка - addInitScript с моком WebApp до загрузки SDK плюс page.route, который вшивает подписанный initData HTTP-заголовком в каждый запрос к бэкенду мимо SDK.

Заменяет ли AI-агент ручного тестировщика?

Рутину - да, решения - нет. Агент прогоняет сценарии, собирает связку симптом-лог-запись в БД и честно пишет, что не проверено. Вердикт "это баг или фича", приоритет и решение о фиксе остаются за человеком. У меня на 26-ходовом прогоне агент вынес 8 вердиктов, из них один FAIL - и это ровно те места, куда стоило смотреть.

Как готовился материал: черновик собран моим AI-конвейером по темам и заметкам из практики, факты сверены с первоисточниками, финальный текст я прочитал, поправил и утвердил перед публикацией. Обложку к статье тоже рисует нейросеть. Про сам конвейер — в разделе обо мне.

Влад Новиков
Пишу про AI-разработку без глянца. Новые разборы — сначала в канале.
Подписаться