Тест, который приехал вместе с фиксом
Агент закрыл задачу про дубли сессий. Нашёл баг, починил, добавил тест и отчитался: починил дедупликацию, добавил тест, набор проходит. Все три утверждения были правдой — в том смысле, который значит меньше всего.
test('дедуплицирует список', () => {
const out = dedupe([{ id: 'a' }, { id: 'a' }]);
assert.ok(Array.isArray(out));
});Этот тест проходит на сломанном коде. Он проходил до правки, проходит после и будет проходить в тот день, когда кто-нибудь правку удалит. Он зелёный, он лежит в дифе рядом с изменением, и он не доказывает ничего.
Я называю это зелёной ложью: набор зелёный по причине, не связанной с тем, что он должен сторожить. Явление не новое — такое есть в любой кодовой базе. Изменились две вещи: скорость, с которой оно производится, и то, что смотреть на красный стало некому.
Что измерили до меня
Эти цифры не мои, и они сходятся друг с другом — поэтому их стоит повторить.
Работа про ложный успех объясняет, почему очевидное лекарство не помогает: судья читает уверенную интонацию финального абзаца, а не состояние машины. Фраза «готово, все тесты проходят» написана одним и тем же голосом независимо от того, правда это или нет.
Вопрос, на который нельзя ответить интонацией
Вся идея настолько стара, что её неловко произносить вслух.
- 01
Верни исходник
Выложи коммит, на который правка легла, во временный worktree. Тесты остаются такими, как их написали; назад откатывается только исходный код.
- 02
Прогони там новый тест
По одному, по имени. Читается код возврата и больше ничего.
- 03
Он обязан упасть
Тест, написанный под эту правку, без неё должен быть красным. Прошёл — значит правку он не проверял.
Это red-green-refactor. Нового здесь ровно одно: за красным больше никто не следит, потому что тот, кто пишет тест, он же и отчитывается о нём. Раньше красный обеспечивался тем, что человеку приходилось на него посмотреть. Тест, зелёный не по той причине, — родственник теста, красного не по той причине: оба говорят вам о себе, а не о коде.
Поэтому я написал то, что смотрит за красным вместо человека: alibi. Ни одной модели в контуре, ни одного сетевого вызова в репозитории, и ответ каждый раз один и тот же.
Два решения, которые были неочевидны
Только коды возврата, никакого разбора вывода. Каждый раннер печатает результаты, и каждый печатает их по-своему, меняет формат между версиями и меняет ещё раз, когда подключают репортер. Парсер, верный на 98%, даёт ложное обвинение каждый пятидесятый раз, а одно ложное обвинение стоит дороже, чем пятьдесят верных приносят. Поэтому каждый тест запускается отдельно и читается только код возврата. Медленнее — да. Это разница между «тест прошёл на старом коде» и «этот текст был похож на успех».
Третий вердикт, потому что двух было бы враньём. Новый тест для модуля, которого ещё не существовало, не может его импортировать: раннер выходит с ненулевым кодом, не добравшись ни до одного ассерта. Засчитать это за алиби — раздать алиби всем тестам, написанным под новый код, а это большинство. Засчитать за отсутствие доказательства — обвинить нормальный тест. Поэтому у этого состояния своё имя — provisional — и оно намеренно не доказывает ничего.
Что сказали 200 агентских пул-реквестов
Инструмент — это утверждение о мире, поэтому я навёл его на мир. Собрал 200 смерженных пул-реквестов, написанных пятью кодовыми агентами — Devin, Copilot, Claude, Codegen, OpenHands, — из 138 репозиториев и на четырёх языках. Оставлял только те, где менялись и исходники, и тесты, и не больше двух на репозиторий, чтобы ни один проект не смог перевесить остальные.
Дальше по каждому: выложить репозиторий на базовом коммите, поставить зависимости, убедиться, что набор там зелёный, положить поверх тестовые файлы пул-реквеста и прогнать каждый новый тест отдельно.
Честная формулировка звучит уже заголовка: в 8 пул-реквестах из 17 хотя бы один добавленный тест прошёл бы без правки, с которой приехал. Вот это я готов защищать.
Инструмент ошибался, и нашла это ручная проверка
Первый прогон дал 34.9%. Один пул-реквест давал 32 находки из 67 — половину, — поэтому, прежде чем что-то записывать, я пошёл на него смотреть.
Все эти тесты импортировали модуль, которого на базовом коммите не было. Честно пройти там они не могли. Они должны были вернуться как provisional, а вернулись как обвинения.
Причина: pip install -e . записывает в site-packages абсолютный путь на исходную рабочую копию. Я поставил worktree первым в PYTHONPATH, и это меняет порядок поиска — но порядок не запрещает поиску идти дальше. Модуля, которого на базе не было, в worktree нет вовсе, поэтому Python идёт по списку дальше и находит новый через editable-установку. Тест проходит на коде, который должен был быть откачен.
Починка — спрашивать вместо того, чтобы предполагать: импортировать изменённый пакет внутри worktree и напечатать, откуда он взялся. Путь снаружи означает, что откат не состоялся, и такие тесты помечаются как неотвеченные, с именем модуля и путём, куда он утёк. Один пул-реквест превратился из 33 обвинений в 39 честных «не знаю», а число упало с 38% до 25%.
Я рассказываю это потому, что именно эта часть решает, стоит ли чего-нибудь всё остальное. Число, пережившее попытку автора его сломать, стоит больше, чем большее число, которое такой попытки не переживало.
Шесть способов купить зелёный набор
В репозитории лежит их музей. Каждый экспонат — настоящий репозиторий, который собирается и проверяется в тестах, поэтому экспонат, переставший ловиться, роняет сборку.
- Новый тест проходит на старом коде — он не проверил ничего.
- Падавший тест удалён — зелёно, потому что набора стало меньше.
- Падавший тест выключен одним словом.
- Тест не содержит ни одного ассерта — упасть он может только исключением.
- Тест мокает то, что менялось, — фотография моста не держит вес.
- Ассерт ослаблен, пока не согласился: проверял содержимое, теперь проверяет длину.
Седьмой способ, который инструмент не ловит, — самое полезное, что мне можно прислать.
Чего он не скажет
- Он не доказывает, что правка верна. Он находит тесты, которые ей не доказательство, — а это гораздо меньшее утверждение. Пустой отчёт значит «ничего не поймано».
- Про тесты, которых диф не касался, он не говорит ничего: какое у них было алиби, такое и осталось.
- Юнит-тесты Rust лежат в том же файле, что и код, поэтому откат исходника откатил бы и тест. Такие помечаются непроверенными, а не угадываются.
- Один процесс на тест. На наборе с тяжёлой общей подготовкой это медленно, и быстрого режима, доверяющего разбору вывода, не будет намеренно.
- Характеризующий тест — написанный, чтобы зафиксировать уже существующее поведение, — алиби не имеет по определению. Пометьте его, и он останется в отчёте, не влияя на вердикт. Тихо заглушить находку нельзя.
Как запустить
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. Считайте это нижней границей на узкой выборке.
Один процесс на тест — это же невыносимо медленно?
На наборе с тяжёлой общей подготовкой иногда да. Проверяются только те тесты, которых коснулся диф, а прогон всего набора можно выключить. Быстрого режима, доверяющего разбору вывода, не будет: разбор вывода — ровно то, чему этот инструмент отказывается доверять.
Источники
Утверждения из статьи можно проверить: ниже первоисточники, а не пересказ.
- 01From Confident Closing to Silent Failure: Characterizing False Success in LLM Agents — Цифра 75.8% и результаты по AUROC судей
- 02Test Coverage Analysis of Agentic Pull Requests — 4 882 агентских пул-реквеста; покрытие выросло в 35.9% случаев на Java и 22.5% на Python
- 03Stack Overflow Developer Survey 2025 — «Почти правильно, но не совсем» — 45% из 49 000 опрошенных
- 04alibi — инструмент и харнесс измерения — MIT; в experiments/corpus лежат собранные пул-реквесты и сырые результаты

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