Слогер Создать блог
Разработка

Как отличить флаки-тест от регрессии: четыре проверки

Красный билд, чужой на вид дифф и тест, который «сам по себе падает». Вместо спора — четыре проверки, которые дают ответ за несколько минут.

Красный билд. Изменение в коммите выглядит посторонним, и уже кто-то говорит: «Да это флаки, перезапусти». Может быть, он прав. А может — вы только что сломали прод. Четыре проверки ниже — это не панацея, а способ за десять минут заменить «а давай перезапустим» на цифры. И вот почему это важно.

Какой вопрос вы на самом деле задаёте

«Это флаки?» — плохой вопрос. Хороший звучит так: зависит ли частота падения теста от моего изменения? У флаки-теста есть своя частота падений — она свойство теста и окружения, и ваш коммит её не меняет. Регрессия — это когда частота падений сдвинулась из-за вашего коммита. В одном красном билде оба случая выглядят одинаково.

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

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

Проверка первая: прокрутить тест на текущем коммите

Запустите упавший тест в одиночку, на том же коммите, несколько раз. Не весь сьют — один тест в цикле, чтобы получить частоту, а не один результат.

pytest tests/test_router.py::test_router_emits_a_tool_call --count 20 -p no:randomly -q

npx vitest run src/router.test.ts -t "emits a tool call" --repeats 20

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

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

Два нюанса делают цикл честным. Отключите рандомизацию порядка тестов (флаг -p no:randomly в примере выше), иначе вы сэмплируете и тест, и порядок запуска. И гоняйте именно один тест, а не файл: соседний тест может оставить патченного клиента или заполненный кэш, и тогда вы измерите чужое влияние, а припишете его своему изменению.

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

Это проверка, которая отвечает на исходный вопрос, и именно её обычно пропускают: билд красный, лишняя работа не нужна. Но без неё нет сравнения.

git stash --include-untracked

git checkout <last-green-sha>

pytest tests/test_router.py::test_router_emits_a_tool_call --count 20 -q

git checkout -

git stash pop

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

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

Если захотите автоматизировать, именно этот сценарий повторяет git bisect run: скрипт, который запускает тест N раз и возвращает количество успешных прогонов, годится для обеих задач.

Проверка третья: свидетельства за пределами репозитория

Три источника закрывают добрую половину споров за минуту, и ничего запускать не нужно:

  • Статус провайдера и changelog. Окно инцидента, в которое попали ваши падения, — почти приговор. Ещё сильнее — деприкация модели или тихий точечный релиз. Незакреплённый алиас модели — самый частый источник «внезапной флаки».
  • Записанные метаданные ответов. Если вы логируете фактический id модели и system_fingerprint, изменение любого из них между зелёным и красным прогоном даёт ответ. OpenAI описывает system_fingerprint как идентификатор комбинации весов и конфигурации бэкенда, а сидированный сэмплинг — как «best-effort», а не гарантию. Смена fingerprint как раз и делает воспроизведение невозможным.
  • Двинулись ли другие тесты. Один тест падает — проблема теста. Одиннадцать тестов в разных фичах упали одновременно — проблема инфраструктуры. Группировать их надо по причине, а не по имени.

Ответить, ваша это поломка или провайдера, проще, когда метаданные записаны одинаково для всех провайдеров. Тогда «поменялась ли модель между зелёным и красным прогоном» — это запрос к логам, а не археология по дашбордам двух вендоров.

Таблица решений

Сложите результаты двух прогонов рядом. HEAD — коммит под проверкой, GOOD — последний известный зелёный. В каждой клетке одинаковое число повторов на обоих.

HEADGOODВывод
0/20 pass20/20 passРегрессия в вашем изменении. Не мёржить.
0/20 pass0/20 passСломано вне репозитория: провайдер, модель, зависимость. Закрепить модель и перепроверить.
mixed20/20 passРегрессия, которая проявляется не всегда. Частота падений — ваше доказательство.
mixedmixedСуществующий флаки. Сравните частоты; если HEAD заметно хуже — лечите как регрессию.
20/20 passлюбойНевоспроизводимо. Зафиксируйте, не карантиньте тест по одному падению и просмотрите дашборд через неделю.

Последняя строка требует дисциплины. Один невоспроизводимый прогон — недостаточное основание для карантина теста: так сьют теряет покрытие по одному тесту за раз. Зафиксируйте инцидент и пусть решение принимает накопленная история. Опубликованный допустимый уровень флаки существует ради того, чтобы решение было принято один раз, как политика, а не переспоривалось каждым, кто сегодня на он-кале.

По материалам: ai. Текст переработан редакцией Слогера.

← На главную

Рекламное место — Конец поста
Реклама · Слогер

Комментарии (0)

Войдите, чтобы комментировать.

Пока нет комментариев. Будьте первым.