AI выдаёт код, который выглядит правильно. В этом и опасность. Компилируется, тесты зелёные, руками кликнул — работает. И вы закрываете задачу. А через две недели выясняется, что при пустой корзине заказ всё равно создаётся, при повторном нажатии списывается дважды, а «ошибка платежа» тихо превращается в успешный ответ. Код был не сломан — он был правдоподобен. Он делал не то, что нужно, но делал это уверенно.
Приёмка — это фаза, где вы отделяете «сделано» от «выглядит как сделано». И это отдельная работа, не побочный эффект того, что «я же прочитал диф».
Приёмка — это не code review
Их легко перепутать, потому что обе происходят над одним и тем же пул-реквестом. Но смотрят они в разные стороны.
Code review отвечает на вопрос «хорошо ли это написано». Контракт на стыках, крайние случаи, тесты на поведение, соответствие методологии — про это отдельный разговор в «Как ревьюить код, который написал AI». Ревью смотрит внутрь кода: чистые ли слои, нет ли выдуманного API, там ли транзакция.
Приёмка отвечает на вопрос «то ли это вообще сделано». Ей всё равно, красивый ли Handler, если он реализует не ту операцию. Можно написать безупречный по всем правилам код, который решает соседнюю задачу. Code review его пропустит — там всё чисто. Приёмка обязана его завернуть.
Разница практическая. Ревью можно частично отдать линтерам и AI-скиллам — качество кода машинно-проверяемо. Приёмку нельзя делегировать целиком: эталон, с которым вы сравниваете результат, живёт не в коде. Он живёт в контракте.
Один и тот же пул-реквест проходит две проверки в разные стороны: ревью смотрит внутрь кода и пропускает безупречно написанный код не той задачи, а приёмка сверяет его с контрактом и заворачивает.
Эталон приёмки — контракт, а не код
Эталон — это контракт, написанный до генерации: границы (что входит, а что явно нет), критерии приёмки (наблюдаемые условия «готово»), интерфейсы на стыках. Приёмка — сверка результата AI с этим документом, пункт за пунктом. Как продуктовый срез превращается в такой контракт, разбирает статья «От модели понятий к контракту» — она дальше в программе; здесь контракт нужен как уже готовый документ.
Три оси сверки:
- Критерии приёмки выполнены? Каждый критерий из контракта — это проверяемое утверждение о поведении. «При оплате заказа с недостаточным балансом заказ остаётся в pending, платёж не проходит, пользователь видит понятную ошибку». Не «платёж работает», а конкретное наблюдаемое условие. Пройдите по списку — каждый пункт либо подтверждён тестом, либо вы смотрите на него руками.
- Границы соблюдены? AI любит сделать «заодно». Вы просили оплату — он добавил отмену, потому что «логично рядом». Всё, что за границей среза, — не бонус, а незапрошенное поведение, которое вы теперь обязаны поддерживать и которое никто не проектировал. Заворачивайте.
- Интерфейсы на стыках совпадают? Сигнатуры, имена полей, коды ошибок, формат события — ровно то, что записано в контракте. Тихое переименование поля «ради удобства» ломает всё, что снаружи.
Ключевой сдвиг: вы сравниваете не «код с тем, как я его себе представлял», а код с документом, написанным до генерации. Если контракта нет, приёмки нет — есть только «мне кажется, похоже на правду», а это ровно то состояние, в котором правдоподобный код проходит.
Контракта нет: с чего начать
«Нет контракта — приёмки нет» верно и бесполезно для того, у кого контракта нет прямо сейчас, а задача уже сделана агентом. Полный контракт писать поздно, но приёмку спасти можно — за двадцать минут и в таком порядке.
1. Выпишите, что должно получиться, до того как смотреть на код. Три-пять строк своими словами: какое поведение ожидаете, что считается успехом, чего быть не должно. Это делается не глядя на диф — иначе вы просто перескажете то, что написал агент, и сверять будете с ним же. Пункт кажется формальным, а он и есть весь приём: у вас появился независимый эталон.
2. Спросите у задачи, а не у кода. Источники, из которых собирается эталон, когда контракта нет: формулировка задачи в трекере, переписка с тем, кто просил, поведение старой версии («было так, должно остаться»), соседний похожий участок кода как образец, чужой контракт на стыке (тот, кто вас вызывает, ожидает конкретного ответа).
3. Проговорите крайние случаи заранее. Три вопроса, которые закрывают большинство правдоподобных ошибок: что при пустом или отсутствующем значении, что при повторном вызове, что при отказе внешней системы. Ответы на них — уже критерии, и их три строки стоят часа отладки.
4. Зафиксируйте это письменно — и в репозиторий. Хоть комментарием в задаче, хоть файлом рядом с кодом. Здесь и происходит превращение: то, что вы только что написали для одной приёмки, становится контрактом для следующей. Через три-четыре таких захода у вас есть документ и привычка, а не «надо бы завести спеку».
5. Попросите агента назвать предположения. «Перечисли, какие допущения ты сделал там, где требования не были заданы» — и вы получите список мест, где он решил за вас. Это самый быстрый способ найти дыры в собственном эталоне: половина пунктов окажется теми самыми скрытыми требованиями.
Чего делать не стоит: принимать по принципу «прокликал — работает» и обещать себе «контракт напишем потом». Потом наступает при следующей похожей задаче, и на ней всё повторяется.
Где AI врёт правдоподобно
Правдоподобная ошибка — та, которую не видно на успешном пути. AI силён в успешном сценарии: его в обучающей выборке подавляющее большинство. Ошибается он там, где данных было меньше, — и ровно там, где ошибка не бросается в глаза.
Крайние случаи. Пустая коллекция, отсутствие данных, ноль, граница диапазона. Пустой заказ создаётся «успешно»; среднее по пустому списку в вещественных числах тихо превращается в NaN и уезжает дальше как обычное число, а в целых — падает с «/ by zero» уже у пользователя; findById вернул «не найдено», и код пошёл дальше с null. Успешный путь при этом безупречен.
Обработка ошибок. Самое типичное правдоподобное враньё. Внешний вызов упал — а catch-блок вернул пустой результат или дефолт. Формально «обработано», по факту ошибка проглочена. Ответ 200, тело правдоподобное, беда — молчаливая. Проверяйте не «есть ли try-catch», а «что именно происходит в ветке отказа»: пробрасывается типизированная ошибка, откатывается транзакция, повторяется идемпотентно — или тихо возвращается заглушка.
Скрытые требования. То, что в контракте есть, но в промпте вы не повторили, потому что «это же очевидно». Идемпотентность повторного платежа. Проверка прав: этот пользователь вообще может отменять этот заказ? Согласованность события и записи в одной транзакции. AI не додумывает неявное — он заполняет пропуски средним по выборке. Если требование не проговорено в контракте и не проверяется — считайте, что его нет.
Общее у всех трёх: они проходят демонстрацию. Поэтому «я прокликал, работает» — не приёмка, а первый и самый слабый её слой.
Порядок чтения: не сверху вниз
Раз ошибки прячутся в определённых местах, то и читать изменение стоит не подряд, а начиная с этих мест. Порядок, который экономит больше всего времени:
- Ветки отказа. Найдите в изменении все
catch, все проверки на пустое значение, все ветки «иначе» — и прочитайте только их. Вопрос к каждой: что здесь произойдёт, если беда случится на самом деле? Вернётся заглушка, проглотится ошибка, останется незакрытая транзакция? Это место, где правдоподобный код врёт чаще всего, и оно занимает пять процентов дифа. - Удаления. Смотрите, что из дифа исчезло: строки проверок, вызовы, условия, целые ветки. Агент, которому что-то мешало, любит «упростить» — убрать проверку прав, убрать условие, которое ломало его вариант, убрать якобы неиспользуемый код. Удаления при обычном чтении сверху вниз проскакивают незаметно, потому что глаз ищет новое.
- Стыки. Имена полей, коды ошибок, подписи методов, формат события — сверить с контрактом буквально. Тихое переименование здесь стоит дороже всего остального.
- Границы среза. Есть ли в диффе файлы и папки, которых в задаче не было? Появились ли новые зависимости, миграции, настройки? Это ответ на вопрос «сделал ли лишнее», и он виден по списку файлов, ещё до чтения кода.
- И только потом — успешный путь. Он почти наверняка в порядке, и читать его стоит последним, когда внимание уже потрачено на опасные места.
Практический приём к пункту 1: попросите агента самого перечислить все ветки отказа в изменении и что происходит в каждой. Список из десяти строк читается за минуту, а несовпадение с кодом видно сразу — и это же удобный вход в разговор «а почему здесь возвращается пустой список».
Почему тесты пишутся из контракта, а не из кода агента
Если AI написал и код, и тесты к нему, — тест проверяет, что код делает то, что код делает. Тавтология в зелёном. Он зафиксирует текущее поведение, включая баги, и будет их защищать при каждом рефакторинге. Такой тест — регрессия от правильного поведения, а не защита от неправильного.
Тест имеет ценность только когда его эталон независим от реализации. Источник эталона — контракт: критерии приёмки прямо превращаются в тест-кейсы. «Заказ с недостаточным балансом остаётся в pending» — это готовый тест, написанный до того, как агент притронулся к коду. Он проверяет поведение из спеки, а не форму кода агента.
Как это выглядит вживую — на том же критерии, который дважды упомянут выше. Критерий из контракта:
ЕСЛИ у покупателя недостаточно средств
КОГДА он оплачивает заказ
ТОГДА заказ остаётся в статусе pending
И платёж не создаётся
И покупатель получает ошибку INSUFFICIENT_FUNDS
Тот же критерий как тест — построчно, один в один:
@Test
void insufficientFunds_keepsOrderPendingAndCreatesNoPayment() {
var order = orders.save(anOrder().status(PENDING).total(money(5_000)).build());
payments.stubBalance(order.customerId(), money(100)); // недостаточно средств
var response = api.pay(order.id()); // КОГДА оплачивает
assertThat(response.status()).isEqualTo(422);
assertThat(response.errorCode()).isEqualTo("INSUFFICIENT_FUNDS");
assertThat(orders.byId(order.id()).status()).isEqualTo(PENDING); // ТОГДА остаётся
assertThat(payments.byOrder(order.id())).isEmpty(); // И не создаётся
}
func TestInsufficientFunds_KeepsOrderPendingAndCreatesNoPayment(t *testing.T) {
order := orders.Save(anOrder().Status(Pending).Total(money(5_000)).Build())
payments.StubBalance(order.CustomerID, money(100)) // недостаточно средств
resp := api.Pay(order.ID) // КОГДА оплачивает
require.Equal(t, 422, resp.Status)
require.Equal(t, "INSUFFICIENT_FUNDS", resp.ErrorCode)
require.Equal(t, Pending, orders.ByID(order.ID).Status) // ТОГДА остаётся
require.Empty(t, payments.ByOrder(order.ID)) // И не создаётся
}
it('insufficient funds keeps order pending and creates no payment', async () => {
const order = await orders.save(anOrder().status('PENDING').total(money(5_000)).build());
payments.stubBalance(order.customerId, money(100)); // недостаточно средств
const response = await api.pay(order.id); // КОГДА оплачивает
expect(response.status).toBe(422);
expect(response.errorCode).toBe('INSUFFICIENT_FUNDS');
expect((await orders.byId(order.id)).status).toBe('PENDING'); // ТОГДА остаётся
expect(await payments.byOrder(order.id)).toEqual([]); // И не создаётся
});
def test_insufficient_funds_keeps_order_pending_and_creates_no_payment():
order = orders.save(an_order().status(PENDING).total(money(5_000)).build())
payments.stub_balance(order.customer_id, money(100)) # недостаточно средств
response = api.pay(order.id) # КОГДА оплачивает
assert response.status == 422
assert response.error_code == "INSUFFICIENT_FUNDS"
assert orders.by_id(order.id).status == PENDING # ТОГДА остаётся
assert payments.by_order(order.id) == [] # И не создаётся
Соответствие тут буквальное, и это главное свойство: «ЕСЛИ» становится подготовкой, «КОГДА» — одним вызовом, каждое «ТОГДА» и «И» — отдельным утверждением. Имя теста повторяет критерий, поэтому в отчёте о падении видно, какой пункт контракта нарушен, без чтения кода.
Три вещи, которые делают такой тест настоящей приёмкой, а не имитацией. Проверяется наблюдаемое состояние, а не вызовы: статус заказа в хранилище и отсутствие платежа, а не «метод такой-то был вызван» — иначе тест привязан к реализации агента. Проверяются все пункты критерия, включая отрицательные («платёж не создаётся»): именно они ловят правдоподобную ошибку, потому что положительный пункт обычно выполняется. И тест пишут до генерации кода — тогда он не может случайно описать то, что агент сделал.
Полезная привычка: попросить агента сначала превратить критерии в тесты (это он делает хорошо — перевод формы), прочитать их глазами и только потом дать задачу на код. Проверка, что тесты не пустые, занимает минуту: сломать проверяемое место и убедиться, что они краснеют.
Отсюда практика: критерии приёмки формулируются до генерации и превращаются в тесты до приёмки. Тогда «зелёные тесты» означают «поведение из контракта соблюдено», а не «код не падает». Разница между этими двумя смыслами зелёного — и есть разница между приёмкой и её имитацией. Формализованный, версионируемый эталон, по которому это проверяется, — тема «Executable engineering standard»: правило, которое машина умеет проверить, стоит десяти правил в голове.
Что руками, что автотестами
Разделение простое: что можно выразить как проверяемое условие — автотест; что требует суждения — руками.
Автотестами закрывайте:
- Каждый критерий приёмки, сформулированный как наблюдаемое условие.
- Крайние случаи из контракта: null, пустая коллекция, граница, конкурентный доступ.
- Ветки отказа: внешний сервис упал — проверьте, что происходит именно то, что задумано.
- Стыки: формат ответа, коды ошибок, поля события — с конкретными значениями, не «не null».
Руками смотрите то, где нет объективного эталона:
- Границы среза — сделал ли AI лишнее. Машина не знает, что за границей; знаете вы.
- Смысл, а не форма — решает ли это исходную продуктовую задачу, а не соседнюю.
- Скрытые требования, которые вы могли не записать в контракт. Приёмка — последний момент их поймать и дописать.
- Правдоподобность ответа при отказе — не выглядит ли «успехом» то, что на деле проглоченная ошибка.
Работает это только с честным эталоном. Нет контракта — приёмка сводится к «на глаз похоже на правду», и правдоподобный код проходит именно потому, что он правдоподобен. Контракт — это то, что переводит приёмку из ощущения в проверку.
Глубже: порядок работы во временирасширенное
Из статьи складывается впечатление, что приёмка — это то, что делают в конце. Наполовину так, но половина работы приходится на до генерации, и именно она определяет, получится ли приёмка вообще. Полный порядок выглядит так.
До генерации (вы). Сформулировать критерии приёмки как наблюдаемые условия и границы среза — что входит и чего явно не делаем. Это единственный момент, когда эталон можно написать честно: после того как вы увидели результат, ваши представления уже подстроились под него.
До генерации (можно с агентом). Превратить критерии в тесты. Перевод формы — сильная сторона модели, и здесь она полезна; ваша работа — прочитать полученные тесты и проверить, что они краснеют на текущем коде. Красный тест до начала работы — это и есть то, что делает приёмку возможной.
Во время. Смотреть на план и на промежуточные шаги, а не ждать готового результата: несоответствие, замеченное на плане, стоит минуты, а замеченное в готовом изменении — переделки. Если агент по ходу спрашивает или предполагает — это сигнал, что критерий был неполон, и его дописывают сразу, пока помните.
После, приёмка (вы). Сверка по трём осям: критерии, границы, стыки. Порядок чтения — с опасных мест, как выше. Здесь же решение: принять, вернуть на доработку с конкретным пунктом или переделать самому.
После приёмки (можно с агентом). Code review — качество кода: слои, дублирование, имена, соответствие правилам проекта. Именно в этом порядке: сначала «то ли сделано», потом «хорошо ли написано». Обратный порядок — самая частая потеря времени: полчаса замечаний по стилю кода, который потом целиком выбрасывается, потому что решал не ту задачу.
Самое важное — кто. Критерии пишет тот, кто отвечает за результат; агент может помочь их сформулировать, но не может их назначить: он не знает, что нужно продукту. И приёмку не делает тот же агент, который писал код, — по той же причине, по которой тесты не пишут из кода: он сверит результат со своим представлением, а не с вашим. Второй агент с другим контекстом в роли проверяющего — рабочий приём, но окончательное «принято» остаётся за человеком.
Половина приёмки делается до генерации: пока результат не увиден, эталон ещё можно написать честно, а ревью качества идёт уже после ответа на вопрос «то ли сделано».
Глубже: приёмка не только кодарасширенное
Агент приносит не только код. Чаще — текст, план, описание, конфигурацию, миграцию. Приёмка нужна каждому виду, и эталон у каждого свой.
План или разбор. Эталон — ваша задача и ограничения. Что проверять: нет ли шагов, которых вы не просили; учтены ли названные ограничения; есть ли шаги, которые агент придумал, потому что «обычно так делают». Самая частая беда планов — пропущенная зависимость: шаг, который нельзя сделать раньше другого. Читается план с конца: «чтобы получилось это, что должно быть готово?»
Текст для людей (описание задачи, письмо, документация). Эталон — факты и адресат. Что проверять: все ли утверждения верны (модель уверенно пишет правдоподобные детали, которых вы не говорили), не появились ли обещания и сроки, которых вы не давали, нет ли выдуманных цифр. Отдельно — тон: модель по умолчанию пишет длиннее и вежливее, чем принято в вашей переписке.
Настройки и инфраструктура. Эталон — текущее состояние и намеренные отличия. Здесь опаснее всего, потому что ошибка проявляется не там, где сделана: изменённый лимит, открытый порт, выключенная проверка, «заодно обновлённая» версия образа. Что проверять построчно: каждое изменённое значение — вы его просили? Что отсутствует по сравнению с текущим? Правило: настройки не принимают чтением, их принимают применением в тестовом контуре.
Миграция базы. Эталон — совместимость и обратимость. Что проверять: совместима ли схема с работающей версией кода (иначе выкат сломает прод), нет ли удаления или переименования в том же шаге, что добавление, есть ли способ откатиться, сколько это будет выполняться на настоящем объёме данных. Это ровно тот случай, где «выглядит правильно» стоит дороже всего, и где приёмка обязана быть отдельным шагом, а не строкой в ревью.
Запрос к базе. Эталон — то, что должно получиться, и объём данных. Проверяют не текст запроса, а результат на настоящих данных и план выполнения: правдоподобный запрос легко возвращает почти правильный ответ — лишние строки из-за соединения, потерянные из-за внутреннего соединения вместо внешнего, неверная группировка.
Общее правило для всех видов: приёмка сверяет с эталоном, а не читает на правдоподобие. У кода эталон — контракт и тесты, у настроек — применение в тестовом контуре, у текста — факты, у миграции — совместимость. Там, где эталона нет, приёмки нет — и это верно не только для кода.
Коротко
- Опасность AI-кода не в том, что он падает, а в том, что он выглядит правильным, делая не то.
- Приёмка ≠ code review. Ревью — про качество кода (внутрь). Приёмка — про «то ли сделано» (сверка с контрактом).
- Эталон приёмки — контракт, написанный до генерации: критерии приёмки, границы, интерфейсы. Нет контракта — нет приёмки.
- AI врёт правдоподобно в крайних случаях, ветках отказа и скрытых требованиях — там, где ошибку не видно на успешном пути.
- Тесты пишутся из контракта, а не из кода агента — иначе они защищают баги, а не ловят их. Критерий превращается в тест буквально: «ЕСЛИ» — подготовка, «КОГДА» — один вызов, каждое «ТОГДА» — утверждение о наблюдаемом состоянии, включая отрицательные пункты.
- Автотест — на всё проверяемое; руками — границы, смысл и скрытые требования.
- Контракта нет — эталон пишут за двадцать минут до чтения дифа: ожидаемое поведение своими словами, три крайних случая, письменно в репозиторий; так контракт и появляется.
- Диф читают не подряд: сначала ветки отказа, потом удаления, стыки и список файлов, и только последним — успешный путь.
- Половина приёмки делается до генерации (критерии и красные тесты), сначала «то ли сделано», потом code review; приёмку не делает тот же агент, что писал код.
- Приёмка нужна не только коду: у плана эталон — задача, у текста — факты, у настроек — применение в тестовом контуре, у миграции — совместимость с работающей версией.
Что почитать дальше
- Ревью и тестирование кода с агентом — первый проход ревью, тесты от агента и проверка серией.
- Спецификация изменения — откуда берётся контракт, с которым сверяют результат.
- Галлюцинации — почему правдоподобное враньё это свойство модели, а не случайность.