Перейти к содержимому

Для ИИ-агентов ​

Эта страница помогает вашему агенту (Claude Code, Codex, Cursor, Copilot и другим) писать правильные спеки с этой библиотекой. Выполните в проекте одну команду:

bash
npx vitest-auto-spy init

Она добавляет короткий указатель в файлы инструкций, которые читают ваши агенты (AGENTS.md, CLAUDE.md, GEMINI.md и несколько файлов для отдельных инструментов; полный список). Указатель отправляет агента в node_modules/vitest-auto-spy/AGENTS.md — справочник, который входит в пакет и совпадает с установленной версией. В Claude Code вместо этого можно поставить плагин; см. Плагин для Claude Code.

Пакет содержит сжатую версию своей документации для агентов: дерево решений, смысл опций, таблицу «ошибка → исправление» и частые ошибки.

Одна команда ​

bash
npx vitest-auto-spy init

Она записывает указатель ниже в файлы инструкций этого репозитория и подставляет детали вашего проекта:

  • какая точка входа подходит вашему раннеру;
  • какой адаптер подходит вашему фреймворку;
  • настоящий путь до setup-файла, где нужен import 'vitest-auto-spy/rxjs' (если rxjs не установлен, этой строки не будет).

Всё записанное лежит между маркерами и перегенерируется при следующем запуске. Обновление — дифф в один блок, а init --uninstall возвращает файл как было.

КомандаЧто делает
npx vitest-auto-spy initзаписать или обновить блок
npx vitest-auto-spy init --checkдля CI: упасть, если блок отличается от того, что пишет эта версия
npx vitest-auto-spy init --uninstallудалить блок

Что она пишет:

ФайлДля когоКогда
AGENTS.mdCodex, Cursor, Copilot и большинство другихвсегда (создаётся, если нет)
CLAUDE.mdClaude Codeвсегда (создаётся, если нет)
GEMINI.mdGemini CLIвсегда (создаётся, если нет)
.claude/skills/vitest-auto-spy/SKILL.mdзаглушка скилла для Claude Codeвсегда
.cursor/rules/vitest-auto-spy.mdcCursor, только для файлов спектолько если есть папка .cursor/
.github/instructions/vitest-auto-spy.instructions.mdGitHub Copilot, только для файлов спектолько если есть папка .github/
.windsurf/rules/…, .devin/rules/…, .clinerules/…, .roo/rules/…Windsurf, Devin, Cline, Rooтолько если есть эта папка
.rules, .cursorrules, .windsurfrulesстарые форматы правилдописываются, только если файл уже есть

Три корневых файла получают один и тот же блок между маркерами. Файл правила Cursor пишется, только если папка .cursor/ уже есть, так что при необходимости создайте её заранее. После обновления пакета запустите init снова: init --check в CI падает, пока блок не совпадёт с установленной версией.

Полностью команда описана на странице CLI. Дальше на этой странице — что именно она пишет и как сделать это руками.

Пять точек входа ​

Здесь «точка входа» — это вид документации, а не путь импорта. Одна и та же документация доходит до агента в пяти видах:

ЧтоГдеДля чего лучше всего
llms.txtкорень сайтакраулеру, выбирающему единственную нужную страницу
llms-full.txtкорень сайтапрочитать всю документацию одним запросом
AGENTS.mdnode_modules/vitest-auto-spy/AGENTS.mdлюбому агенту, без сети — файл едет в тарболе
Паттерны спексайт документацииформы, к которым пришёл настоящий набор тестов, с частотами
Скилл для Claude Codeплагин или .claude/skills/, который пишет initClaude Code — грузится, только когда спека упоминает библиотеку

llms.txt — соглашение: карта из одних ссылок в корне сайта, чтобы агент скачивал одну страницу, а не разбирал десять. Оба файла llms генерируются из сайдбара этого сайта и проверяются в CI, поэтому не расходятся с ним.

Что доезжает до агента без всякой настройки ​

Две вещи доходят до агента сразу после npm install, без единой строки в вашем репозитории:

  • Ошибки, которые называют, чем чинить. Каждая ошибка и каждое предупреждение заканчиваются строкой Docs: со ссылкой на страницу с разбором. Стек-трейс агент читает гораздо чаще, чем README, поэтому этот канал работает лучше всего. См. примеры ниже.
  • TSDoc в dist/*.d.ts. Каждый экспорт описан там, куда и так смотрят редактор и «перейти к определению». Если документ расходится с кодом, правы типы.

Всему остальному нужна одна записанная где-то строка. Вот почему.

Скилл внутри npm-пакета сам собой не находится

skills/vitest-auto-spy/SKILL.md лежит внутри npm-пакета, но Claude Code его там не находит. Он ищет скиллы только в ~/.claude/skills/, в .claude/skills/ проекта и в skills/ установленного плагина. С папками правил других инструментов то же самое: зависимость не может попасть в инструкции агента без настройки. Поэтому скилл приходит одним из двух путей:

  • через плагин;
  • через заглушку, которую npx vitest-auto-spy init пишет в .claude/skills/vitest-auto-spy/SKILL.md. Она копирует frontmatter скилла из пакета (он решает, когда скилл загрузится), а её тело только указывает на пакет, поэтому устареть не может.

AGENTS.md тоже входит в пакет, поэтому node_modules/vitest-auto-spy/AGENTS.md лежит на диске, без сети, в установленной версии. Но агенту всё равно нужно сказать, чтобы он его прочитал; эту строку и пишет init.

Справочник описывает каждый экспорт, опцию и ошибку, поэтому он разбит на два уровня:

  • AGENTS.md — карта примерно на 30 kB: меньше бюджета Codex в 32 KB, и её можно прочитать целиком. В ней точки входа, основной рецепт, Spy<T> против T, сброс и fakeAsync, а для каждого остального раздела — заглушка с подсказкой, когда его открывать.
  • node_modules/vitest-auto-spy/agent-docs/ — остальное, по файлу на раздел с теми же номерами: фабрики, конфигурация, хелперы по типу возврата, setup-файл, Angular, ESLint-плагин, Error → fix (таблица, в которой ищут по тексту ошибки), чек-лист перед отчётом об успехе и другое.

Скилл из пакета и заглушка от init сначала отправляют агента к карте.

Наведите агента один раз ​

Добавьте это в файл инструкций, который читает ваш агент:

  • корневой AGENTS.md — для Codex, Cursor, Copilot и большинства других инструментов;
  • CLAUDE.md — для Claude Code и для GLM или Kimi внутри него;
  • GEMINI.md — для Gemini CLI.
md
When writing or fixing tests that use `vitest-auto-spy`, first read
`node_modules/vitest-auto-spy/AGENTS.md` whole — a short map — then only the topic files in
`agent-docs/` it names for the task.

Этот файл лежит на диске в каждом проекте, где установлен пакет. Агенту не нужна сеть, и он читает ту версию, которая у вас действительно стоит.

Какой файл читает ваш агент ​

Фрагмент одинаков для всех инструментов, меняется только имя файла. Три корневых файла покрывают все инструменты. init пишет свой блок во все три. Руками держите текст в AGENTS.md, а CLAUDE.md и GEMINI.md пусть указывают на него.

  • Большинство инструментов читают AGENTS.md: Codex, Cursor, Copilot, Cline, Windsurf/Cascade, Zed, OpenCode, Qwen, Junie, Roo и Aider.
  • Claude Code читает CLAUDE.md, а не AGENTS.md.
  • Gemini CLI читает GEMINI.md, если context.fileName не называет другой файл.

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

АгентКакой файл инструкций читаетЧитает ли AGENTS.md?
Claude CodeCLAUDE.md — проектный, .claude/CLAUDE.md и ~/.claude/CLAUDE.md, все склеиваютсяНет. Свяжите строкой импорта @AGENTS.md или симлинком
OpenAI Codex — CLI codex, расширение для IDE, облачный CodexAGENTS.md, по одному на директорию от корня git вниз до cwd (ниже)нативно
GLM (тарифный план z.ai), Kimi K3то, что читает их клиент — внутри Claude Code это CLAUDE.md (ниже)через клиент
Cursorкорневой AGENTS.md; .cursor/rules/*.mdc для правил с glob-областьюнативно — и корневой CLAUDE.md он применяет тем же всегда-включённым способом
GitHub Copilotкорневой AGENTS.md; .github/copilot-instructions.mdнативно, включая coding agent
OpenCodeAGENTS.md, затем CLAUDE.md, по директориям вверхнативно
Clineкорневой AGENTS.md; директория .clinerules/нативно
Windsurf / Cascadeкорневой AGENTS.md; .windsurf/rules/*.md (.devin/rules/*.md, если она есть)да
Zedпобеждает первое совпадение, без слияния: .rules → .cursorrules → .windsurfrules → .clinerules → .github/copilot-instructions.md → AGENT.md → AGENTS.md → CLAUDE.md → GEMINI.mdда — но только если ничего более раннего в этом списке нет
Gemini CLIGEMINI.md (ниже)по умолчанию нет
Qwen CodeQWEN.mdнативный фолбэк
Roo Codeкорневой AGENTS.md; .roo/rules/да
Junieкорневой AGENTS.md — учтите, что .junie/AGENTS.md заменяет его целикомда
Aiderничего неявно — перечислите файл: read: [AGENTS.md] в .aider.conf.ymlпо запросу
Jules, Factory, goose, Amp, Warp, Devin, Kilo, Augment, VS Codeкорневой AGENTS.mdнативно

Никогда не заводите legacy-файл правил только ради этого фрагмента

Zed читает .rules → .cursorrules → .windsurfrules → .clinerules → …, и побеждает первое совпадение, без слияния. Новый legacy-файл молча перекроет AGENTS.md, на который опирается весь проект. Дописывайте в такой файл, только если он уже есть.

Как поставить это своему агенту ​

npx vitest-auto-spy init пишет всё это сам. Руками хватит трёх файлов в корне репозитория, чтобы покрыть все инструменты из таблицы выше:

bash
# 1 — AGENTS.md: источник. Codex, Cursor, Copilot, Cline, Windsurf, Zed, OpenCode, Qwen, Roo, Junie, Aider…
cat >> AGENTS.md <<'MD'

## Tests that use `vitest-auto-spy`

When writing or fixing tests that use `vitest-auto-spy`, first read
`node_modules/vitest-auto-spy/AGENTS.md` whole — a short map — then only the topic files in
`agent-docs/` it names for the task.
MD

# 2 — CLAUDE.md: Claude Code и GLM / Kimi внутри него. Одна строка, вторую копию поддерживать не надо
printf '\n@AGENTS.md\n' >> CLAUDE.md

# 3 — GEMINI.md: Gemini CLI, который по умолчанию не читает AGENTS.md
printf '\nRead `AGENTS.md` in this directory — it is the single source.\n' >> GEMINI.md

@AGENTS.md — синтаксис импорта Claude Code, так что инструкции живут в одном файле. Симлинк (ln -s AGENTS.md CLAUDE.md) тоже подойдёт, если второй файл не нужен. В Gemini CLI импорта нет: GEMINI.md содержит фразу-указатель выше, или вы правите .gemini/settings.json ниже. В любом случае держите текст в одном месте: две копии справочника по API расходятся за один релиз.

Дальше — по инструментам. Всё в колонке «Установка» необязательно и добавляется поверх трёх корневых файлов:

АгентУстановка
Claude Code/plugin marketplace add ASDAlexey/vitest-auto-spy, затем /plugin install vitest-auto-spy@vitest-auto-spy — скилл, файлы проекта не трогаются
OpenAI Codexбольше ничего; по желанию ~/.codex/config.toml отсюда
GLM (z.ai), Kimi K3как у Claude Code — тот же клиент, та же команда плагина
Cursor.cursor/rules/vitest-auto-spy.mdc, чтобы грузить его только для файлов спек (см. ниже)
GitHub Copilot.github/instructions/vitest-auto-spy.instructions.md (см. ниже)
Cline.clinerules/vitest-auto-spy.md — те же три строки плюс paths: ["**/*.spec.ts", "**/*.test.ts"]
Windsurf / Cascade.windsurf/rules/vitest-auto-spy.md с trigger: glob (см. ниже)
Roo Code.roo/rules/vitest-auto-spy.md — всегда включён, так что ограничьтесь указателем в три строки
Gemini CLIGEMINI.md или правка .gemini/settings.json отсюда
Aider.aider.conf.yml: read: [AGENTS.md]
Zed, OpenCode, Qwen Code, Junie, Jules…ничего — корневой AGENTS.md и есть вся установка

Три инструмента умеют подключать правило только для файлов спек. Тело у всех одно и то же, отличается только frontmatter:

.cursor/rules/vitest-auto-spy.mdc

md
---
description: How to write tests with vitest-auto-spy
globs: **/*.spec.ts, **/*.spec.tsx, **/*.test.ts, **/*.test.tsx
alwaysApply: false
---

Read `node_modules/vitest-auto-spy/AGENTS.md` before writing or fixing a spec that uses
`vitest-auto-spy` — the API, the configuration semantics and the common mistakes.

.github/instructions/vitest-auto-spy.instructions.md

md
---
applyTo: '**/*.spec.ts,**/*.spec.tsx,**/*.test.ts,**/*.test.tsx'
---

Read `node_modules/vitest-auto-spy/AGENTS.md` before writing or fixing a spec that uses
`vitest-auto-spy` — the API, the configuration semantics and the common mistakes.

.windsurf/rules/vitest-auto-spy.md (или .devin/rules/vitest-auto-spy.md, если есть эта папка)

md
---
trigger: glob
globs: **/*.spec.ts, **/*.spec.tsx, **/*.test.ts, **/*.test.tsx
---

Read `node_modules/vitest-auto-spy/AGENTS.md` before writing or fixing a spec that uses
`vitest-auto-spy` — the API, the configuration semantics and the common mistakes.

globs у Cursor — строка через запятую, а не YAML-массив. Файл правила Windsurf ограничен 12 000 символов. Поэтому правило указывает на справочник, а не копирует его.

OpenAI Codex ​

Codex (CLI codex, расширение для IDE и облачный Codex) читает AGENTS.md, так что корневого AGENTS.md достаточно. Дойдёт ли он до модели, решают две детали:

  • Codex читает по одному файлу на папку, от корня git до текущей папки, и склеивает их. AGENTS.override.md важнее AGENTS.md. Если в монорепозитории пакет использует другой раннер, положите блок и в AGENTS.md этого пакета. Только так можно сказать агенту «здесь bun test, а по соседству Vitest», а от этого зависит, какую точку входа он импортирует.
  • Вся цепочка ограничена параметром project_doc_max_bytes, по умолчанию 32 768 байт. Всё, что сверх лимита, обрезается с предупреждением. Если ваш AGENTS.md уже длинный, поставьте указатель ближе к началу.

Если инструкции репозитория лежат в CLAUDE.md, скажите Codex читать его, когда AGENTS.md нет. Это глобальный конфиг на вашей машине, коммитить нечего:

toml
# ~/.codex/config.toml
project_doc_fallback_filenames = ["CLAUDE.md"]   # по директориям, где нет AGENTS.md
project_doc_max_bytes = 65536                    # поднять бюджет в 32 КБ для цепочки монорепозитория

Облачный Codex читает тот же корневой AGENTS.md и по умолчанию не имеет доступа в интернет. Поэтому справочник входит в пакет, а не живёт только на этом сайте: после установки зависимостей node_modules/vitest-auto-spy/AGENTS.md уже на диске.

GLM (z.ai), Kimi K3 и другие Claude-совместимые модели ​

GLM — это модель, а не агент. Какие файлы она читает, решает клиент, в котором вы её запускаете.

Тариф z.ai для программирования запускает GLM внутри Claude Code: ANTHROPIC_BASE_URL (вместе с ANTHROPIC_AUTH_TOKEN) указывает на Anthropic-совместимый адрес z.ai. Поиск файлов от этого не меняется: CLAUDE.md, .claude/skills/ и плагин работают так же, как на Claude, потому что клиент тот же. То же верно для Kimi K3 внутри Claude Code. Там скилл и плагин лучше вставленного фрагмента: они загружаются, только когда спека упоминает библиотеку.

В другом клиенте решает он. OpenCode, Cline, Roo Code и Kilo Code читают корневой AGENTS.md. kimi-cli от Moonshot читает свою цепочку AGENTS.md, включая .kimi/AGENTS.md.

Gemini CLI ​

Gemini CLI читает GEMINI.md и не читает AGENTS.md по умолчанию. Либо вставьте фрагмент в GEMINI.md, либо один раз назовите оба файла:

json
// .gemini/settings.json
{ "context": { "fileName": ["GEMINI.md", "AGENTS.md"] } }

Qwen Code сделан на основе Gemini CLI и принимает ту же настройку context.fileName, но AGENTS.md читает и без неё.

Плагин для Claude Code ​

Репозиторий также служит маркетплейсом плагинов Claude Code. Этому пути не нужны файлы в вашем проекте. Это один из двух способов, которыми Claude Code находит скилл; второй — заглушка в .claude/skills/ от init. Копию внутри пакета он не находит.

/plugin marketplace add ASDAlexey/vitest-auto-spy
/plugin install vitest-auto-spy@vitest-auto-spy

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

Наводите на подпуть, а не только на пакет ​

Каждый путь импорта подключает библиотеку к своему раннеру или фреймворку, а некоторые, например /rxjs и /zone, намеренно подключаются вручную. init вписывает в свой блок нужный для вашего проекта. Агент, который знает только vitest-auto-spy, напишет спеку, которая упадёт на первом же хелпере:

ПодпутьНужен для
vitest-auto-spy/rxjsnextWith, observablePropsToSpyOn, throwWith — импортируется один раз, в setup
vitest-auto-spy/bunлюбой спеки, которую гоняет bun test (/bun-angular — для тамошнего TestBed Angular)
vitest-auto-spy/nodeтесты на node --test, ESM или CJS
vitest-auto-spy/rstestлюбой спеки, которую гоняет Rstest, — npx rstest run
vitest-auto-spy/angularprovideAutoSpy, injectSpy, renderShallow, хелперов переопределения
vitest-auto-spy/setupsetupAutoSpy (со strayConsole и preset: 'strict'), хелперов часов, installPerTest, матчеров фокуса
vitest-auto-spy/zonefakeAsync / waitForAsync на Vitest — zone.js не попадает ни в одну другую точку входа

Ошибки, которые называют, чем чинить ​

Стек-трейс агент читает гораздо чаще, чем README. Поэтому каждая ошибка и каждое предупреждение этого пакета говорят, что пошло не так, называют исправление и заканчиваются ссылкой на разбор:

[vitest-auto-spy] Observable spies require rxjs, and 'vitest-auto-spy/rxjs' was not imported in this run. Add `import 'vitest-auto-spy/rxjs';` once to the setup file.
Docs: https://asdalexey.github.io/vitest-auto-spy/runtimes/rxjs

То же верно для:

  • отсутствующего mock-адаптера;
  • метода, которого нет в классе;
  • advanceTimers() без фейковых таймеров;
  • preload-файла bun-angular без DOM-пакета;
  • templateUrl, который не удаётся найти;
  • нарушения mustBeCalledWith;
  • повторной установки пакета.

Под setupAutoSpy({ strayConsole: 'throw' }) вывод в консоль, который не поймал ни один спай, роняет тест. Ошибка называет метод консоли, первые выведенные строки и код, который их вывел. Если после включения этой проверки покраснела спека, которую агент не трогал, смотрите туда в первую очередь.

Более дешёвый вывод прогона — --reporter=agent ​

В Vitest 4.1 есть репортер для агентов. Он печатает те же падения без списка прошедших тестов и повторяющихся баннеров, так что агенту дешевле читать вывод и держать его в контексте.

bash
npx vitest run --reporter=agent src/app/cart.component.spec.ts

Это часть Vitest, а не этого пакета, настраивать ничего не нужно. Добавьте ещё фильтр по пути: агенту редко нужен весь прогон, обычно только файл, который он только что правил.

В чём агенты чаще всего ошибаются ​

На это приходится подавляющее большинство сломанных спек, и всё перечисленное разобрано в AGENTS.md:

  1. let s: MyService = createSpyFromClass(MyService). Spy<T> — это mapped type, и приватные члены он отбрасывает. Объявляйте как Spy<T> или наводите мост через asInstance / asSpy — но никогда через as unknown as T.

  2. Попытка ограничить набор через methodsToSpyOn. Он дополняет найденные методы, как и в jest-auto-spies. Исчерпывающий белый список — это onlyMethodsToSpyOn, и он полностью пропускает обход прототипа.

  3. Вызов nextWith без import 'vitest-auto-spy/rxjs'. Observable-слой подключается вручную.

  4. Импорт vitest-auto-spy внутри файла под bun test. Каждая точка входа регистрирует при импорте свой mock-адаптер; берите vitest-auto-spy/bun.

  5. expect() внутри колбэка subscribe(). Молчащий поток делает из этого зелёный тест, который ничего не проверил, — берите expectEmission, где проверка и есть await.

  6. vi.fn().mockImplementation(() => instance) для того, что код зовёт через new. Идиома из Jest не переносится: Vitest пробрасывает new только в конструируемую реализацию, поэтому стрелка запишет вызов, пропустит тело и вернёт пустой объект — или бросит X is not a constructor уже изнутри продакшен-кода. Берите mockConstructor / stubConstructor.

  7. Экспортируемый const с vi.fn() внутри, общий на несколько файлов спек. При isolate: false модуль вычисляется один раз на воркер, то есть это один набор спаев на все файлы, которые его импортируют. Фикстура — это фабрика; файл спеки не экспортирует вообще ничего.

  8. it('x', (done) => …). Vitest передаёт TestContext, так что done() бросает внутри промиса, которого никто не ждёт, а тест проходит, выполнив едва ли не пустое тело. Ловит это правило линтера no-done-callback; чинится через await.

  9. await Promise.resolve(), чтобы дождаться динамического import() под фейковыми таймерами. Так время не двигается ни на такт, а setTimeout — фейковый; берите settleDynamicImport / flushEventLoop.

  10. Уверенность, что хуки из setup-файла доедут до каждого файла спек. Они принадлежат тому файлу, чья сборка импортировала модуль, и раннер, который держит этот модуль в кеше между файлами — в дикой природе это @angular/build:unit-test до 22.2.0 под --coverage, — отдаёт их первому файлу каждого воркера и больше никому. Никто об этом не сообщает; симптом — утёкший глобал или настоящие таймеры в спеке, которая сама по себе проходит. @angular/build 22.2.0 это исправляет; на более старом билдере гоняйте покрытие с --isolate или вызывайте setupAutoSpy() из чего-то, что вычисляется на каждый файл. В любом случае держите вызов на верхнем уровне самого setup-файла, а не в модуле, который он импортирует.

  11. vi.spyOn(console, 'error'), чтобы спека молчала. Без реализации он вызывает оригинал, так что строка всё равно печатается — а под strayConsole тест на ней падает. Ставьте тихие спаи через installConsoleSpies() из vitest-auto-spy/console в beforeEach и проверяйте consoleErrorSpy; голую форму ловит правило no-passthrough-console-spy.

  12. Советы, перенесённые из туториалов и шпаргалок времён Jest, — каждый пункт ниже неверен под Vitest, проверено на Vitest 5.0.0:

    • Фабрика vi.mock, читающая переменную, объявленную выше. vi.mock поднимается над импортами, поэтому фабрика выполняется раньше, чем появляется const, и файл падает с [vitest] There was an error when mocking a module…, причина — ReferenceError: Cannot access 'load' before initialization. Исключения Jest для имён, начинающихся с mock, здесь нет — mockLoad падает так же. Объявляйте то, что нужно фабрике, через const mocks = vi.hoisted(() => ({ load: vi.fn() })).
    • vi.requireActual / jest.requireActual, чтобы сохранить остальной модуль. Ни того ни другого нет; фабрика получает importOriginal: vi.mock('./api', async (importOriginal) => ({ ...(await importOriginal<typeof import('./api')>()), load: vi.fn() })). vi.importActual — асинхронная отдельная форма.
    • import { jest } from 'vitest'. vitest не экспортирует jest; пространство имён — vi.
    • import { userEvent } from '@testing-library/user-event'. Именованный экспорт есть только с 14.5.0; экспорт по умолчанию работает на любой 14.x. API 14.x асинхронный: const user = userEvent.setup(); await user.click(button).
    • vi.restoreAllMocks(), чтобы отменить vi.useFakeTimers(). Восстановление, сброс и очистка моков оставляют фейковые часы на месте; настоящие возвращает vi.useRealTimers() в afterEach.
    • globalThis.fetch = vi.fn(). Ни vi.restoreAllMocks(), ни vi.unstubAllGlobals() не достают до голого присваивания, так что фейк отвечает каждому следующему тесту файла. Используйте vi.stubGlobal('fetch', …) с unstubGlobals: true или mockValueProp(globalThis, 'fetch', …), который восстанавливает setupAutoSpy(); blockNetwork() — для спеки, которой нужно лишь не ходить в сеть, и stubResponse({ body }) — для самого Response. Голую форму ловит правило no-hand-assigned-global, а prefer-stub-response — тот { ok: true, json } as Response, который из неё возвращают.
    • await import('./thing'), чтобы дождаться модуля, который лениво грузит код под тестом. Оно дожидается модуля, а не продолжения обработчика, который его грузил, поэтому ассерт выполняется на такт раньше. Нужен settleDynamicImport(() => import('./thing')), который добавляет тот самый flushEventLoop(1); голую форму ловит prefer-settle-dynamic-import.
    • Тест, каждое утверждение которого выполнено тем, что поток промолчал. let, который пишет только колбэк subscribe, проверенный через toEqual([]) против собственного инициализатора или через toBeUndefined / not.toHaveBeenCalled, не отличает пустой результат от отсутствия результата. Скажите, что имеете в виду: expectNoEmission(source$) — про молчание, expect(await expectEmission(source$)) — про значение; написанную руками форму ловит правило no-vacuous-absence-assertion.
    • { id: '1', isOffline: false } as SomeType для фикстуры. Каст спрашивает, пересекаются ли два типа, а не является ли значение одним из них: проверка лишних свойств не выполняется, так что проходит и ключ, которого тип не объявляет, и обязательное поле, которого в фикстуре нет. Оба тайп-гейта молчат, а фикстура потом пинит ключ, которого в контракте нет. Нужен createMock<SomeType>({ … }) — он принимает DeepPartial<SomeType> и отвечает SomeType, — либо просто удалить каст там, где литерал и так стоит в типизированном слоте; сообщает prefer-create-mock.
    • (TestBed.inject(S).m as Mock).mockReturnValue(…). Mock без параметров — это Mock<any>, поэтому каст не добавляет методы спая, а убирает сигнатуру: toHaveBeenCalledWith перестаёт сравнивать аргументы. Член и так спай — читайте его как injectSpy(S).m; каст ловит no-mock-cast.
    • Reflect.get(component, 'privateField'), чтобы прочитать мимо модификатора. Ключ здесь — обычный строковый аргумент, и его не проверяет никто: ни компилятор, ни шаблонный гейт, ни строгий проход tsc. А Reflect.set вешает собственное свойство поверх прототипа, так что после переименования в продакшене спека пишет мёртвое свойство, а ассерты под ней проходят вечно. Ведите член через публичный API и проверяйте эффект — на компоненте это отрендеренный шаблон; mockValueProp — запись, которая регистрирует свой откат, если значение всё-таки нужно задать спаю. Ловит no-reflect-member-access, а window и globalThis оставляет в покое — ради них идиома и существует.
    • Тест, который сам вызывает заспаенный метод, а потом утверждает, что его вызвали.vi.spyOn(component.output, 'emit'), затем component.output.emit(payload), затем expect(spy).toHaveBeenCalledWith(payload) доказывает, что emit вызывает emit: удалите привязку в шаблоне, которую называет заголовок, — тест останется зелёным. Запускайте настоящий триггер; выдающий себя порядок ловит no-self-called-spy.
    • Сброс моков в хуке, который раннер и так делает. При включённых clearMocks, mockReset или restoreMocks Vitest сбрасывает их перед каждым тестом — до цепочки beforeEach и после предыдущего afterEach, — так что тот же вызов в хуке делает работу дважды, а читается как строка, на которой держится честность тестов. Её надо удалить; no-redundant-mock-reset сообщает про неё, когда ему сказали, какие флаги включены, и не трогает сброс внутри теста: там он разделяет две подготовки внутри одного теста.
    • expect(spy).toHaveBeenCalled() там, где тест как раз про аргументы. Голый матчер проходит на любых аргументах, поэтому тест с заголовком «…with the host element», который больше ничего не утверждает, зелёный и когда передали не тот узел. Назовите их — toHaveBeenCalledWith(…) или mustBeCalledWith(…) там, где настраивается спай; no-unasserted-argument сообщает про две формы, в которых сам файл говорит, что аргументы значимы.

Опубликовано под лицензией MIT.