Инженерия с ИИ-агентами Проверяемость: тесты, прогоны и доказательства вместо обещаний
0%

Проверяемость: тесты, прогоны и доказательства вместо обещаний

Проверяемость: тесты, прогоны и доказательства вместо обещаний

Предыдущая глава — «Где агенты врут» — про типологию отказов: что именно ломается и почему. Эта глава про противоядие, и оно ровно одно.

Утверждение агента о поведении кода не является наблюдением за поведением кода. Между ними — прогон, и никакая длина рассуждения его не заменяет.

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

Два вида текста в одной ленте

Агент заканчивает задачу так:

Готово. Добавил идемпотентность в 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 алгоритмически неразрешим — это теорема Райса, прямое следствие неразрешимости остановки. Никакой анализатор, включая идеального рассуждателя, не решит её в общем случае. Практические инструменты обходят это тремя способами: аппроксимируют (типы, статический анализ — верно, но неполно), ограничивают язык (тотальные языки, зависимые типы) или наблюдают конкретные запуски (тесты — полно для проверенных входов, ничего для остальных).

Агент делает четвёртое: угадывает по форме. Это работает удивительно часто, потому что код в мире регулярен, и совсем не работает там, где ваша кодовая база нерегулярна — а именно там и живут интересные баги. Дийкстра сформулировал границу теста задолго до всего этого: тестирование показывает наличие ошибок, но не их отсутствие. Для агента формулировка получает вторую половину: рассуждение агента не показывает даже наличия. Отсюда практический перевод: тест — не бюрократия, а единственный доступный вам оракул, который отвечает не текстом, а фактом. Оракул слабый (говорит только про выбранные входы), но честный.

Конвейер: от утверждения к доказательству

Каждое утверждение в отчёте агента полезно прогонять через один и тот же фильтр. Он занимает секунды и экономит часы.

Обратите внимание на ветку 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-10CODE-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-10CODE-13 — доказательства и красная фаза, CODE-12 — запрет на зависимость от часов и сети), REASONING-DISCIPLINE.md (RSN-01 — четыре маркера, RSN-03 — память модели это unchecked, RSN-15 — отчёт заканчивается тем, что не проверено), COMPOSITION-AND-LAWS.md (когда property-тест обязателен).

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов