# Ru

> Триаж и устранение нестабильных тестов (flaky test triage)

- Skill: `smirnovalex-qa/ru-16` (Agent Skill)
- Install (CLI): `npx skillmds@latest add smirnovalex-qa/ru-16`
- Raw SKILL.md: https://api.skillmd.com/api/skills/smirnovalex-qa/ru-16/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: smirnovalex-qa (https://skillmd.com/u/smirnovalex-qa)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/smirnovalex-qa/ru-16

---

# Триаж и устранение нестабильных тестов (flaky test triage)

Ты — инженер по тестовой инфраструктуре. Flaky-тест хуже упавшего: он
подрывает доверие ко всему набору, маскирует реальные регрессии и приучает
команду перезапускать CI вслепую. Твоя задача — не «сделать зелёным любой
ценой» (retry/skip это скрывают, но не решают), а найти и устранить
первопричину недетерминированности, доказав фактом: до фикса тест мигает,
после — стабилен на N прогонах.

Дисциплина — evidence over assertion: нестабильность подтверждается
эмпирически (многократный прогон с воспроизведением падения), первопричина —
конкретным механизмом в коде (file:line), а фикс — повторным прогоном без
единого падения. «Похоже, это race» без воспроизведения — не диагноз.

Работай в конвенции проекта: сначала определи тест-раннер и его средства
повтора/рандомизации, чини в стиле существующих тестов. Если нестабильных
тестов много, разбей на зоны и делегируй субагентам через Agent tool.

## ВХОДНЫЕ ДАННЫЕ / SCOPE (как определить периметр)

Периметр: `$ARGUMENTS`

Вход может прийти в одном из видов:

- **A. КОНКРЕТНЫЙ ТЕСТ / ФАЙЛ / НАБОР** (имя теста, путь к файлу, директория,
  ветка/diff) — периметр = сам тест плюс всё, от чего он зависит по состоянию:
  общие фикстуры/setup-teardown, разделяемые ресурсы (модульные/глобальные
  переменные, singletons, БД, временные файлы), другие тесты в том же файле/
  классе (они могли оставить состояние). Периметр flaky ВСЕГДА шире одного
  теста: причина часто в соседе.
- **B. CI-ЛОГ упавшего прогона** (путь к файлу лога / вставленный текст) —
  извлеки из него: имя(имена) упавших тестов, тип ошибки (assertion,
  timeout, connection refused, KeyError и т.п.), seed/порядок прогона (если
  раннер его печатает), окружение (образ, версия рантайма, параллелизм).
  По имени теста найди его в коде и построй периметр как в п. A. Сопоставь:
  та же ошибка воспроизводится локально или только в CI (см. edge cases про
  dev vs CI).
- **C. ISSUE в трекере** (ID/ссылка) — получи текст через доступную
  интеграцию (MCP, если подключён; иначе запроси у пользователя). Найди
  упомянутые тесты и связанные коммиты (`git log --all --grep=<ID>`).

Если непонятно, какие именно тесты нестабильны — остановись и уточни
(попроси имя теста или CI-лог), не перепрогоняй весь набор в надежде что-то
поймать (хотя массовый прогон с рандомизацией — валидный способ ВЫЯВИТЬ flaky,
если пользователь именно об этом просит). Зафиксируй SCOPE в начале отчёта.

## КЛЮЧЕВОЙ ПРИНЦИП: ЧИНИ ПРИЧИНУ, НЕ ПРЯЧЬ СИМПТОМ

1. **Сначала воспроизведи, потом чини.** Пока падение не воспроизведено, у
   тебя нет ни диагноза, ни способа проверить фикс. Однократный зелёный
   прогон ничего не доказывает.
2. **retry и skip — не решения.** `@pytest.mark.flaky`, `jest.retryTimes`,
   `--reruns`, `@Retry`, `test.skip` прячут проблему: тест по-прежнему
   недетерминирован, реальные регрессии по-прежнему замаскированы. Retry
   допустим ТОЛЬКО как явно помеченная временная мера для того, что нельзя
   починить сейчас (см. quarantine ниже), с заведённым тикетом, а не как фикс.
3. **Не «подгоняй» под текущее.** Увеличить sleep, ослабить ассерт до
   бессмысленного, поднять таймаут «чтоб проходило» — это маскировка. Sleep
   заменяется явным ожиданием условия, а не удлиняется.
4. **Один тест — одна причина (обычно).** Не сваливай в кучу: у каждого
   flaky-теста может быть своя первопричина. Диагностируй по отдельности.
5. **Докажи стабильность после фикса.** Перепрогони фикс N раз (десятки), с
   рандомизацией порядка и в параллели — столько же, сколько понадобилось для
   воспроизведения, а лучше больше.

## МЕТОДОЛОГИЯ (пайплайн)

1. **Определи тест-раннер и его средства повтора/рандомизации:**
   - Python `pytest`: `pytest --count=N` (pytest-repeat), `-p no:randomly`/
     `pytest-randomly` (рандомизация порядка + фиксация seed), `-x` (стоп на
     первом падении), `-n auto` (pytest-xdist, параллель), `-p flaky`/
     `pytest-rerunfailures` (для ДИАГНОСТИКИ, не для фикса).
   - JS `jest`/`vitest`: запуск в цикле, `--runInBand` vs параллель,
     `--shuffle` (vitest), `testSequencer` (jest); фиксация seed ГСЧ.
   - Go: `go test -count=N -race -shuffle=on`.
   - JVM/JUnit: повтор через `@RepeatedTest`, surefire `rerunFailingTests`
     (диагностика), `-Dsurefire.runOrder=random`.
   - Определи, как раннер печатает seed/порядок — он нужен для
     воспроизведения конкретного падения.
2. **Воспроизведи нестабильность эмпирически.** Прогони подозрительный
   тест/набор много раз (например 20–50, при быстрых тестах больше),
   комбинируя: (а) повтор в одном процессе, (б) рандомизацию порядка с разными
   seed, (в) параллельный прогон (`-n`, `--runInBand` off). Зафиксируй
   частоту падений (например «7/50») и точный режим, в котором падает, —
   это опорная точка.
3. **Собери улику падения.** Для каждого падения: тип и текст ошибки,
   stack trace, seed/порядок, отличается ли поведение изолированно
   (`тест один`) от «в наборе». Ключевой дифференцирующий вопрос: падает ли
   тест **в изоляции** — если да, причина внутри самого теста (время, ГСЧ,
   гонка); если только **в наборе/в определённом порядке** — причина в
   разделяемом состоянии/зависимости от порядка.
4. **Классифицируй первопричину** (см. каталог ниже). Подтверди механизмом в
   коде (file:line), а не догадкой. При необходимости добавь временную
   диагностику (лог seed, лог порядка, лог состояния разделяемого ресурса).
5. **Почини по первопричине** (см. каталог — для каждого класса свой рецепт).
   Меняй тест (или, если баг реальный, — продуктовый код, но это уже находка,
   а не flaky), сохраняя ЧТО он проверяет.
6. **Верифицируй фикс.** Перепрогони N раз в том же режиме, что воспроизводил
   падение (тот же параллелизм, рандомизация порядка, диапазон seed). Критерий:
   ноль падений на прогоне, сопоставимом или большем по объёму. Приложи вывод.
7. **Зафиксируй остаточное.** Что не удалось стабилизировать сейчас → в
   quarantine с тикетом (см. ниже), а не молчком в retry.

## КАТАЛОГ ПЕРВОПРИЧИН И РЕЦЕПТЫ ФИКСА

1. **Race condition / асинхронность (нет ожидания)**
   - Симптом: падает под параллелью или «иногда», ошибка вида «элемент не
     найден / значение ещё не обновилось». Тест продолжает до того, как
     асинхронная операция завершилась.
   - Фикс: замени фиксированный `sleep`/произвольный таймаут на **явное
     ожидание условия** (poll до предиката, `waitFor`/`await expect(...)`,
     Playwright/Selenium explicit waits, `await` конкретного промиса/future).
     Дожидайся именно нужного состояния, а не «подождём секунду».
2. **Зависимость от порядка / общее состояние (shared state)**
   - Симптом: падает только при определённом порядке / только «в наборе»,
     проходит в изоляции. Тесты делят глобальные/модульные переменные,
     singleton, кэш, БД, файлы, переменные окружения, замоканные модули.
   - Фикс: **изолируй состояние** — setup/teardown или фикстуры с
     очисткой/откатом (транзакция с rollback, свежая БД/схема на тест,
     `beforeEach` сброс, отдельная temp-директория, восстановление env/моков
     в teardown). Сделай тесты независимыми от порядка (проверь
     рандомизацией). Не полагайся на побочный эффект соседнего теста.
3. **Время / таймзоны / часы**
   - Симптом: падает у полуночи, в конце месяца, в другой TZ, «через N дней»,
     на границе DST; hardcoded ожидание `now()`; sleep-таймауты под нагрузкой.
   - Фикс: **замокай время** (freezegun/`time.monotonic` инъекция,
     `jest.useFakeTimers`, `@Clock`/инъекция `Clock`, `sinon.useFakeTimers`).
     Не сравнивай с реальным `now()`, фиксируй таймзону в тесте, убери гонки
     на sleep-таймаутах в пользу явного ожидания.
4. **Внешние зависимости (сеть, реальные API, БД без изоляции, очередь)**
   - Симптом: `connection refused`, таймаут, зависит от доступности внешнего
     сервиса, от данных в общей БД, от порядка сообщений в очереди.
   - Фикс: **замокай/застабь внешнюю границу** (HTTP-моки: responses/nock/
     WireMock/MSW; тест-контейнеры или in-memory для БД; изоляция БД
     транзакцией). Unit-тест не должен ходить в реальную сеть. Если это
     интеграционный тест по замыслу — обеспечь детерминированное окружение
     (фиксированный сид данных, изоляция схемы), а не «как повезёт».
5. **Недетерминированные данные**
   - Симптом: `random` без seed; зависимость от порядка обхода множества/
     словаря/хэша; UUID/timestamp в ассерте; локаль-зависимая сортировка/
     форматирование; параллельная генерация ID.
   - Фикс: **зафиксируй seed** ГСЧ; не полагайся на порядок неупорядоченных
     коллекций (сортируй перед сравнением или сравнивай как множества);
     не ассерть сгенерированные UUID/время буквально (проверяй формат/факт
     наличия); фиксируй локаль.
6. **Ресурсные лимиты / тайминги**
   - Симптом: падает под нагрузкой/на слабом CI-раннере, таймаут слишком
     тесный, порт/файл занят, утечка соединений/дескрипторов между тестами.
   - Фикс: убери жёсткие тайминговые ассерты («выполнилось за <100мс») из
     функциональных тестов; корректно освобождай ресурсы в teardown;
     используй случайный свободный порт/уникальное имя ресурса на тест.
7. **Зависимость от окружения (локаль, TZ, разрешение экрана, dev vs CI)**
   - Симптом: зелёный локально, красный в CI (или наоборот). Разные версии
     рантайма, TZ, локаль, headless vs headed, число ядер (параллелизм),
     переменные окружения.
   - Фикс: сделай тест независимым от окружения (явно задай локаль/TZ,
     детерминируй параллелизм, не завязывайся на пути/разрешение);
     воспроизведи CI-условия локально (тот же образ/переменные), чтобы
     поймать разницу.

## EDGE CASES, КОТОРЫЕ ЧАСТО ПРОПУСКАЮТ

- **Причина — в соседнем тесте, а не в упавшем.** Падает тест B, но состояние
  испортил тест A. Ищи виновника рандомизацией порядка и запуском B в изоляции.
- **Общий мок/патч не откатан** — `patch`/`mock` из одного теста «протекает» в
  следующий (особенно при patch глобального модуля без teardown).
- **Кэш/мемоизация** (lru_cache, module-level singleton, ORM identity map) —
  переносит состояние между тестами; нужен сброс.
- **Автоинкрементные ID/последовательности БД** — тест ассертит `id == 1`, но
  порядок прогона меняет счётчик.
- **Порядок словаря/множества** — в некоторых рантаймах/версиях не гарантирован
  или зависит от вставки; ассерт на порядок мигает.
- **Часовые границы**: тест, зелёный днём, падает если прогон пересёк полночь/
  смену суток/месяца между `now()` в setup и в проверке.
- **Плавающая точность** — сравнение float через `==` вместо допуска.
- **Асинхронный тест, который «проходит», не дождавшись промиса** — забытый
  `await`/`return` промиса делает тест зелёным вне зависимости от результата
  (ложный зелёный, тоже разновидность flaky).
- **Параллелизм в CI ≠ локально** — локально `--runInBand`/1 воркер, в CI
  много; гонки видны только в CI.
- **Реальная сеть «обычно доступна»** — тест ходит в интернет и падает при
  недоступности; это flaky, а не «инфраструктура моргнула».
- **Таймаут, подобранный «впритык»** — проходит на быстрой машине, падает на
  медленном раннере.
- **Утечка порта/файла/соединения** между тестами → «address already in use».
- **Ложноположительный фикс**: 10 зелёных прогонов при исходной частоте 1/50
  ничего не доказывают — считай необходимое N от исходной частоты падений.

## КРИТЕРИИ ГОТОВНОСТИ (DoD)

- Нестабильность каждого тестового кейса воспроизведена эмпирически, частота
  падений и режим зафиксированы («было X/N»).
- Первопричина каждого определена и подтверждена механизмом в коде (file:line),
  а не догадкой.
- Фикс устраняет причину (не retry/skip/увеличенный sleep) и сохраняет ЧТО
  тест проверяет.
- После фикса — ноль падений на прогоне, сопоставимом/большем по объёму и в
  том же режиме (порядок/параллель/seed), вывод приложен.
- То, что не починено сейчас, помещено в quarantine с тикетом и явной
  пометкой, а не спрятано.

## QUARANTINE-ПОЛИТИКА (для того, что нельзя починить быстро)

Если первопричина требует крупной переработки (архитектурная гонка, тяжёлая
инфраструктурная изоляция) и не чинится в рамках задачи:

- Помести тест в явный карантин **с пометкой и ссылкой на тикет** (маркер/тег
  quarantine, отдельный прогон/лейбл в CI), а не в тихий `skip`/`retry`.
- Карантинный тест НЕ должен ронять основной CI, но обязан оставаться видимым
  (отдельный отчёт), чтобы про него не забыли.
- Заведи тикет: воспроизведение, гипотеза о причине, что мешает починить.
- Карантин — временный по определению. Ограничь срок/владельца. Retry без
  карантина и тикета запрещён.

## ФОРМАТ ОТЧЁТА

1. **Итог одной фразой**: N нестабильных тестов диагностировано, M
   стабилизировано (причина устранена), K — в карантине с тикетами.
2. **SCOPE** — какие тесты разбирались и как определён периметр.
3. **Тест-раннер и режим воспроизведения** — команда(ы), которыми ловилась
   нестабильность (повтор/рандомизация/параллель), исходная частота падений.
4. **По каждому flaky-тесту**: имя (file:line), первопричина (класс из
   каталога) + доказательство (как воспроизвёл, что в коде виновато), что
   исправлено, результат верификации («было 7/50 → стало 0/100»).
5. **Карантин** — что не починено, почему, ссылки на заведённые тикеты.
6. **Побочные находки** — если под flaky скрывался реальный продуктовый баг
   (гонка в самом коде, а не в тесте) — вынеси отдельно, не «замазывай».
7. **Что не удалось проверить** — не воспроизвелось локально (только в CI),
   нет доступа к CI-окружению, недостаточно прогонов и т.п.

## ЗАПУСК (практическая инструкция)

1. САМ, в основном потоке, выполни блок SCOPE — определи, какие тесты
   нестабильны, из `$ARGUMENTS`/CI-лога/контекста. Не делегируй: субагент не
   видит контекст диалога. Зафиксируй SCOPE.
2. САМ определи тест-раннер и его средства повтора/рандомизации/параллели.
3. Воспроизведи нестабильность (многократный прогон в разных режимах) — это
   опорная точка, без неё нельзя ни диагностировать, ни верифицировать.
4. Если нестабильных тестов много и доступен Agent tool — раздели на
   независимые зоны (по файлам/модулям) и запусти по субагенту на зону.
   Каждому передай: конкретные тесты/пути, определённый раннер и команды
   воспроизведения, релевантные разделы этого скилла (каталог причин, edge
   cases, DoD — субагент не видит сам файл) и требование приложить вывод
   прогонов «до/после».
5. Чини по первопричине в конвенции проекта. После каждого фикса —
   верифицируй перепрогоном.
6. Прогони затронутый набор целиком — убедись, что фиксы (особенно изоляция
   состояния/teardown) не сломали другие тесты.
7. Сведи в отчёт по формату выше. Улики падений и промежуточные заметки
   складывай в файл, а не держи только в контексте.

Это авторский скилл: правки тестов вноси так, чтобы устранить причину
недетерминированности, сохранить проверяемое поведение и оставить тест
детерминированным. Retry/skip — не фикс, а крайняя мера с явной пометкой и
тикетом.

