Воспроизводимый контракт удобнее повторяющихся объяснений агенту о том, что нельзя сломать. Но количество автоматических проверок само по себе не говорит об их качестве.
Рассмотрим пример с Make targets, CI и контрактными тестами. Проверка может постепенно перестать защищать поведение системы и начать фиксировать детали одной реализации. LLM способны ускорить этот переход: очередной пограничный случай легко превращается в новый слой парсинга.
Всё начинается с полезного контракта
Требование простое: определённая проверка должна запускаться через один канонический Make target. Он должен существовать, реально выполнять нужную команду и одинаково использоваться локально и в CI. Обходного пути с другой реализацией той же проверки быть не должно.
Для такого контрактные тесты подходят хорошо.
Они защищают не конкретную строку Makefile, а инженерную договорённость:
есть один публичный способ выполнить проверку
→ локальный запуск использует его
→ CI использует его
→ изменение структуры не создаёт обходного пути
Проблема начинается, если пытаться доказать этот контракт через слишком подробное моделирование текста команд.
Тест может превратиться в парсер команд
На ревью легко придумать очередной гипотетический способ обойти проверку.
А если нужное имя встречается только в echo?
А если executable указан полным путём?
А если команда запускается через xargs?
А если Docker получает другой --entrypoint?
А если две команды имеют одинаковый префикс?
А что с разными вариантами quoting?
Каждый вопрос сам по себе выглядит разумно. Но если отвечать на все вопросы дополнительным разбором строк, тест постепенно получает собственную модель shell- и Docker-команд.
Он начинает различать аргументы, вызовы и строки данных, распознавать разные способы запуска executable и учитывать синтаксически разные, но эквивалентные конструкции.
Исходное требование при этом не меняется:
нужный Make target должен существовать и запускать требуемую проверку.
Для проверки нескольких строк Makefile получается маленький интерпретатор shell-команд. Тут стоит спросить: что именно он защищает?
Хрупкость может выглядеть как надёжность
Обычно хрупким называют тест, который падает после безобидного рефакторинга.
В рассматриваемом примере изменение quoting может потребовать правки теста. Эквивалентный способ вызвать ту же команду создаёт новый сценарий для парсера. Ещё один допустимый вариант запуска Docker требует научить тест понимать и его.
Наблюдаемое поведение при этом не меняется.
Ужесточается не контракт, а описание одной его реализации. Чем подробнее становится эта модель, тем сильнее тесты привязывают Makefile к текущей форме и тем дороже становятся изменения, которые контракт вообще не должен запрещать.
LLM здесь не причина, а ускоритель
Такую систему можно построить и без ИИ.
Но LLM резко снижают стоимость очередного усложнения.
Ревьюер за несколько секунд придумывает ещё пять способов обойти тест. Исполнитель так же быстро добавляет их поддержку. Следующее ревью находит новые варианты.
Получается цикл:
нашли теоретический обход
→ усложнили тест
→ добавили собственной логики
→ нашли обход уже этой логики
→ снова усложнили тест
Каждая итерация выглядит как повышение надёжности. Тестов больше, сценариев больше, отчёт агента убедительнее.
А вопрос «какое свойство системы мы защищаем?» постепенно теряется.
Агент хорошо решает локально поставленную задачу. Если попросить сделать тест сложнее для обхода, он может продолжать укреплять его намного дольше того момента, когда это перестало быть хорошим инженерным решением.
Вернуться к исходному контракту
Решением не обязательно должен быть более точный парсер.
Make уже умеет читать Makefile и разворачивать target. Поэтому часть собственной логики можно заменить поведением самого Make:
make --dry-run --silent <target>
Если target не существует или не разворачивается, это обнаруживает Make. Не нужно частично воспроизводить его поведение внутри теста. При этом разворачивание команды и её успешное выполнение — разные проверки.
Дальше стоит оставить проверки действительно важных свойств: публичных targets, канонической цепочки локального запуска и CI, обязательных зависимостей и реальных обходных путей.
Проверки синтаксических вариантов, которые не меняют контракт, требуют отдельного обоснования. Иначе вместе с ними приходится поддерживать служебные маркеры, парсинг и мутации, нужные в основном для проверки самого парсера.
Цель — не уменьшить число тестов любой ценой, а сделать понятным, что именно каждый из них защищает.
Контрактный тест не должен знать лишнего
Он не должен максимально подробно описывать устройство системы. Наоборот, хороший контракт оставляет реализации свободу меняться.
Если сегодня команда записана одним способом, а завтра другим, тест должен стать красным только тогда, когда нарушилось важное свойство системы.
Для себя я использую простой вопрос:
Если реализацию изменить, полностью сохранив наблюдаемое поведение, должен ли этот тест упасть?
Если да, нужна причина, почему конкретная деталь реализации сама является частью контракта.
Иногда такая причина есть. Например, CI действительно должен вызывать канонический Make target, иначе локальные и серверные проверки могут разойтись.
Но способ расставить кавычки внутри команды таким контрактом обычно не является.
Мутационное тестирование тоже требует границы
Само количество мутаций, на которых тест становится красным, ещё не говорит о качестве проверки.
Хорошая мутация представляет реальное нарушение контракта.
Удалить обязательную команду — полезная мутация.
Подменить нужный target другим — полезная.
Создать реальный обход обязательной проверки — тоже полезная.
А перебирать способы синтаксически иначе записать эквивалентную shell-команду бессмысленно, если её форма не является частью контракта.
Иначе тестовая система начинает защищать саму себя.
Ограничивать нужно и тесты агента
Недостаточно ограничить только область реализации: не делать лишний рефакторинг, не менять соседний код, добавить регрессионный тест, выполнить проверки.
Тест тоже может выйти за границы задачи.
Меня интересует не только то, ловит ли он ошибку. Важно понять, почему эта ошибка относится к контракту и нельзя ли проверить то же свойство на более устойчивой границе.
Особенно это важно при независимом ревью другим агентом.
Формулировка:
найди способ обойти тест
легко запускает бесконечную игру в кошки-мышки.
Полезнее проверить другое:
какое свойство защищает тест
→ действительно ли он его проверяет
→ не проверяет ли он лишнего
→ можно ли подтвердить то же свойство через наблюдаемое поведение
Так ревью проверяет качество самого контракта, а не изобретательность автора теста.
Автоматизировать можно и переусложнение
Если требование можно превратить в воспроизводимую проверку, это обычно надёжнее текстовой инструкции агенту.
Но у этого правила есть вторая половина.
Тест — тоже инженерное решение. Его можно переусложнить, слишком тесно связать с реализацией и потом поддерживать уже ради него самого.
ИИ-агенты делают написание тестов дешёвым. Они не делают дешёвым решение о том, что вообще стоит тестировать.
Поэтому количество автоматических проверок для меня не является самостоятельным признаком надёжности.
Хороший тест должен быть достаточно строгим, чтобы упасть при нарушении контракта, и достаточно свободным, чтобы не мешать менять всё остальное.
