Проверяемость: тесты, прогоны и доказательства вместо обещаний
Предыдущая глава — «Где агенты врут» — про типологию отказов: что именно ломается и почему. Эта глава про противоядие, и оно ровно одно.
Утверждение агента о поведении кода не является наблюдением за поведением кода. Между ними — прогон, и никакая длина рассуждения его не заменяет.
Звучит банально. На практике это правило нарушают все, включая тех, кто его формулирует, потому что в транскрипте сессии утверждение и наблюдение выглядят одинаково: обе строки — текст на экране, одним шрифтом, одна под другой. Вся глава — про то, как их различать и во что обходится различение.
Два вида текста в одной ленте
Агент заканчивает задачу так:
Готово. Добавил идемпотентность в reserve() по ключу Idempotency-Key,
покрыл тестами, тесты проходят. Существующие тесты не сломались.
Здесь четыре утверждения, и у них четыре разных эпистемических статуса:
| Фраза | Что это на самом деле | Чем проверяется |
|---|---|---|
| «Добавил идемпотентность» | Утверждение о смысле дифа | Чтение дифа человеком |
| «по ключу Idempotency-Key» | Утверждение о факте в дифе | git diff, дёшево, объективно |
| «покрыл тестами» | Утверждение о существовании файлов | git diff --stat, объективно |
| «тесты проходят» | Утверждение о событии в мире | Только повторный прогон |
Разница между третьей и четвёртой строкой — принципиальная. Существование теста проверяется просмотром дифа за секунду. Прохождение теста — это событие: процесс запустился, отработал, вернул код выхода. Событие уже произошло (или не произошло) в прошлом, и в вашем распоряжении только рассказ о нём.
Ключевая механика, из-за которой это трудно: отчёт пишет модель. Даже когда прогон реально был, строка 12 passed in 3.41s в финальном отчёте — это пересказ, сгенерированный из контекста, а не скопированный оператором cp. Настоящий вывод команды лежит выше в ленте, в блоке результата инструмента (см. модель исполнения). А если контекст успел компактнуться (глава про окно), то и в ленте его больше нет — остался только пересказ пересказа.
Отсюда первое рабочее правило, тривиальное и почти никем не соблюдаемое:
Читайте блоки результата инструментов, а не финальный отчёт. Отчёт — это оглавление, а не доказательство.
Честная вторая сторона
Не надо делать вид, что агенты не запускают тесты. Современные кодовые харнессы запускают их охотно и часто — это одно из главных отличий агента от чата (глава 01). Проблема не в том, что прогонов нет. Проблема в том, что доля утверждений, подкреплённых прогоном, меньше единицы, а по тексту отчёта нельзя понять, какие именно утверждения попали в эту долю. Вы не ловите систематического лжеца — вы работаете с источником, у которого перемешаны наблюдения и правдоподобные достройки, причём без пометок. Именно поэтому дисциплина маркировки (что verified, что assumed) — не бюрократия, а способ восстановить утраченное различие; к ней вернёмся в конце главы.
Лестница свидетельств
Не все проверки равны. Полезно держать в голове упорядоченную шкалу — что именно доказывает каждый вид свидетельства и во сколько обходится.
| Ступень | Что доказывает | Чего не доказывает | Цена |
|---|---|---|---|
| 1. Воспоминание модели | Ничего | Ничего | Ноль |
| 2. Прочитанный исходник | Что в файле написано так | Что оно так работает | Токены на чтение |
| 3. Компилятор, типы, линтер | Отсутствие целого класса ошибок | Правильность поведения | Секунды |
| 4. Прогон теста у агента | Поведение на выбранных входах | Поведение на остальных | Секунды-минуты |
| 5. Гейт в CI на чистой машине | Воспроизводимость вне машины автора | Что важное покрыто тестами | Минуты, деньги за раннеры |
| 6. Наблюдение в проде | То, что действительно важно | Ничего заранее | Инциденты |
Три следствия, которые стоит проговорить.
Ступень 1 — это ноль, а не «немного». Когда агент пишет «в этой библиотеке timeout по умолчанию 30 секунд», не спросив ни исходник, ни документацию, — это не слабое свидетельство, это отсутствие свидетельства. Правило RSN-03 из нашего пакета шаблонов формулирует жёстко: recall — это unchecked по определению, вне зависимости от уверенности формулировки. Уверенность тона и обоснованность утверждения в тексте модели не коррелируют так, как у человека, и это единственная асимметрия, к которой инженеру приходится специально привыкать.
Ступень 3 недооценена. Типы, компилятор и линтер — самая выгодная строчка таблицы: секунды времени, ноль внимания человека, и они ловят ровно тот класс дефектов, который агент производит чаще всего, — несуществующие методы, неверная арность, перепутанный порядок аргументов. Если у вас есть выбор, куда вложить час, — вложите его в строгий режим типов, а не в ещё десять юнит-тестов.
Ступень 6 не заменяется ничем и никогда не наступает вовремя. Это единственная ступень, которая говорит правду о том, что вас волнует, — и говорит её после релиза; как её слушать, разбирается в главе про observability трека DevOps.
Почему рассуждение не заменяет прогон
Есть соблазн думать, что достаточно умная модель могла бы «просто понять», что код делает, и тогда прогон не нужен. Это ошибка не про качество моделей, а про природу задачи.
Вопрос «делает ли эта программа X» для нетривиального семантического свойства X алгоритмически неразрешим — это теорема Райса, прямое следствие неразрешимости остановки. Никакой анализатор, включая идеального рассуждателя, не решит её в общем случае. Практические инструменты обходят это тремя способами: аппроксимируют (типы, статический анализ — верно, но неполно), ограничивают язык (тотальные языки, зависимые типы) или наблюдают конкретные запуски (тесты — полно для проверенных входов, ничего для остальных).
Агент делает четвёртое: угадывает по форме. Это работает удивительно часто, потому что код в мире регулярен, и совсем не работает там, где ваша кодовая база нерегулярна — а именно там и живут интересные баги. Дийкстра сформулировал границу теста задолго до всего этого: тестирование показывает наличие ошибок, но не их отсутствие. Для агента формулировка получает вторую половину: рассуждение агента не показывает даже наличия. Отсюда практический перевод: тест — не бюрократия, а единственный доступный вам оракул, который отвечает не текстом, а фактом. Оракул слабый (говорит только про выбранные входы), но честный.
Конвейер: от утверждения к доказательству
Каждое утверждение в отчёте агента полезно прогонять через один и тот же фильтр. Он занимает секунды и экономит часы.
(API, флаг, дефолт)"| D{"Есть адрес:
файл, команда, URL?"} B -->|"О поведении кода"| E{"Есть прогон
в ленте?"} B -->|"О намерении и смысле"| F["Ревью человеком —
автоматизации нет"] D -->|Нет| G["Статус: unchecked.
Либо проверяю, либо помечаю"] D -->|"Да"| L["Иду по адресу.
Доказательство принято"] E -->|Нет| G E -->|"Да, но пересказ"| I["Прогоняю сам —
одна команда"] E -->|"Да, блок инструмента"| J{"Тест падал
до изменения?"} J -->|Неизвестно| K["Откатываю фикс,
прогоняю, возвращаю"] J -->|Да| L C --> L I --> J K --> L
Обратите внимание на ветку J. Зелёный прогон сам по себе не доказывает, что тест что-то проверяет: тест, который проходит и до, и после изменения, не имеет отношения к изменению. Это важнейшая и самая пропускаемая проверка во всей главе, и ниже она разобрана отдельно.
Жизненный цикл утверждения
Полезно смотреть на каждое утверждение как на объект с состоянием, а не как на предложение в тексте. Тогда становится видно, что «принято» — это не конец, а промежуточная стадия.
Переход R --> D — самый частый и самый обидный. Агент действительно проверил поведение прогоном ad-hoc скрипта, вы это видели, всё было честно. Потом сессия закончилась, скрипт не сохранился, тест не написан — и через две недели никто в команде не может ответить, гарантируется ли идемпотентность. Знание было и утекло.
Отсюда правило, которое стоит вешать на стену:
Проверка, не оформленная в артефакт репозитория, — это не проверка, а эпизод. Тест, тип, схема, ассерт, проверка в CI — вот что живёт дольше сессии. Куда именно складывать долгоживущее знание и почему заметка в памяти агента — худший из вариантов, разбирается в главе про память.
Типология поддельных доказательств
Теперь главное: список того, что выглядит как доказательство и им не является. Порядок примерно по частоте встречаемости в реальных сессиях, а не по опасности.
| # | Подделка | Как выглядит | Как ловится |
|---|---|---|---|
| 1 | Прогон, которого не было | «тесты проходят» без блока инструмента | Поиск блока результата в ленте |
| 2 | Тест, проходящий без фикса | Зелёный после, зелёный до | Откат фикса и прогон |
| 3 | Тавтологический тест | Тест повторяет реализацию | Чтение: что сломается, если поведение изменить |
| 4 | Мок проверяет мок | Ассерт на mock.called |
Есть ли ассерт на состояние или результат |
| 5 | Не та команда | Прогнан не тот пакет / не та папка | Сверить команду с той, что назвали заранее |
| 6 | Ноль тестов, код выхода 0 | 0 passed, No tests ran |
Читать число, а не цвет |
| 7 | Подгонка теста под код | В дифе изменён и код, и ожидание | git diff по файлам тестов отдельно |
| 8 | Заглушённый тест | skip, xfail, закомментировано |
Grep по маркерам пропуска в дифе |
| 9 | Флак, прошедший случайно | Зелёный один раз из трёх | Повторный прогон, лучше дважды |
| 10 | Покрытие вместо проверки | «покрытие 92%» | Покрытие ≠ проверка, см. ниже |
| 11 | Процитированный вывод | Лог в отчёте выглядит правдоподобно | Сверить с блоком инструмента дословно |
Тест, который проходит и без фикса
Самая дорогая подделка, потому что она переживает ревью: тест есть, тест зелёный, диф выглядит осмысленно. И при этом тест не имеет отношения к багу.
Механизм простой. Агент получил задачу «почини баг», написал тест на то поведение, которое понял, и починил то, что понял. Если понял неверно — тест проверяет что-то другое, а баг остался. Проверка занимает тридцать секунд:
# Правило: тест обязан покраснеть без фикса. Иначе он ничего не проверяет.
git stash push -- src/ # откатываем только код, тесты оставляем
pytest tests/test_reserve.py::test_reserve_is_idempotent -x
# ждём FAILED и читаем причину падения
git stash pop
pytest tests/test_reserve.py::test_reserve_is_idempotent -x
# ждём PASSED
Второе требование важнее первого: тест должен падать по той причине, которую вы чините. Тест, падающий из-за опечатки в фикстуре, тоже красный — и тоже ничего не доказывает. Это правило CODE-13 из products/workbench/templates/memory/VERIFIED-CODE.md: «исправление бага начинается с теста, который падает по названной причине», с явным наблюдаемым нарушением — коммит с фиксом, чей тест проходит при откате фикса. Ту же дисциплину классически описывает TDD: красный, зелёный, рефакторинг. С агентом красная фаза становится не педагогикой, а средством контроля — она единственная отличает тест от декорации.
Тавтологический тест
Агент видел ваш код и пишет тест по нему, а не по контракту. Получается зеркало:
# ПЛОХО: тест повторяет реализацию (if total > 1000: return total * 0.1).
def test_calculate_discount():
assert calculate_discount(2000) == 2000 * 0.1
# ХОРОШО: требование бизнеса, записанное числом, плюс граница.
def test_discount_is_ten_percent_above_threshold():
# Требование: заказы дороже 1000 получают скидку 10%.
assert calculate_discount(Decimal("2000")) == Decimal("200")
def test_no_discount_at_threshold():
# Включительно или исключительно — то, что чаще всего понято неверно.
assert calculate_discount(Decimal("1000")) == Decimal("0")
Первый тест сломается ровно тогда, когда сломается реализация, — потому что он и есть реализация. Второй сломается, когда поведение разойдётся с требованием. Разница видна по одному признаку: если в тесте повторяется формула из кода, тест бесполезен. Проверяйте это глазами — автоматики тут нет. Помогает и просьба к агенту: «в комментарии к тесту напиши требование словами» — формулировка словами делает несоответствие видимым ещё до прогона.
Мок, который проверяет мок
# ПЛОХО: единственный ассерт — что мы позвали то, что сами и подсунули.
def test_sends_notification():
mailer = Mock()
OrderService(mailer=mailer).complete(order_id="42")
mailer.send.assert_called_once() # доказано: код вызывает метод объекта
# ЛУЧШЕ: наблюдаемый результат и содержание, а не факт вызова.
def test_completion_sends_receipt_with_total():
mailer = FakeMailer() # накапливает письма в списке
OrderService(mailer=mailer).complete(order_id="42")
assert len(mailer.sent) == 1
assert mailer.sent[0].to == "buyer@example.com"
assert mailer.sent[0].body_contains("1 200,00")
Агенты любят моки: их легко сгенерировать, они всегда зелёные, и они не требуют понимания системы. В этом и беда — тест с моками проходит независимо от того, работает ли интеграция. Хорошая эвристика для ревью: посчитайте ассерты на состояние и ассерты на вызовы. Если вторых больше — тест проверяет структуру кода, а не поведение. Разбор границ, где мок оправдан, а где вреден, — в главах «Юнит-тестирование» и «Интеграционное тестирование».
Ноль тестов, код выхода ноль
$ pytest tests/billing/
============================= test session starts =============================
collected 0 items
============================ no tests ran in 0.02s ============================
$ echo $?
5
Здесь pytest честно вернёт 5. А вот npm test в проекте без матчащихся файлов, go test ./... по пакету без тестов и dotnet test с неверным фильтром спокойно вернут ноль — и агент напишет «тесты проходят». Формально он не соврал: ни один тест не упал. Лечится одной привычкой: читайте число, а не цвет. 12 passed — информация. passed без числа — ничего. И полезно один раз настроить провал сборки на пустом наборе (в pytest — --strict-markers плюс проверка кода выхода 5, в CI — явная проверка, что число собранных тестов больше нуля).
Подгонка теста под код
Агент прогнал тесты, два упали, он «починил». Смотрим диф — исправлены ожидания в тестах, а не код. Иногда это правильно: ожидание действительно устарело. Чаще — нет.
Единственная защита процедурная: смотрите диф по тестам отдельно от дифа по коду (git diff -- 'tests/**') и требуйте обоснования каждой изменённой константы. Если менять поведение не предполагалось, а ожидания в тестах изменились — это не «починка», это удаление проверки. То же с добавленными skip и xfail: в зелёном прогоне они не видны никак.
Покрытие вместо проверки
Покрытие измеряет, какие строки исполнились, а не что проверено. Тест без единого ассерта даёт покрытие. Тест с моками на все зависимости даёт покрытие. Агент, которому поставили целью «поднять покрытие до 90%», честно достигнет цели способом, который вам не понравится, — это классический Гудхарт: показатель, ставший целью, перестаёт быть показателем.
Что действительно измеряет качество тестов — мутационное тестирование: инструмент вносит мелкие искажения в код (меняет > на >=, + на -, выкидывает вызов) и смотрит, покраснеет ли хоть один тест. Выживший мутант — это дыра в проверках, а не в покрытии. Инструменты: mutmut и Cosmic Ray для Python, PIT для JVM, Stryker для JS, C# и Scala. Опыт промышленного применения описан в работе State of Mutation Testing at Google (Petrović, Ivanković, ICSE-SEIP 2018) — и там же честно сказано, что наивный прогон всех мутантов слишком дорог и шумен, поэтому применяли фильтрацию.
Практический режим для работы с агентом: не гоняйте мутационное тестирование на всём репозитории — гоняйте на файлах, которые агент изменил в этой задаче, и только на модулях с реальной ценой ошибки: биллинг, права доступа, миграции. Это несколько минут вместо часов и точный ответ на вопрос «тесты, которые он написал, вообще что-нибудь ловят».
Флак
Тест, который проходит не всегда, — хуже отсутствующего теста: он приучает игнорировать красное. Классическое исследование An Empirical Study of Flaky Tests (Luo et al., FSE 2014) разобрало причины по категориям — асинхронность, порядок выполнения, конкуренция, зависимость от внешних ресурсов; у Google есть практический разбор того, как с этим живут в масштабе.
Код от агента даёт флаки чаще обычного по понятной причине: агент охотно пишет sleep(0.1) вместо ожидания условия, зависит от порядка словаря, берёт текущее время и реальную сеть. Правило CODE-12 из того же пакета формулирует запрет прямо: тест не зависит от часов, сети, локали и порядка итерации, если это не предмет теста. Проверка — прогнать новые тесты дважды и один раз со сдвинутым временем или другим сидом.
# Дешёвая проверка на флак и на скрытые зависимости между тестами.
pytest tests/test_reserve.py -q # прогон 1
pytest tests/test_reserve.py -q --count=2 # повтор, плагин pytest-repeat
pytest tests/test_reserve.py -q --randomly-seed=1234 # другой порядок, pytest-randomly
Кто здесь оракул
Полезно один раз нарисовать, где в цепочке находится источник истины и в каких местах он подменяется пересказом.
Диаграмма даёт точный ответ на вопрос «кому верить»: истину производит процесс, а не модель. Модель — транспорт, и на участке от шага 9 к шагу 10 транспорт лоссовый. CI ценен не тем, что «так принято», а тем, что это единственный канал, в котором между фактом и вами нет языковой модели (как его строить — «Тесты в CI» и «Основы CI»). Отсюда же схема «узкий прогон, потом широкий гейт»: агент итерирует по узкому — быстро и дёшево, — а решение о приёмке принимается по широкому, который он не запускал и не мог подстроить.
Практики, которые превращают прогон в доказательство
Команда названа заранее
Самое дешёвое, что можно сделать, — записать команду приёмки до того, как агент начал писать код, прямо в промпте:
Готово = обе команды зелёные, вывод вставлен дословно:
1) pytest tests/billing/test_reserve.py -q
2) mypy src/billing --strict
Не менять файлы в tests/, кроме нового test_reserve_idempotency.py.
Если для выполнения нужно изменить существующий тест — остановись и спроси.
Это работает по двум причинам. Во-первых, команда, названная заранее, не подстраивается под результат — а названная постфактум подстраивается почти всегда («ну, я прогнал те тесты, которые относятся к задаче»). Во-вторых, запрет на правку существующих тестов превращает подгонку из невидимого действия в явное нарушение контракта, которое видно в дифе. Подробнее про то, как формулировать такой контракт, — в главе «Промпт как спецификация».
Доказательство — вывод, а не прилагательное
Требуйте вставки вывода команды дословно, вместе с кодом выхода. Не потому, что вы будете его читать целиком, а потому что дословный вывод трудно сочинить непротиворечиво: числа тестов, тайминги, пути — расхождение бросается в глаза. Это правило CODE-10: каждое поведенческое утверждение в отчёте отображается либо в имя теста, либо во вставленную команду с её кодом выхода; наблюдаемое нарушение — предложение о поведении без того и другого.
Property-based там, где агент слаб
Агент хорошо генерирует примеры и плохо — инварианты. Это делает property-based тестирование естественным дополнением: вы формулируете свойство, а генератор ищет контрпример, включая те входы, о которых ни вы, ни агент не подумали.
from decimal import Decimal
from hypothesis import given, strategies as st
money = st.decimals(min_value=0, max_value=10**6, places=2)
@given(total=money)
def test_discount_never_exceeds_total(total):
"""Свойство: скидка не больше суммы заказа — на всех входах, а не на трёх."""
assert Decimal("0") <= calculate_discount(total) <= total
@given(amount=money.filter(lambda x: x > 0), key=st.text(min_size=1, max_size=32))
def test_reserve_is_idempotent(amount, key):
"""Свойство: повтор с тем же ключом не меняет состояние.
Именно это и означает идемпотентность — а не «есть колонка key».
"""
account = make_account(balance=Decimal("10000000.00"))
first = reserve(account.id, amount, key)
balance_after_first = get_balance(account.id)
second = reserve(account.id, amount, key)
assert second.id == first.id
assert get_balance(account.id) == balance_after_first
Свойство доказывает больше, чем пример, и — что важнее для нашей темы — его труднее подделать: тавтологическое свойство написать можно, но это заметно сложнее, чем тавтологический пример. Инструменты: Hypothesis для Python, наследник идеи QuickCheck (Claessen, Hughes, ICFP 2000); в нашем пакете шаблонов условия, когда property-тест обязателен, лежат в COMPOSITION-AND-LAWS.md: merge, retry, map-подобная операция, любая операция, объявленная независимой от порядка. Оговорка честности ради: такие тесты дороже в написании и медленнее в прогоне, и на CRUD-коде без интересных инвариантов не окупаются. Их место — арифметика денег, сериализация, слияние, кэш, повторы.
Характеризационные тесты перед рефакторингом
Если вы отдаёте агенту рефакторинг legacy-модуля, у вас нет спецификации — есть только текущее поведение. Тогда сначала фиксируем поведение как есть, потом рефакторим. Это характеризационные тесты Майкла Физерса («Эффективная работа с унаследованным кодом»), и именно для агента они критичны: без них у него нет вообще никакого критерия, кроме «выглядит чище».
Практично сгенерировать их прогоном на реальных входах и сохранить как golden-файлы:
# 1. Снимаем поведение ДО изменений на выборке реальных входов.
python -m tools.capture --inputs data/samples/*.json --out tests/golden/before/
# 2. Контракт агенту: поведение на tests/golden/before не меняется ни в одном байте.
# 3. После рефакторинга сравниваем побайтово.
python -m tools.capture --inputs data/samples/*.json --out tests/golden/after/
diff -ru tests/golden/before tests/golden/after && echo "поведение сохранено"
Слабое место метода честно назову: golden-файлы фиксируют и баги тоже. Это ровно то, что нужно при рефакторинге (изменение внутреннего устройства без изменения поведения) и категорически не то, что нужно при исправлении багов.
Проверка отчёта скриптом
Раз агент обязан маркировать утверждения, соблюдение проверяется машинно. Алгоритм — один проход по строкам: строка похожа на утверждение о поведении, но не содержит ни маркера, ни адреса — нарушение. Сложность O(n) по числу строк, память O(k) по числу нарушений.
import re, sys
MARKER = re.compile(r"\[(verified|derived|assumed|unchecked)\b")
# адрес: путь:строка, команда в обратных кавычках или ссылка
ADDRESS = re.compile(r"[\w./-]+\.\w+:\d+|`[^`]+`|https?://\S+")
BEHAVIOUR = re.compile(r"\b(проход|падает|работает|возвраща|гарантир|быстрее)", re.I)
def unsupported_claims(report: str) -> list[tuple[int, str]]:
"""Строки о поведении без маркера и без адреса — по одной на нарушение."""
return [
(n, line.strip())
for n, line in enumerate(report.splitlines(), start=1)
if BEHAVIOUR.search(line) and not (MARKER.search(line) or ADDRESS.search(line))
]
if __name__ == "__main__":
bad = unsupported_claims(sys.stdin.read())
for n, line in bad:
print(f"{n}: без свидетельства -> {line}")
sys.exit(1 if bad else 0)
Не переоценивайте такой скрипт: он ловит форму, а не истинность, и агент, знающий про проверку, легко припишет маркер к любой фразе. Ценность в другом — пропуск маркера становится виден сразу, а не на ревью, и «инженер забыл посмотреть» превращается в «пайплайн покраснел». Ровно так же работает любой линтер.
Цена проверяемости
Проверяемость не бесплатна, и глава была бы нечестной без этого раздела.
Токены. Каждый прогон возвращает вывод в контекст. Провалившийся набор тестов на большом проекте — это тысячи токенов трейсбеков, которые останутся в истории до компакции и будут переотправляться на каждом шаге (глава про контекст). Отсюда практическое: -q, -x, узкий путь до конкретного теста. Прогонять весь набор в цикле агента — самый дорогой из распространённых способов сжечь бюджет.
Время. Медленный набор ломает цикл агента сильнее, чем цикл человека: человек уходит пить чай, агент — уходит писать код дальше на непроверенном основании. Если гейт идёт двадцать минут, агент до него просто не доживёт в рамках одной сессии. Сюда же инфраструктура: раннеры, окружения, тестовые базы — разово дорого, дальше окупается, но это не ноль.
Внимание человека. Самая дефицитная статья. Проверить, что тест падает без фикса, — тридцать секунд. Прочитать тест и понять, тавтологичен ли он, — минуты, и эти минуты не делегируются: именно здесь человек незаменим (глава про ревью).
Читать так: левый верхний квадрант — то, что включается по умолчанию в любом проекте, где работает агент. Правый верхний — по решению, на модулях с реальной ценой ошибки. Нижняя половина — не доказательства: покрытие и объяснения агента там не потому, что бесполезны, а потому что не отвечают на вопрос «работает ли». И главный вывод про цену, который редко произносят вслух: если задача дешевле и надёжнее делается руками, делайте руками. Написать регулярку самому — три минуты. Поставить задачу агенту, дождаться, прочитать диф, проверить, что тест не тавтологичен, прогнать — пятнадцать. Подробный разбор арифметики — в главе про цену.
Единственное число, которое я приведу, и сразу с оговорками: рандомизированное исследование METR (июль 2025) на опытных контрибьюторах крупных open source проектов зафиксировало, что с инструментами начала 2025 года задачи выполнялись в среднем медленнее, чем без них, — при том что сами участники ожидали ускорения и после эксперимента считали, что ускорились. Выборка маленькая, задачи специфические (зрелые репозитории, высокие требования к качеству), инструменты с тех пор изменились. Переносить вывод на вашу ситуацию нельзя. Что переносится — методологическая мораль: субъективное ощущение скорости не является измерением скорости, и это ровно та же ошибка, что «агент сказал — тесты проходят».
Что прогоном не проверяется
| Класс утверждения | Почему тест не оракул | Чем проверять |
|---|---|---|
| «Стало быстрее» | Тест меряет тестовую машину и тестовые данные | Бенчмарк с прогретым кэшем, замер в проде |
| «Безопасно» | Тест проверяет известные атаки | Модель угроз, ревью, статический анализ |
| «Понятно другим» | Машинного оракула нет | Ревью человеком |
| «Правильно с точки зрения бизнеса» | Тест кодирует ваше понимание требования | Приёмка заказчиком |
| «Миграция отработает на проде» | Данные в проде другие | Прогон на копии, dry-run, план отката |
| «Не сломает соседний сервис» | За границей репозитория | Контрактные тесты, стейджинг |
Первая строка стоит отдельного слова: агент особенно охотно утверждает про производительность, потому что «оптимизация» — сильный паттерн в обучающих данных. «Заменил список на множество, стало быстрее» звучит убедительно и может быть ложью на ваших размерах данных. Замер — или молчание; про методику — «Нагрузочное тестирование». Четвёртая строка о том, что тест кодирует ваше понимание требования: поняли неверно — зелёный набор закрепит ошибку. Тесты защищают не от неверной спецификации, а от расхождения кода с ней (см. «Приёмка»).
Дисциплина, а не ритуал
Собранный контракт проверяемости, который можно положить в файл правил репозитория и держать в контексте агента. Он короткий намеренно: длинные контракты не соблюдаются.
## Проверяемость
1. Утверждение о поведении = имя теста или вставленная команда с кодом выхода.
Всё остальное помечается [unchecked].
2. Утверждение о внешней системе = адрес: файл:строка, команда с выводом
или ссылка с датой. Память модели адресом не является.
3. Исправление бага начинается с теста, падающего по названной причине.
В отчёте — вывод падения ДО фикса.
4. Существующие тесты не редактируются. Нужно — остановись и спроси.
5. Новые тесты прогоняются дважды; при зависимости от порядка — с другим сидом.
6. Отчёт заканчивается разделом «Что не проверено» — списком, а не прочерком.
Шестой пункт делает больше, чем первые пять. Раздел «что не проверено» переводит умолчание из невидимого в явное: агент, обязанный заполнить этот список, заполняет его — а вы получаете карту дыр вместо ощущения полноты. В нашем пакете это RSN-15; сам пакет лежит в products/workbench/templates/memory/ (REASONING-DISCIPLINE.md — маркеры и требование называть фальсификатор до действия, VERIFIED-CODE.md — правила CODE-10…CODE-13 про доказательства, COMPOSITION-AND-LAWS.md — когда обязателен property-тест).
Отдельно про соблазн, к которому все приходят: попросить агента проверить самого себя — «перечитай свой код и найди ошибки». Это работает частично и по понятной причине: свежий проход без груза собственных объяснений действительно ловит часть дефектов. Но у самопроверки есть потолок — она не создаёт новых наблюдений. Модель проверяет текст текстом. Всё, что требует запуска, самопроверкой не проверяется. Правильная схема: самопроверка перед прогоном (дёшево отсеять мусор), прогон как оракул, человек на смысл. Про то, когда для проверки имеет смысл заводить отдельного агента и когда это дорогая иллюзия, — следующая глава.
Типичные ошибки инженера
- Верить зелёному без вопроса «а что было бы красным». Зелёный набор без ответа на этот вопрос не информативен.
- Читать отчёт вместо ленты. Отчёт написан моделью, лента содержит факты.
- Ставить целью процент покрытия. Гудхарт сработает быстрее, чем вы успеете обрадоваться.
- Прогонять весь набор на каждом шаге агента. Токены и время сгорят раньше, чем решится задача: узкий прогон в цикле, широкий — на гейте.
- Считать, что тесты, написанные агентом, — это спецификация. Это его гипотеза о спецификации, зафиксированная в коде.
- Разрешать агенту править существующие тесты по умолчанию. Это отключение сигнализации изнутри.
- Принимать «стало быстрее» без замера. И «стало безопаснее» без модели угроз (глава про безопасность).
Мини-итог
- Утверждение о поведении кода и наблюдение за поведением кода — разные вещи, которые в транскрипте выглядят одинаково. Различение — работа инженера, и она не автоматизируется полностью.
- Отчёт агента пишет модель: даже честный прогон превращается в пересказ на пути к вам. Факты лежат в блоках результата инструментов и в CI — единственном канале без языковой модели посередине.
- Свидетельства упорядочены: воспоминание (ноль) — чтение — типы — прогон — CI — прод. Утверждение стоит ровно столько, сколько ступень, на которой оно проверено.
- Рассуждение не заменяет прогон принципиально, а не из-за качества моделей: нетривиальные семантические свойства программ неразрешимы, и остаются аппроксимация, ограничение языка либо наблюдение конкретных запусков.
- Главная подделка — тест, который проходит и без фикса; проверка стоит тридцать секунд. Дальше по частоте: тавтологический тест, ассерты на моки, ноль собранных тестов при нулевом коде выхода, подгонка ожиданий,
skipв дифе, флак, покрытие вместо проверки. - Качество тестов измеряется мутационным тестированием, а не покрытием; property-based и golden-файлы закрывают две слабости агента — отсутствие инвариантов и отсутствие спецификации в legacy.
- Проверка, не оформленная в артефакт репозитория, — эпизод, а не проверка. Тест, тип, схема, гейт живут дольше сессии.
- Проверяемость стоит токенов, времени, внимания и инфраструктуры. Если задача дешевле делается руками — делайте руками.
- Есть класс утверждений, для которых прогон не оракул: производительность, безопасность, понятность, соответствие бизнес-требованию, миграции. Для них другие процедуры, и молчание лучше уверенного «проверено».
Что дальше
Мы разобрали, как проверить одного агента. Отсюда рождается естественная идея: если проверка стоит внимания человека, пусть проверяет второй агент — критик, ревьюер, «красная команда». Иногда это работает. Чаще получается схема, где два источника правдоподобного текста согласованно подтверждают друг друга, счёт за токены растёт кратно, а проверяемость не меняется. Следующая глава — про то, где многоагентность окупается, где она дорогая иллюзия и по каким признакам одно отличается от другого.
Многоагентные схемы: когда оправданы, а когда дорогая иллюзия
Источники
- Rice’s theorem — почему нетривиальные семантические свойства программ не проверяются анализом в общем случае.
- Edsger W. Dijkstra, «Notes on Structured Programming» (1970) — «тестирование показывает наличие ошибок, но не их отсутствие»; исходный текст доступен в архиве EWD.
- Michael Feathers, «Working Effectively with Legacy Code» — характеризационные тесты; Kent Beck, «Test-Driven Development: By Example» — красная фаза как средство контроля, а не ритуал.
- An Empirical Study of Flaky Tests (Luo et al., FSE 2014) и Flaky Tests at Google — причины нестабильных тестов и практика жизни с ними.
- State of Mutation Testing at Google — Petrović, Ivanković, ICSE-SEIP 2018. Мутационное тестирование в продакшене, включая честный разбор цены.
- PIT, Stryker, mutmut, Cosmic Ray — инструменты мутационного тестирования.
- Hypothesis и QuickCheck (Claessen, Hughes, ICFP 2000) — property-based тестирование.
- Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity — METR, 2025. Единственное число в главе и все оговорки к нему.
- Introducing SWE-bench Verified — OpenAI, 2024. Показательная история: часть задач исходного SWE-bench пришлось отфильтровать из-за недоспецифицированных условий и тестов, проверяющих не то. Читать как напоминание, что слабый набор тестов делает «зелёный» бессмысленным даже в академическом бенчмарке; про оценку моделей подробнее — в главе «Оценка и бенчмарки».
products/workbench/templates/memory/— наш пакет правил:VERIFIED-CODE.md(CODE-10…CODE-13— доказательства и красная фаза,CODE-12— запрет на зависимость от часов и сети),REASONING-DISCIPLINE.md(RSN-01— четыре маркера,RSN-03— память модели этоunchecked,RSN-15— отчёт заканчивается тем, что не проверено),COMPOSITION-AND-LAWS.md(когда property-тест обязателен).