Это первая статья из цикла «Хроники Agent Driven Development трансформации». В цикле я рассказываю, как постепенно перевожу реальный продакшен-проект на рельсы agent-driven development — когда LLM-агенты становятся полноценными участниками разработки, а не просто подсказчиками в автокомплите.

В нулевой статье я рассказал, как ускорил прогон ~800 тестов в 6 раз — с 10 минут до 101 секунды. Это было необходимой подготовкой: если agent feedback loop занимает 10 минут на каждый цикл «сгенерировал тест → скомпилировал → запустил → получил результат», то никакой agent-driven development не взлетит.

Что вы узнаете из этой статьи

  1. Два подхода к генерации тестов — sprint-driven и coverage-driven — и когда какой применять

  2. Шестиуровневый pipeline верификации, отсеивающий бесполезные тесты

  3. Двухагентная архитектура Writer + Reviewer с feedback loop

  4. Как ускорение компиляции напрямую влияет на эффективность agent-driven разработки

  5. Конкретные приёмы экономии токенов при массовой генерации

  6. Что сказали разработчики на code review — и почему 13% тестов были отклонены

Результаты в цифрах

68 тестовых файлов

сгенерировано LLM-агентом

86.8% acceptance rate

при ревью живыми разработчиками

~6% прирост branch coverage

на чистых тестах для непокрытого кода

30-50% экономия токенов

за счёт оптимизации feedback loop

Совсем кратко о себе: разрабатываю высоконагруженные сервисы, веду канал о разработке в стартапах в TG и в канале Max, где делюсь своим опытом.

Оглавление

  1. Контекст

  2. Идея: LLM как тестировщик

  3. Verification Pipeline: 6 gate'ов

  4. Двухагентная архитектура: Writer + Reviewer

  5. Подход 1: Sprint-driven генерация

  6. Подход 2: Coverage-driven генерация

  7. Ускорение компиляции: agent feedback loop

  8. Экономия токенов: agent efficiency

  9. Ревью разработчиками: human-in-the-loop

  10. Что дальше: LLM-тесты как часть спринта

  11. Что я понял


1. Контекст

Стек: Scala, Akka, SBT, ScalaTest, ScalaMock, PostgreSQL, Testcontainers. Бэкенд-монолит — ядро системы. Тесты — смесь unit и интеграционных. Интеграционные работают через собственный фреймворк поверх Testcontainers с миграцией БД через Liquibase.

Исходная картина (после оптимизации скорости тестов):

  • Прогон ~800 тестов за 101 секунду — CI больше не узкое место

  • Низкое покрытие — и statement, и branch coverage далеки от целевых значений

  • Пороги покрытия установлены в билде — ниже падать нельзя, но до целевых 90% как до Луны

  • Тесты пишутся вручную, и их хронически не хватает — продуктовые фичи всегда важнее

Задача: быстро нарастить покрытие, не тратя человеко-месяцы на написание тестов вручную.

2. Идея: LLM как тестировщик

Генерация тестов через LLM — тема не новая. Но исследования (Meta TestGen-LLM, MUTGEN 2025) показывают неприятную правду:

Проблема

Описание

Тесты-попугаи

Тестируют реализацию, ломаются при рефакторинге

Тесты-пустышки

Покрывают строки, но не ловят баги. MUTGEN показал: 100% line coverage → 4% mutation score

Test smells

Assertion Roulette, Magic Numbers, Thread.sleep, мокирование SUT

Просто попросить LLM «напиши тесты для этого класса» — получишь строки покрытия, но не безопасность релиза. Нужна система контроля качества. Как именно — дальше.

3. Verification Pipeline: 6 gate'ов

Я спроектировал многоуровневый pipeline, через который проходит каждый сгенерированный тест:

Шесть ступеней — от банальной компиляции до семантического ревью вторым LLM-агентом. Каждый gate отсеивает часть тестов: компиляция — ~20-25%, зелёный прогон — ещё ~15-20%, mutation testing — самый ценный, проверяет не что тест работает, а что он нужен.

Подробнее: описание каждого gate'а

Gate 1: Компиляция

Банальный, но отсеивающий ~20-25% первых попыток. LLM часто путает импорты, использует несуществующие методы или неправильные типы. При reject агент получает полный вывод компилятора и пытается исправить.

Gate 2: Зелёный тест

Тест должен проходить. Ещё ~15-20% отсеивается: неправильные assertions, неверные expected values, проблемы с тестовым окружением.

Gate 3: Стабильность (anti-flaky)

Запуск 3-5 раз. Любое расхождение → reject без права на апелляцию. Flaky тесты — это race conditions и shared state, которые LLM плохо чинит. Лучше сразу отбросить.

Gate 4: Mutation Testing

Ключевой gate. Stryker4s вносит мутации в целевой файл (замена > на >=, true на false, удаление вызовов) и проверяет, падает ли тест. Если не падает — тест бесполезен.

Пороги mutation score:

  • Чистые функции, модели: ≥ 50%

  • Сервисы, бизнес-логика: ≥ 40%

  • HTTP routes, DAO, акторы: ≥ 30%

При reject LLM получает конкретику:

Мутант выжил: строка 42, замена `if (count > 0)` → `if (count >= 0)`.
Ни один тест не обнаружил эту мутацию.
Добавь тест-кейс для граничного случая count == 0.

Gate 5: Smell Analysis

Автоматическая проверка антипаттернов:

Smell

Правило

No assertions

Тест без единого assert → REJECT

Assertion Roulette

> 5 assertions без описания → REJECT

Excessive Mocking

> 6 mock-объектов → REJECT

Thread.sleep

В unit-тестах → WARNING

Empty test

Пустое тело → REJECT

Gate 6: Семантическое ревью (Reviewer Agent)

Самый интересный. Отдельный LLM-агент проверяет то, что не ловят автоматические gate'ы:

  • Тавтологичность — expected value вычисляется тем же кодом, что и SUT

  • Соответствие задаче — тест реально проверяет то, что описано в задаче трекера

  • Полнота сценариев — есть happy path, граничные случаи, ошибки

  • Ценность — тест не тривиален (не проверяет getter или copy() data-класса)

Ключевой инсайт: Gate 4 (Mutation Testing) — самый ценный из шести. Остальные gate'ы проверяют, что тест работает. Mutation testing проверяет, что тест нужен — ловит ли он реальные баги при изменении кода.

4. Двухагентная архитектура: Writer + Reviewer

Главный инсайт: один LLM-агент не может одновременно и генерировать, и критиковать свой код. Нужно разделение ролей.

Writer Agent — пишет тесты. Получает задачу из трекера, анализирует SUT (system under test), генерирует тест-кейсы, пишет код.

Reviewer Agent — ревьюит результаты. Работает по чеклисту из 12 пунктов, знает антипаттерны проекта, имеет примеры хороших и плохих тестов из существующей кодовой базы.

Когда Reviewer отклоняет тест, Writer получает конкретный feedback:

REVISE: тест "should process message" тавтологичен.
Expected value `SUT.convert(input)` совпадает с вызовом SUT.
Замени на литерал, рассчитанный вручную.

Writer дорабатывает (не переписывает с нуля!), тест проходит быстрый re-check (Gate 1-2) и возвращается к Reviewer. Максимум 2 итерации.

Ключевой инсайт: feedback loop Writer → Reviewer даёт +21-32% к качеству assertions (по данным исследования Self-Refining LLM Unit Testers). Два агента по отдельности слабее, чем один цикл ревью между ними.

Архитектура описана — теперь два конкретных подхода, как её применять.

5. Подход 1: Sprint-driven генерация

Генерировать тесты «на весь проект» — утопия. Нет контекста, непонятно что важно. Первый подход — идти от задач трекера.

Python-скрипт загружает спринты и задачи из трекера по API, классифицирует их, и для каждой core-задачи Writer Agent генерирует тесты через pipeline.

Подробнее: классификация и шаги генерации

Шаг 1. Python-скрипт загружает спринты и задачи из трекера по API.

Шаг 2. Скрипт классификации автоматически распределяет задачи:

Категория

Критерий

Пример

core

Изменения в бэкенд-логике

Новая бизнес-фича, исправление бага

frontend

Только фронт

Правка CSS, новый компонент

adapters

Интеграции

Новый канал, webhook

technical

Инфраструктура

Миграция БД, обновление зависимостей

incidents

SRE

Расследование инцидента

Шаг 3. Для каждой core-задачи Writer Agent:

  1. Анализирует коммиты (какие файлы менялись)

  2. Находит SUT в коде

  3. Генерирует тест-кейсы в JSON

  4. Пишет тесты

  5. Прогоняет через pipeline

Каждый тест помечается тегами для трассировки:

it should "корректно обработать пустой ввод (TASK-1234)" taggedAs(LlmGenerated, Sprint("2.4.1")) in {
  // ...
}

LlmGenerated — помечает, что тест написан LLM (а не человеком). Sprint("2.4.1") — привязка к спринту. Так можно отфильтровать все LLM-тесты или все тесты конкретного спринта.

Результаты sprint-driven

Серия спринтов

Спринтов обработано

Тестов написано

Тип

Ранние спринты (3 серии)

8

12

integration

Средние спринты (3 серии)

23

119

unit + integration

Поздние спринты (2 серии)

14

155

unit + integration

Итого

45

286

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

Хорошее начало — но sprint-driven имеет ограничение. О нём — в следующем подходе.

6. Подход 2: Coverage-driven генерация

Sprint-driven подход дал 286 тестов — отличный старт. Но он генерирует тесты для задач, а не для кода. Если файл уже хорошо покрыт — тратим время на дубли. Если файл никогда не фигурировал в задачах — он остаётся непокрытым.

Следующий шаг — тестировать непокрытый код. Но не весь, а только тот, для которого можем гарантировать качество контекста.

Шаг 1: Анализ покрытия по пакетам

Отчёт покрытия с разбивкой по ~50 пакетам. Инструмент — scoverage. Ключевая метрика — branch coverage, а не statement (она честнее: показывает, какие ветки if/else/match реально проверены).

Шаг 2: Анализ свежести документации

LLM пишет хорошие тесты только при хорошем контексте. Если wiki-страница не обновлялась два года — контракты поменялись, а документация врёт. Тесты по устаревшей документации будут проверять поведение, которого уже нет.

Я проанализировал ~150 страниц внутренней wiki:

Категория

Критерий

Количество

Свежие

Обновлены в последние 6 месяцев

~60 страниц

Устаревшие

Не обновлялись более 6 месяцев

~90 страниц

Правило: генерируем тесты только для пакетов, у которых есть свежая документация. Остальные — в «долг», пока люди не обновят доки.

Шаг 3: Пересечение — низкое покрытие + свежие доки

На пересечении — идеальные кандидаты:

  • Модели и конвертеры внешних API (покрытие 5-15%, доки свежие)

  • Утилиты и хелперы (покрытие 10-25%, доки свежие)

  • Протоколы сериализации (покрытие 0-20%, доки свежие)

  • Конфигурационные модели (покрытие 15-30%, доки свежие)

Подробнее: результаты по раундам генерации

Работа шла итеративными раундами — по 10-50 тестов за раунд:

Раунд

Что покрывали

Тестов

Результат

1–6

Модели таймеров, конвертеры, утилиты, настройки

~170

Все зелёные

7–8

Протоколы фильтрации, статусы пользователей

~50

Все зелёные

9–13

Фильтры, конфиги платформ, модели сообщений

~65

62 зелёных, 3 удалены

Branch coverage вырос на ~6% относительно исходного значения — не headline-цифра, но это прирост на «чистых» тестах для непокрытого кода, а не на дублировании уже покрытых путей.

Ключевой инсайт: coverage-driven подход нацелен точно на непокрытые ветки. А фильтр по свежести документации отсекает пакеты, где LLM сгенерирует мусор из-за устаревшего контекста.

Оба подхода дали тесты. Но насколько быстро агент может итерироваться — зависит от скорости компиляции и обратной связи. Об этом — следующие два раздела.

7. Ускорение компиляции: agent feedback loop

Agent-driven development — это цикл: агент пишет код → компилирует → запускает тесты → анализирует результат → исправляет → повторяет. Каждая итерация стоит времени и токенов. Чем быстрее цикл — тем эффективнее агент.

Что

Было

Стало

Эффект

Полная компиляция модуля

~90с

~60с

–33%

Вывод компиляции

~2000 строк

~50 строк (только warnings)

–97% шума

Запуск одного теста

~15с (startup + test)

~12с

–20%

Для агента, который делает 5-10 итераций compile-test на один тестовый файл, это экономит 3-5 минут на файл.

Подробнее: что именно оптимизировали

Параллельная компиляция

Scala-компилятор по умолчанию компилирует backend (генерацию байткода) в один поток. Флаг -Ybackend-parallelism распараллеливает эту фазу:

scalacOptions += "-Ybackend-parallelism" -> "4"

На 4-ядерной машине это даёт ощутимое ускорение полной компиляции. Для инкрементальной (когда поменялся один файл) эффект меньше, но всё равно заметен.

Подавление шума компиляции

По умолчанию SBT выводит INFO-логи для каждого скомпилированного файла. При 500+ файлах в модуле — это тысячи строк, которые агент честно читает и тратит на них токены. Решение:

Compile / logLevel := Level.Warn

Теперь в вывод попадают только предупреждения и ошибки — то, что реально нужно для принятия решений.

JVM-тюнинг для сборки

Scala-компилятор — тяжёлое JVM-приложение. Дефолтные настройки памяти для него недостаточны:

-Xms3048m
-Xmx3048m
-XX:ReservedCodeCacheSize=256m
-XX:MaxMetaspaceSize=512m

Увеличенный heap и code cache снижают частоту сборки мусора, что критично при компиляции больших модулей. Отдельно — отключение логирования макросов ORM-фреймворка (-Dquill.macro.log=false), которое генерировало тысячи строк debug-вывода при компиляции DAO-слоя.

Ключевой инсайт: –97% шума компиляции — это не только экономия токенов. Это чище контекстное окно агента: меньше мусора → точнее анализ ошибок → меньше итераций.

8. Экономия токенов: agent efficiency

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

Суммарный эффект: 30-50% экономия токенов за раунд генерации. На масштабе 68 тестовых файлов — разница между «укладываемся в бюджет» и «нужно в два раза больше».

Подробнее: 4 проблемы и решения

Проблема 1: SBT-вывод затапливает контекстное окно

Типичный запуск sbt compile на Scala-проекте — это 2000+ строк вывода: разрешение зависимостей, компиляция каждого файла, загрузка плагинов. Агент читает всё это, тратит токены, а полезной информации там — exit code и, может быть, 5 строк ошибок.

Решение: logLevel := Level.Warn для компиляции + чтение результатов тестов из XML-отчётов (target/test-reports/*.xml) вместо парсинга консольного вывода. XML содержит структурированные данные: имя теста, статус, время выполнения, stacktrace при ошибке — всё, что нужно агенту для принятия решений.

Проблема 2: Избыточные измерения покрытия

В начале я запускал измерение покрытия после каждого раунда генерации (10-15 тестов). Каждое измерение — это полная пересборка с инструментацией + запуск всех 800+ тестов + генерация отчёта. 10-15 минут и несколько тысяч токенов на парсинг отчёта.

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

Проблема 3: Промежуточные артефакты

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

Решение: при массовой генерации тестов для покрытия — сразу писать тесты, без промежуточного JSON. Артефакт нужен для трассировки до задач трекера (sprint-driven), но при coverage-driven подходе задачи трекера не задействованы.

Проблема 4: Чтение полных исходных файлов

Агент читал целые файлы по 500-1000 строк, хотя для генерации теста нужна была одна функция на 20 строк. Остальные 980 строк — чистый расход токенов.

Решение: читать файлы точечно — с указанием offset и limit. Если нужна сигнатура метода — читаем 30 строк вокруг. Если нужен весь класс — тогда да, весь файл. Но осознанно.

Компиляция ускорена, токены сэкономлены. Но финальный вердикт — за живыми разработчиками.

9. Ревью разработчиками: human-in-the-loop

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

Все 68 LLM-сгенерированных тестовых файлов были отданы на ревью команде. Вердикт:

«В целом качество тестов хорошее, но некоторые тесты я бы всё-таки убрал.»

Статистика

Метрика

Значение

Всего тестовых файлов на ревью

68

Принято (good quality)

59

Рекомендовано к удалению

9

Acceptance rate

86.8%

Подробнее: почему 9 файлов отклонены

Паттерн отклонённых тестов — тесты на тривиальное поведение:

  • Простые маппинги enum → string — компилятор и так гарантирует, что маппинг работает. Тест проверяет, что StatusA.toString == "StatusA" — бесполезно.

  • Тесты геттеров data-классов — проверка, что entity.field возвращает то, что было передано в конструктор. Это не тест, это проверка, что Scala работает.

  • Тесты тривиальных конвертеров — когда конвертация сводится к case A => B; case C => D без бизнес-логики.

Интересно, что эти 9 файлов — как раз тот случай, когда Gate 6 (семантическое ревью) должен был их отсечь. Pipeline пропустил их, потому что формально тесты были корректны: компилировались, проходили, имели assertions. Но ценность этих тестов — околонулевая.

Уроки из ревью

  1. 86.8% acceptance rate — хороший результат для LLM-генерации. Но 13% мусора — тоже важная цифра. Без человеческого ревью эти 9 файлов остались бы навсегда, создавая ложное чувство безопасности.

  2. Pipeline не ловит тривиальность. Все 6 gate'ов проверяют корректность и стабильность. Но вопрос «а стоит ли вообще это тестировать?» — пока только для человека.

  3. Ревью LLM-тестов быстрее, чем ревью LLM-кода. Тесты — это контракт на поведение. Разработчику проще оценить «имеет ли смысл этот контракт?», чем вникать в реализацию.

Ключевой инсайт: ревью LLM-тестов занимает минуты, а не часы. Тест — это спецификация поведения, и оценить «нужна ли эта спецификация?» гораздо быстрее, чем писать её с нуля.

10. Что дальше: LLM-тесты к��к часть спринта

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

Идея: каждый спринт заканчивается не только релизом, но и автоматической генерацией тестов для задач этого спринта. Разработчик пишет код и свои тесты, а LLM-агент параллельно генерирует дополнительные — на граничные случаи, негативные сценарии, ветки, которые человек мог пропустить.

Как это выглядит на практике:

  1. Спринт закрыт, задачи смержены в основную ветку

  2. Агент получает список core-задач спринта из трекера

  3. Для каждой задачи — анализирует изменённые файлы, находит непокрытые ветки

  4. Генерирует тесты, прогоняет через pipeline

  5. Результат — merge request с новыми тестами, готовый к ревью

Разработчику остаётся только посмотреть MR и принять или отклонить. По нашему опыту, 86.8% тестов принимаются без правок — это 10-15 минут ревью вместо нескольких часов написания.

LLM-тесты не заменяют тесты разработчика, а дополняют их. Разработчик лучше понимает бизнес-контекст и пишет тесты на критические сценарии. LLM лучше перебирает комбинации и граничные случаи — ту рутину, на которую у людей вечно не хватает времени.

11. UPD_1: По просьбам читателей добавил промт:

Промт

# Verification Pipeline (6 Gates) — универсальный промпт

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


Общие правила

Для каждого сгенерированного теста последовательно выполняй Gates 1 → 2 → 3 → 4 → 5 → 6.

  • При REJECT (Gates 1–5) — исправляй тест и повторяй проход (до 3 попыток).

  • При REVISE (Gate 6) — дорабатывай по feedback и повторяй Gate 6 (до 2 итераций).


Gate 1: COMPILE

Цель: тест компилируется без ошибок.

Действие: запусти компиляцию тестового модуля (например: [build_tool] [test_module]/Test/compile).

Успех: exit code 0, нет ошибок компиляции.

При REJECT:

  • Прочитай вывод компилятора.

  • Типичные проблемы: отсутствующие импорты, неверные типы, несуществующие методы/API.

  • Исправь тест и повтори Gate 1.

Формат feedback для исправления:

REJECT: Gate 1 — COMPILE ERROR

Ошибки компилятора:
{вывод компилятора}

Инструкция: исправь ошибки компиляции. Проверь импорты, типы, совместимость API.

Gate 2: GREEN TEST

Цель: тест проходит (все кейсы зелёные).

Действие: запусти только данный тестовый класс (например: [build_tool] [test_module]/testOnly [full.package.TestClassName]).

Успех: все тест-кейсы зелёные.

При REJECT:

  • Прочитай stack traces и сообщения об ошибках assertions.

  • Типичные проблемы: неверные ожидаемые значения, неправильный setup, недостающие заглушки.

  • Не подгоняй тест — пойми реальное поведение кода и скорректируй ожидания.

  • Исправь и повтори с Gate 1 (перекомпиляция).

Формат feedback:

REJECT: Gate 2 — TEST FAILURE

Упавшие тесты:
{test_name}: {assertion_error_or_stack_trace}

Инструкция: исправь ожидаемые значения или setup. Не подгоняй тест — проверяй реальный контракт.

Gate 3: STABILITY (anti-flaky)

Цель: тест стабильно проходит при повторных запусках.

Процедура:

  • Для unit-тестов: запусти один и тот же тест 3 раза.

  • Для integration-тестов: запусти 5 раз.

  • Все запуски должны дать одинаковый результат (все зелёные).

Успех: все запуски зелёные.

При REJECT (flaky):

  • Flaky-тесты не исправляются через feedback loop (часто из-за гонок или недетерминизма).

  • Пометь тест-кейс как skipped_flaky и перейди к следующему.

  • В отчёте по тесту укажи "status": "skipped_flaky".


Gate 4: MUTATION SCORE

Цель: тест реально ловит баги, а не просто проходит по строкам кода.

Действие: запусти mutation testing для тестируемого кода с фильтром по текущему тестовому классу (инструмент и команда зависят от стека: например, Stryker, PIT, mutmut и т.п.).

Пороги (ориентиры):

Тип кода

Минимальный mutation score

Чистые функции, модели, конвертеры

≥ 50%

Сервисы, бизнес-логика

≥ 40%

HTTP/API, DAO, акторы/слои I/O

≥ 30%

Успех: mutation score ≥ порога для данного типа кода.

При REJECT:

  • Открой отчёт мутационного тестирования.

  • Найди выжившие мутанты — это места, которые тест не проверяет.

  • Добавь тест-кейсы для граничных значений и пропущенных веток.

  • Повтори с Gate 1.

Формат feedback:

REJECT: Gate 4 — LOW MUTATION SCORE ({score}% < {threshold}%)

Выжившие мутанты:
1. Файл: {file}, строка {line}
   Мутация: замена `{original}` → `{mutated}`

Инструкция: добавь тест-кейсы, которые упадут при указанных мутациях.

Опционально: на первых итерациях Gate 4 можно пропустить, если мутационное тестирование ещё не настроено или слишком медленное. В этом случае пометь "gate4": "skipped" и вернись к нему позже.


Gate 5: SMELL ANALYSIS

Цель: тест не содержит антипаттернов (test smells).

Действие: запусти скрипт/инструмент статического анализа тестов на антипаттерны для файла с тестом (например: [smell_script] [path/to/TestSpec]).

Успех: exit code 0, нет REJECT-level нарушений.

При REJECT:

  • Прочитай вывод — там указаны конкретные smells и строки.

  • Исправь REJECT-level проблемы, например:

    • No assertions → добавь осмысленные проверки.

    • Assertion Roulette → разбей на отдельные тесты с одной идеей на тест.

    • Excessive Mocking → упрости, используй реальные объекты или null где уместно.

    • Reflection → тестируй через публичный API.

    • Empty test → заполни тело теста.

  • WARNING можно оставить, но лучше исправить.

  • Повтори с Gate 1.


Gate 6: SEMANTIC REVIEW (Reviewer Agent)

Цель: семантическая проверка качества тестов вторым агентом (Reviewer). Ловит проблемы, которые автоматические Gates 1–5 не видят.

Что проверяет Reviewer:

Аспект

Пример проблемы

Соответствие задаче

Задача про баг с пустым входом, а тест проверяет только happy path

Тавтологичность

expected = SUT.method(x); result shouldBe expected

Полнота сценариев

Нет теста на граничный случай или ошибку

Корректность контракта

Тест ожидает исключение, а метод по контракту возвращает Option/Result

Осмысленность моков

Замокан сам тестируемый класс вместо зависимостей

Дублирование

Новый тест повторяет существующий

Ценность

Тестируется тривиальное поведение (getter, copy без логики)

Входы для Reviewer:

  1. Задача из трекера (ключ, тип, описание).

  2. Исходный код тестируемого класса (SUT).

  3. Сгенерированный тест (прошедший Gates 1–5).

  4. Обоснование от Writer Agent (какие сценарии покрыты).

  5. Существующие тесты (для проверки дублирования).

  6. Результаты Gate 4 (mutation score, выжившие мутанты).

Вердикты:

  • ACCEPT — тесты семантически корректны → тест принят.

  • REVISE — есть замечания → Writer дорабатывает тест (быстрый re-check).

  • REJECT — тесты в целом не соответствуют задаче → Writer переписывает с нуля.

При REVISE (быстрый re-check):

  1. Writer получает конкретный feedback от Reviewer.

  2. Writer вносит правки.

  3. Если изменения малые (diff ≤ 30% строк) — прогон только Gate 1 + Gate 2.

  4. Если изменения значительные (diff > 30%) — полный проход Gates 1–5.

  5. После этого — повторный Gate 6.

  6. Максимум 2 итерации Gate 6. После 2 ревизий — принять с пометкой или skip.

При REJECT: тест возвращается Writer'у на полную перегенерацию → снова Gates 1–5 → Gate 6. Это расходует одну из 3 попыток основного loop.


Правила Feedback Loop

Ситуация

Действие

Gate 1 REJECT

Исправить компиляцию → повтор с Gate 1

Gate 2 REJECT

Исправить assertions/setup → повтор с Gate 1

Gate 3 REJECT (flaky)

Skip — пометить skipped_flaky, перейти к следующему кейсу

Gate 4 REJECT

Добавить тесты на выживших мутантов → повтор с Gate 1

Gate 5 REJECT

Устранить smell → повтор с Gate 1

Gate 6 REVISE

Доработать по feedback Reviewer → Gate 1–2 (или 1–5) → Gate 6 (до 2 итераций)

Gate 6 REJECT

Переписать тест с нуля → полный pipeline Gates 1–5 → Gate 6 (расходует попытку)

3 попытки исчерпаны

Skip — удалить/не принимать тест, перейти к следующему кейсу

  • Максимум 3 попытки на тест-кейс (основной loop, Gates 1–5). Каждая попытка — заново с Gate 1.

  • Максимум 2 итерации Gate 6. После 2 ревизий — принять с пометкой или skip.


Промпт для Reviewer Agent (Gate 6)

Ниже — обобщённый системный промпт для агента, выполняющего семантическое ревью.

Ты — опытный разработчик, который проводит code review сгенерированных тестов.
Твоя задача — оценить СЕМАНТИЧЕСКОЕ КАЧЕСТВО тестов: насколько они полезны,
корректны и соответствуют бизнес-требованиям.

Тесты уже прошли механические проверки (компиляция, зелёный прогон, стабильность,
mutation testing, smell analysis). Тебе НЕ нужно проверять:
- Компилируется ли тест
- Проходит ли тест
- Есть ли test smells (assertion roulette, magic numbers и т.д.)

### ЧТО ТЫ ПРОВЕРЯЕШЬ

1. СООТВЕТСТВИЕ ЗАДАЧЕ.
   Тесты должны покрывать ИМЕННО то поведение, которое описано в задаче.
   Если задача про обработку ошибок — тесты должны проверять ошибочные сценарии.

2. ТАВТОЛОГИЧНОСТЬ.
   Тест НЕ должен вычислять ожидаемое значение тем же способом, что и тестируемый код.
   Корректный подход: expected values — литералы или заранее подготовленные данные.

3. ПОЛНОТА СЦЕНАРИЕВ.
   Должны быть: happy path, граничные случаи (пустой вход, null, 0), ошибочные сценарии.

4. КОРРЕКТНОСТЬ КОНТРАКТА.
   Assertions должны соответствовать реальному контракту метода (документация, тип возврата).

5. ОСМЫСЛЕННОСТЬ МОКОВ.
   Тестируемый класс (SUT) не должен быть замокан. Моки — только для зависимостей.

6. ДУБЛИРОВАНИЕ.
   Новый тест не должен повторять уже существующие сценарии.

7. ЦЕННОСТЬ.
   Тест тривиального поведения (getter, copy без логики) — низкоценный; помечать, но не reject'ить файл из-за одного такого.

### ФОРМАТ ОТВЕТА

Для каждого тест-кейса:
  Тест: "{имя теста}"
  Вердикт: OK | REVISE | LOW_VALUE
  Уверенность: HIGH | MEDIUM | LOW
  Причина: {если REVISE или LOW_VALUE}
  Рекомендация: {если REVISE — конкретное указание Writer Agent'у}

Итоговый вердикт файла:
  Вердикт: ACCEPT | REVISE | REJECT
  Резюме: {1–2 предложения}
  Пропущенные сценарии: {если есть}

### ПРАВИЛА ВЕРДИКТА

- REVISE только при уверенности HIGH.
- Строго к тавтологичности и соответствию задаче.
- Не переписывай тесты сам — давай конкретные инструкции Writer Agent'у.

Итог

  • 6 Gates: Compile → Green Test → Stability → Mutation Score → Smell Analysis → Semantic Review.

  • До 3 попыток на исправление при REJECT на Gates 1–5.

  • До 2 итераций Gate 6 при REVISE.

  • Команды, пути, имена модулей и пороги — подставляются под конкретный проект и стек.

12. Что я понял

Agent feedback loop — главный bottleneck

Не качество модели, не количество токенов, а скорость цикла «написал → скомпилировал → запустил → получил результат». Если цикл занимает 10 минут — агент делает 6 итераций в час. Если 2 минуты — 30 итераций. Разница в 5 раз при тех же затратах.

Два подхода дополняют друг друга

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

Свежесть документации — фильтр качества

Генерировать тесты по устаревшей документации — создавать тесты, которые проверяют поведение двухлетней давности. Анализ свежести wiki занял полдня, но сэкономил десятки часов на ревью и переделке.

Экономия токенов — не мелочь

При массовой генерации тестов разница между «агент читает 2000 строк SBT-лога» и «агент читает 50 строк ошибок» — это 30-50% бюджета. Оптимизация вывода сборки, точечное чтение файлов, отложенное измерение покрытия — каждое по отдельности мелочь, вместе — существенная экономия.

86.8% acceptance rate — хороший, но не идеальный результат

13% отклонённых тестов — это тесты тривиального поведения, которые pipeline не отсёк. Нужен дополнительный gate: проверка ценности теста. Добавить в Reviewer Agent правило «если SUT — чистый маппинг без бизнес-логики, пропускай».

Двухагентная архитектура работает, но не панацея

Writer + Reviewer с feedback loop даёт +21-32% к качеству assertions. Но 9 файлов из 68 всё равно прошли pipeline и были отклонены человеком. Агент-ревьюер пока не умеет оценивать «бизнес-ценность» теста — только его техническую корректность.

Заключение

68 тестовых файлов, 86.8% acceptance rate при ревью разработчиками, branch coverage вырос на ~6%.

Цифры скромные? Возможно. Но за ними — выстроенный процесс, который масштабируется. Sprint-driven генерация + coverage-driven наращивание + оптимизированный feedback loop — это машина, которая каждую неделю добавляет в проект десятки тестов без участия людей в написании кода.

Главное, что я вынес из этого этапа: agent-driven development — это не про «дай LLM написать код». Это про инженерию процесса. Ускорение компиляции, подавление шума, анализ покрытия, фильтр документации, pipeline верификации — каждый из этих элементов по отдельности тривиален. Вместе они превращают LLM из игрушки в инструмент.

В своем канале в Telegram и в канале Max о разработке в стартапах рассказываю ещё больше интересного и делюсь опытом, заходите, буду рад!

Всем добра и тихих релизов!