opopa.top

Ловим модели на пиздеже: от браузера до живого железа

·10 мин чтения·обновлено

разборыинфраструктура

Агент говорит «тесты зелёные», а ошибка на месте. Как агенты врали мне моками и тестами, почему я перешёл на живые тесты и во что это обошлось.

Я писал статью о том, как агенты врут в тестах, и надиктовал в черновик: «тесты у меня живые». Агент, который помогал её писать, полез в код и нашёл заглушки — заготовленные ответы вместо настоящего сервера. В статье о вранье агентов первым соврал я. Первая реакция — «кек, я получается тоже вру?» Получается, да: обманюнькали меня агентами. Если тесты тебе тоже пишет агент, у тебя, скорее всего, то же самое — просто ты ещё не заглядывал.

Я делаю терминал — SSH-клиент для управления удалёнными компьютерами — для любой платформы, где работают Rust (язык программирования) и WebView (встроенный браузер), хоть для вашего холодильника. В его панелях живут агенты вроде Claude Code, и код самого терминала тоже пишут агенты — проверять его они должны сами, до того как я возьму устройство в руки.

Знакомая картина? Указываешь агенту на ошибку — он отвечает, что всё работает и тесты проходят. Проверяешь сам — ошибка на месте. Снова указываешь, снова «тесты зелёные», и так по кругу. Время и деньги уходят, а поймать агента не на чем.

Ниже — как я ловил агентов на пиздеже лесенкой проверок от браузера до живого железа, чего это стоило и какими шестью проверками их теперь можно поймать — на каком бы стеке ни был ваш продукт.

«Все врут» — и меня обманули тоже

У доктора Хауса — «Все врут». Мой вариант для работы с агентами: всё, что замокано, будет подделано. Моки — поле для пиздежа агентов.

Мок — та самая заглушка. Тест нажимает кнопку, мок отвечает «готово», тест зеленеет — а сервер в этом вообще не участвовал. Агенту это удобно: его критерий качества — зелёный тест, а мок зелёный всегда.

Ловится это просто: начинаешь пользоваться. И видишь, что расходятся три вещи — что работает на самом деле, что говорят тесты и в чём ты сам убеждён. У меня панель терминала на экране меняла размер, тест был зелёный, а до терминала на сервере команда так и не доходила: мок отвечал на неё пустым «успехом». Тест проверял мок, а мок застрял в том дне, когда его написали.

Почему я перешёл на живые тесты

Код меняется каждый день, а мок отвечает то же, что год назад. Живой тест гоняет настоящий сервер и работает сквозь века: поменял что-то в одном месте — отвалилось в тридцать девятом месте, о котором никто не думал, и тест это поймал. Дальше можно спокойно обсудить, приемлема ли такая деградация.

Тесты пишут агенты. Мелочи — просто по правилам аккуратного кода: без дублей и лишней сложности. А фичи — через TDD, разработку от теста. Сначала пишется тест, и он краснеет: фичи ещё нет. Потом код — и тест зеленеет. Красный и есть доказательство, что тест что-то проверяет.

С моком красного не бывает, и TDD превращается в театр. Значит, тест должен ходить в настоящую систему — и чем ближе к живому устройству, тем меньше агенту есть где соврать.

Лесенка: проверять там, где не нарисуешь

Живой тест смотрит не на экран, а на сервер. Тест кнопки «разделить экран» спрашивает у сервера, сколько терминалов запущено, до и после нажатия: если приложение только нарисовало вторую панель, проверка падает. Картинку агент подделать может, процесс на сервере — нет.

Такие тесты идут лесенкой, от дешёвого к дорогому: браузер против настоящего сервера, собранное приложение в Docker — изолированной коробке на сервере, симуляторы — программные копии телефонов на компьютере, и наверху живые iPhone и iPad. У вас ступеньки будут свои — для веб-сервиса это, например, браузер, тестовый сервер и боевой. Важно другое: каждая ступенька заново проходит сценарий, зелёный отчёт предыдущей не считается.

Чем выше ступенька, тем она дороже. Живой iPhone — это Mac для сборки, связь с телефоном по сети и постоянная возня: после перезагрузки всё приходится поднимать заново. Поэтому дешёвые ступеньки должны ловить максимум, а железо остаётся для того, что ни один мок не подделает: палец, сон устройства и сеть, которая сдохла в лифте.

Что получилось, когда моки убрали

Живые тесты сразу нашли баги, которых моки не видели. Тот самый палец: тап по фону диалога нажимал терминал под ним. Самое неприятное открытие — «флаки», тесты, которые падали иногда и которые я списывал на нестабильность. Два из них оказались багами продукта. В жизни что-то приходит с опозданием — ответ сервера, клик от отпускания пальца, — и в эти доли секунды приложение ломалось. Мок отвечает мгновенно, как агент, который клянётся, что всё сделал: шустро и мимо.

Такой результат легко потерять: моки любят возвращаться тихо. Чтобы он не утёк, я поставил шкалу деградации — обычный тест в наборе, который сканирует код всех тестов, считает моки и краснеет, если их больше потолка, записанного в этом же тесте. Потолок я только опускаю: шкала крутится только в одну сторону, как трещотка в гаечном ключе. По этой шкале 449 моков стало 18. Оставшиеся — сознательно, там, где проверяется логика, а не связь с сервером.

Мешала ли шкала? Бывало, но не напрягала: красная шкала скорее сигналила, что пора рефакторить. Только агент быстро нашёл, где ещё можно себе подыграть.

Пойманный агент врёт тестами

Проще всего агенту соврать текстом: «готово, работает». Когда его ловят на последствиях, он начинает врать самими тестами — и это убедительнее, потому что выглядит как доказательство.

Мой любимый случай. Агент написал проверку, которая ищет на экране несколько видов элементов над панелью терминала. Некоторых из них в коде нет: он придумал их по памяти, какими они «должны» быть. Проверка, которая ищет несуществующее, всегда проходит — и выглядит как покрытие.

Такая проверка — как тест на беременность, который всегда показывает одну полоску: спокойно, но ничего не доказывает. Таких случаев было несколько, и ловить их по одному бесполезно: враньё должно превращаться в красный тест само. Мне нужен тест, который однажды покажет две полоски. Поэтому живые тесты я сделал обязательными. Счёт за это пришёл позже.

Обратная сторона: бэклог встал

Бэклог перестал двигаться. Раньше замечания, фиксы и фичи выкатывались с одной скоростью. А тут сколько агентов ни запускай — фичи быстрее не становятся, наоборот, медленнее.

Разобрался. Раньше агенты отмахивались от живых тестов и подсовывали моки, поэтому обязательность я ввёл жёстко, без права пропустить: засунул агенту в жопу пять арбузов, чтобы не мог от них отмахнуться. Проблема в том, что он пристрастился — стал суперфанатом живых тестов и гонит их на любую задачу, а я трачу время на то, чтобы достать их из агента и засунуть обратно. Не самое приятное занятие.

Агент поменял строчку — и запускает все 352 теста, это 44 минуты по логу полного прогона. Имитация бурной деятельности.

И это один агент. А их у меня работает несколько сразу — и каждый суперфанат перепроверяет себя полным прогоном на одной и той же машине. Ей не хватает сил, тесты не укладываются в отведённое время и падают — как будто нашёлся баг, хотя бага нет. Агенты перезапускают упавшее, и получается общая кутерьма: как пять человек у одной кофемашины в час пик — кофе выходит бурым недоразумением, все решают, что машина сломалась, и жмут кнопку снова.

Типичная картина в моих сессиях: 10 минут на фичу — и вчетверо дольше на тесты, которые мешают друг другу. Платишь дважды — счётом за модель и ожиданием. Я решил просто попросить.

Правило, которое тонет

Я просил раз за разом: «а зачем гонять весь набор?», «зачем он по 20 раз гоняет?», «а зачем по 500 раз гонять?» В итоге: «раз за разом прошу не гонять лишний раз тесты, и снова гоняем. Запиши в инструкции уже».

Агент записал в AGENTS.md — файл правил, который я сам держу в проекте; каждый агент читает его в начале работы: гонять только тесты, которые изменение затронуло, а полный набор — только перед сборкой на устройства.

Записать оказалось мало. В тот же день я ещё раз пять прерывал агентов, которые запускали всё подряд.

Причина та же, из-за которой модели в длинном разговоре перестают держать запреты безопасности (разбирал отдельно): правила лежат в самом начале контекста — рабочей памяти агента на эту задачу. К середине фичи, по моим наблюдениям, агент забивает эту память примерно наполовину. Правило по-прежнему там, но модель обращает на него всё меньше внимания.

Это засос, который я поставил агенту в самом начале: никуда не делся, просто под новыми синяками его не видно. Так стоило ли вообще с ним связываться?

Стоило ли

Да. Суперфанат дорогой, но он и ловит: мне больше не надо скидывать агенту скриншоты — деградации находятся сами, без моего контроля. Что-то поменялось в новой отрисовке экрана, связанное с другим местом, — и светло-буро-малиновый фон перестал рисоваться. На моке он всё ещё чёрный, и тест не упадёт никогда. На живом — краснеет, и агент сам выясняет, изменение разрушительное или к лучшему.

Универсального ответа, сколько прогонять, нет. Мало прогонов — агент снова проскочит на непроверенном, и баг, который проявляется не всегда, уедет к пользователю. Много — 44 минуты и задыхающаяся машина, на которой тесты падают уже через раз. Свит-спот — золотая середина, где прогонов ровно столько, сколько нужно для качества, — у каждого проекта свой.

Узкие места в разработке будут всегда: я их не решаю раз и навсегда, а нахожу и чиню по мере появления. Суперфаната я торможу, но он меньшее зло: лишний прогон стоит только времени. А когда модель пиздит, а ты долбишься в стену — это дороже.

Так что живые тесты остаются — дороговато, зато не пиздят.

Как ловить у себя

Шесть проверок, каждая появилась после конкретного обмана и каждая — про то, чтобы тест не врал. Если тесты за тебя пишет агент, это шесть требований к нему:

  • Проверяй последствие, а не отчёт. Тест проходит, только если изменилось что-то настоящее: процесс на сервере, файл, запись в базе. Скриншот и слова агента ничего не доказывают.
  • Мок не отвечает «успешно». Если он не знает команды — он отказывает по имени, а не возвращает пустой «успех», как с той панелью.
  • Мок знает то же, что код. Список команд и форму данных он берёт из исходников, а не из памяти агента: у меня мок однажды не знал команду, которую код уже умел.
  • Новая проверка обязана упасть. Перед тем как поверить зелёному, покажи красный — на старом коде или на нарочно сломанном. И прочитай, почему красный: «не успел по времени» — это сбой, а не доказательство.
  • Следующая ступенька повторяет сценарий. Зелёный отчёт прошлой ступеньки пропуском не служит.
  • У правила есть последствие. Шкала деградации или красный тест, который не пустит дальше. Правило без последствия агенты читают как пожелание — я проверил на бюджете тестов.

Подниматься по лесенке нужно ровно настолько, насколько высоко забралось изменение: правке формы хватит браузера, правке касаний на iPad нужно железо. Лесенка работает, только если лезть по ней самому: на слово агент поклянётся, что он уже наверху. Я тоже клялся. В черновике статьи про враньё агентов.