dbit.one© 2026
000
booting_
Loading experience0%
dbit.one

Откатил 200 правок, сделанных агентами, и перепрогнал их тесты. Четверть прошла

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

11 мин чтения

Коротко

  • Тест, приехавший вместе с правкой, обязан падать без неё. Проходит — значит он не доказательство, а украшение.
  • Из 17 пул-реквестов, где проверку удалось довести до конца, 35 из 140 добавленных тестов (25%) проходят на коде до правки.
  • Число сконцентрировано: медиана по пул-реквесту — ноль, один репозиторий дал 14 находок из 35. Это нижняя граница на узкой выборке, а не про индустрию.
  • Спросить другую модель не работает: ни одна конфигурация из пяти судей и пяти стратегий промпта не превысила AUROC 0.65 — судья читает уверенную интонацию, а не состояние машины.
  • Ручная проверка результата вскрыла дефект в самом измеряющем инструменте: он обвинял хорошие тесты. После починки число упало с 38% до 25%.

Тест, который приехал вместе с фиксом

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

js
test('дедуплицирует список', () => {
  const out = dedupe([{ id: 'a' }, { id: 'a' }]);
  assert.ok(Array.isArray(out));
});
Правка была настоящая. Это тест, который приехал вместе с ней.

Этот тест проходит на сломанном коде. Он проходил до правки, проходит после и будет проходить в тот день, когда кто-нибудь правку удалит. Он зелёный, он лежит в дифе рядом с изменением, и он не доказывает ничего.

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

Что измерили до меня

Эти цифры не мои, и они сходятся друг с другом — поэтому их стоит повторить.

Что измерено
Источник
75.8% проваленных прогонов кодовых агентов в AppWorld всё равно отчитались об успехе; 45–48% в однопользовательских доменах tau2-bench
Advani, июнь 2026
Ни одна конфигурация из 5 моделей-судей и 5 стратегий промпта не превысила AUROC 0.65; те же судьи на AppWorld дали 0.54
та же работа
Из 4 882 агентских пул-реквестов те, что меняли и код, и тесты, улучшили покрытие в 35.9% случаев на Java и в 22.5% на Python
Dipongkor и др., июль 2026
Самая частая претензия к ИИ у 49 000 разработчиков: «почти правильно, но не совсем» (45%); 66% тратят на починку этого больше времени
Stack Overflow 2025

Работа про ложный успех объясняет, почему очевидное лекарство не помогает: судья читает уверенную интонацию финального абзаца, а не состояние машины. Фраза «готово, все тесты проходят» написана одним и тем же голосом независимо от того, правда это или нет.

Вопрос, на который нельзя ответить интонацией

Вся идея настолько стара, что её неловко произносить вслух.

Проверка
  1. 01

    Верни исходник

    Выложи коммит, на который правка легла, во временный worktree. Тесты остаются такими, как их написали; назад откатывается только исходный код.

  2. 02

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

    По одному, по имени. Читается код возврата и больше ничего.

  3. 03

    Он обязан упасть

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

Это red-green-refactor. Нового здесь ровно одно: за красным больше никто не следит, потому что тот, кто пишет тест, он же и отчитывается о нём. Раньше красный обеспечивался тем, что человеку приходилось на него посмотреть. Тест, зелёный не по той причине, — родственник теста, красного не по той причине: оба говорят вам о себе, а не о коде.

Поэтому я написал то, что смотрит за красным вместо человека: alibi. Ни одной модели в контуре, ни одного сетевого вызова в репозитории, и ответ каждый раз один и тот же.

Два решения, которые были неочевидны

Только коды возврата, никакого разбора вывода. Каждый раннер печатает результаты, и каждый печатает их по-своему, меняет формат между версиями и меняет ещё раз, когда подключают репортер. Парсер, верный на 98%, даёт ложное обвинение каждый пятидесятый раз, а одно ложное обвинение стоит дороже, чем пятьдесят верных приносят. Поэтому каждый тест запускается отдельно и читается только код возврата. Медленнее — да. Это разница между «тест прошёл на старом коде» и «этот текст был похож на успех».

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

Что сказали 200 агентских пул-реквестов

Инструмент — это утверждение о мире, поэтому я навёл его на мир. Собрал 200 смерженных пул-реквестов, написанных пятью кодовыми агентами — Devin, Copilot, Claude, Codegen, OpenHands, — из 138 репозиториев и на четырёх языках. Оставлял только те, где менялись и исходники, и тесты, и не больше двух на репозиторий, чтобы ни один проект не смог перевесить остальные.

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

Сколько
Пул-реквестов взято
200
Доведено до ответа
17
Отпало — набор не стартует, уже красный на базе или требует живой базы данных
183
Добавленных тестов, получивших определённый ответ
140
Прошли на коде до правки
35 (25.0%)
Не смогли запуститься на базе вообще (provisional)
19
Существующих тестов удалено или выключено в этих же дифах
4

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

Инструмент ошибался, и нашла это ручная проверка

Первый прогон дал 34.9%. Один пул-реквест давал 32 находки из 67 — половину, — поэтому, прежде чем что-то записывать, я пошёл на него смотреть.

Все эти тесты импортировали модуль, которого на базовом коммите не было. Честно пройти там они не могли. Они должны были вернуться как provisional, а вернулись как обвинения.

Причина: pip install -e . записывает в site-packages абсолютный путь на исходную рабочую копию. Я поставил worktree первым в PYTHONPATH, и это меняет порядок поиска — но порядок не запрещает поиску идти дальше. Модуля, которого на базе не было, в worktree нет вовсе, поэтому Python идёт по списку дальше и находит новый через editable-установку. Тест проходит на коде, который должен был быть откачен.

Починка — спрашивать вместо того, чтобы предполагать: импортировать изменённый пакет внутри worktree и напечатать, откуда он взялся. Путь снаружи означает, что откат не состоялся, и такие тесты помечаются как неотвеченные, с именем модуля и путём, куда он утёк. Один пул-реквест превратился из 33 обвинений в 39 честных «не знаю», а число упало с 38% до 25%.

Я рассказываю это потому, что именно эта часть решает, стоит ли чего-нибудь всё остальное. Число, пережившее попытку автора его сломать, стоит больше, чем большее число, которое такой попытки не переживало.

Шесть способов купить зелёный набор

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

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

Седьмой способ, который инструмент не ловит, — самое полезное, что мне можно прислать.

Чего он не скажет

  • Он не доказывает, что правка верна. Он находит тесты, которые ей не доказательство, — а это гораздо меньшее утверждение. Пустой отчёт значит «ничего не поймано».
  • Про тесты, которых диф не касался, он не говорит ничего: какое у них было алиби, такое и осталось.
  • Юнит-тесты Rust лежат в том же файле, что и код, поэтому откат исходника откатил бы и тест. Такие помечаются непроверенными, а не угадываются.
  • Один процесс на тест. На наборе с тяжёлой общей подготовкой это медленно, и быстрого режима, доверяющего разбору вывода, не будет намеренно.
  • Характеризующий тест — написанный, чтобы зафиксировать уже существующее поведение, — алиби не имеет по определению. Пометьте его, и он останется в отчёте, не влияя на вердикт. Тихо заглушить находку нельзя.

Как запустить

bash
npx skills add BOTIROFF-D/alibi     # агент прогоняет проверку, прежде чем сказать «готово»
npm install -g @botiroff/alibi      # для терминала и CI

Скиллу не нужно ничего устанавливать: он проводит агента через проверку обычными git-командами и командой тестов самого проекта, поэтому работает в репозитории, который node в глаза не видел. MIT, ноль зависимостей в рантайме, работает с vitest, jest, mocha, node --test, pytest, go test, cargo test, rspec и phpunit — или с любой командой, которую вы ему дадите.

Харнесс измерения лежит в репозитории в experiments/corpus вместе с собранными пул-реквестами и сырыми результатами, так что числа выше можно проверить, ничего не перезапуская.

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

Это же обычное мутационное тестирование?

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

Разве тест не может законно проходить до правки?

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

Почему не попросить другую модель проверить тесты?

Потому что это измерено. Ни одна конфигурация из пяти моделей-судей и пяти стратегий промпта не превысила AUROC 0.65, а на AppWorld те же судьи дали 0.54. Судья читает уверенную интонацию. У кода возврата интонации нет.

Значит ли 25%, что четверть агентских тестов бесполезна?

Нет. Это значит, что в семнадцати пул-реквестах, где проверку удалось довести механически, четверть добавленных тестов прошла на коде до правки. Медиана по пул-реквесту при этом нулевая, а один репозиторий дал 14 находок из 35. Считайте это нижней границей на узкой выборке.

Один процесс на тест — это же невыносимо медленно?

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

Источники

Утверждения из статьи можно проверить: ниже первоисточники, а не пересказ.

  1. 01From Confident Closing to Silent Failure: Characterizing False Success in LLM AgentsЦифра 75.8% и результаты по AUROC судей
  2. 02Test Coverage Analysis of Agentic Pull Requests4 882 агентских пул-реквеста; покрытие выросло в 35.9% случаев на Java и 22.5% на Python
  3. 03Stack Overflow Developer Survey 2025«Почти правильно, но не совсем» — 45% из 49 000 опрошенных
  4. 04alibi — инструмент и харнесс измеренияMIT; в experiments/corpus лежат собранные пул-реквесты и сырые результаты
Автор
Дониёр Ботиров
Основатель dbit.one · автор материала

Этот материал подписан именем, в отличие от остальных: за ним не стоит клиент. Код, о котором идёт речь, открыт целиком и лежит под тем же именем — его можно прочитать, запустить и проверить утверждения статьи, не поверив ни одному из них на слово.

Услуги по теме

Читать дальше

Инженерия8 мин чтения

Тест падает раз в триста прогонов. Это не погода

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

Инженерия18 мин чтения

Все пять safety-свойств Raft прошли. Две реплики разошлись

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

Инженерия10 мин чтения

Тесты не доказывают отсутствие бага. Что доказывает

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

Остались вопросы по вашему проекту?

Опишите задачу — в течение 24 часов вернёмся с оценкой, сроками и планом.

[email protected]