# Регресс-тесты навыков Snapshot-тестирование скриптов навыков: навык получает вход → генерирует файлы → результат сравнивается с эталоном. Быстрые, файловые, без зависимости от платформы 1С. ## Запуск ```bash node tests/skills/runner.mjs # все кейсы node tests/skills/runner.mjs cases/meta-compile # один навык node tests/skills/runner.mjs cases/meta-compile/catalog-basic # один кейс node tests/skills/runner.mjs --verbose # подробный вывод (дерево) node tests/skills/runner.mjs --update-snapshots # обновить эталоны node tests/skills/runner.mjs --runtime python # запуск на PY-версиях node tests/skills/runner.mjs --json report.json # JSON-отчёт node tests/skills/runner.mjs --concurrency 4 # ограничить параллельность node tests/skills/runner.mjs --with-validation # + платформенная валидация node tests/skills/runner.mjs --help # полный список опций ``` Exit code: 0 = все прошли, 1 = есть падения. ### Платформенная верификация снапшотов ```bash node tests/skills/verify-snapshots.mjs --skill form-compile # один навык node tests/skills/verify-snapshots.mjs --case table # один кейс node tests/skills/verify-snapshots.mjs --help # полный список опций ``` Перепрогоняет навык из DSL кейса и грузит результат в 1С — отлавливает случаи, когда снапшоты обновили, но платформа уже не принимает выход. ## Интеграционные тесты Помимо snapshot-кейсов есть многошаговые сценарии в `integration/<имя>.test.mjs` — цепочка навыков (init → compile → build → validate…), проверяющая что навыки работают вместе. Запуск: ```bash node tests/skills/runner.mjs integration # все интеграционные node tests/skills/runner.mjs integration/platform-partial # один сценарий ``` Тест-модуль экспортирует `name`, `setup`, `steps` и опционально: | Экспорт | Описание | |---|---| | `requiresPlatform` | `true` — нужен 1С (резолвится из `.v8-project.json`). Без платформы тест `○ skipped` | | `engines` | Массив движков для **матрицы**: по умолчанию `['1cv8']`. `['1cv8','ibcmd']` — те же шаги прогоняются на обоих движках | ### Движковая матрица (1cv8 / ibcmd) Навыки `db-*`/`epf-*` выбирают движок по имени exe в `-V8Path` (опт-ин: `ibcmd.exe` → ibcmd, иначе DESIGNER). Тест с `engines: ['1cv8','ibcmd']` прогоняется по разу на каждый движок: на ibcmd-проходе плейсхолдер `{v8path}` подставляется в `ibcmd.exe`, на 1cv8 — в каталог `bin` (авто-резолв `1cv8.exe`). Результаты помечаются суффиксом id: `… [1cv8]` / `… [ibcmd]`. ibcmd-проход автоматически `○ skipped`, если рядом с `1cv8.exe` нет `ibcmd.exe`. Шаги тестов при этом **не меняются** — добавляется одна строка `export const engines`. Так контракт «операция держится на обоих движках» кодируется без дублирования сценария. ### Типы шагов Шаг — это запуск навыка (`script` + `args` + опц. `input`/`validate`) либо один из вспомогательных: | Поле шага | Действие | |---|---| | `script` + `args` | Запустить навык. `args` поддерживают плейсхолдеры `{workDir}`, `{inputFile}`, `{v8path}` и др. | | `input` | JSON, передаётся навыку через temp-файл (`{inputFile}`) | | `writeFile` + `content` | Записать файл (путь — плейсхолдеры) | | `editFile` + `replace` + `with` | Подстановочная замена в файле (напр. вставить маркер). Падает, если паттерн не найден | | `assertContains` + `expect` | Упасть, если файл не содержит подстроку (проверка round-trip) | | `validate` | Доп. валидация навыком после шага (только с `--with-validation`) | ## Что делать при падении 1. Смотри **case id** в выводе — это путь к файлу кейса (можно перезапустить: `node runner.mjs `) 2. Открой `.json` кейса — посмотри что на входе 3. Открой `snapshots/<кейс>/` — посмотри эталон 4. Если изменение **намеренное** (доработка навыка) — обнови эталон: `node runner.mjs --update-snapshots` 5. Если **баг** — починить скрипт навыка и перезапустить тест ## Как добавить навык 1. Создать папку `tests/skills/cases/<имя-навыка>/` 2. Положить `_skill.json` — описание навыка для раннера 3. Добавить кейсы — по одному `.json` файлу на кейс ### Формат _skill.json ```json { "script": "meta-compile/scripts/meta-compile", "setup": "empty-config", "args": [ { "flag": "-JsonPath", "from": "inputFile" }, { "flag": "-OutputDir", "from": "workDir" } ], "snapshot": { "root": "workDir", "normalizeUuids": true } } ``` | Поле | Описание | |---|---| | `script` | Путь от `.claude/skills/`, без расширения. Раннер добавит `.ps1` (по умолчанию) или `.py` | | `setup` | Фикстура: `"empty-config"`, `"base-config"`, `"none"`, `"fixture:"` (из `fixtures/` папки навыка), `"external:"` (реальная выгрузка, read-only, skip если недоступна) | | `args` | Маппинг параметров навыка (см. ниже) | | `snapshot` | Настройки сравнения: `root` (`"workDir"` или `"outputPath"`) и `normalizeUuids` | ### Значения `from` в args | Значение | Что подставляется | |---|---| | `"inputFile"` | Путь к temp-файлу с `case.input` (JSON) | | `"workDir"` | Рабочая директория (копия фикстуры) | | `"outputPath"` | `workDir` + `case.outputPath` | | `"workPath"` | `workDir` + значение из `params.`. Поле указывается в `mapping.field` (по умолчанию `objectPath`) | | `"case."` | Значение из `params.` (приоритет) или корня кейса | | `"switch"` | Флаг без значения (напр. `-Detailed`) | | `"literal"` | Фиксированное значение из `mapping.value` | ## Как добавить кейс Положить `.json` файл в папку навыка. Имя файла = имя кейса. ### Позитивный кейс (минимальный) ```json { "name": "Простой справочник", "input": { "type": "Catalog", "name": "Валюты" } } ``` Раннер проверит: exitCode=0 + выход совпадает с эталоном. Эталон **обязателен**: если его нет и кейс не объявил `noSnapshot`, тест падает. Иначе потерянный (или не созданный при добавлении кейса) эталон неотличим от намеренного отсутствия — тест зелёный, хотя выход не проверяется. ### С параметрами навыка ```json { "name": "Обзор справочника", "params": { "objectPath": "Catalogs/Номенклатура" }, "expect": { "stdoutContains": "Номенклатура" } } ``` `params` — параметры для навыка. Используются через `case.` и `workPath` в `_skill.json`. `expect.stdoutContains` / `expect.stdoutNotContains` — строка **или массив строк**. Каждая подстрока проверяется на наличие (`stdoutContains`) или отсутствие (`stdoutNotContains`) в stdout навыка. Удобно для info-навыков: проверить, что нужная строка есть, а лишней — нет. ```json { "name": "Представление типа у ПВХ", "setup": "external:C:/WS/tasks/cfsrc/erp_8.3.24", "params": { "objectPath": "ChartsOfCharacteristicTypes/ВидыСубконтоХозрасчетные" }, "expect": { "stdoutContains": ["Представление типа: Вид субконто", "Представление объекта: Вид субконто"], "stdoutNotContains": "Представление списка:" } } ``` ### С дополнительными CLI-аргументами ```json { "name": "Конфигурация с поставщиком", "params": { "name": "Бухгалтерия" }, "args_extra": ["-Vendor", "Тест", "-Version", "2.0.1"] } ``` `args_extra` — дополнительные аргументы, не описанные в `_skill.json`, передаются навыку как есть. ### С предварительными шагами ```json { "name": "Добавление реквизита к справочнику", "preRun": [ { "script": "meta-compile/scripts/meta-compile", "input": { "type": "Catalog", "name": "Контрагенты" }, "args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" } } ], "params": { "objectPath": "Catalogs/Контрагенты" }, "input": { "operations": [{ "op": "add-attribute", "name": "ИНН", "type": "String", "length": 12 }] } } ``` `preRun` — шаги подготовки перед основным навыком. Каждый шаг: `script` (путь без расширения), `input` (JSON), `args` (маппинг с `{workDir}` и `{inputFile}` плейсхолдерами). ### Кейс с реальной выгрузкой ```json { "name": "Реальный справочник Номенклатура (БП)", "setup": "external:C:/WS/tasks/cfsrc/acc_8.3.24", "params": { "objectPath": "Catalogs/Номенклатура" }, "expect": { "stdoutContains": "Номенклатура" } } ``` `setup: "external:"` — использует реальную выгрузку конфигурации 1С как read-only рабочую директорию (без копирования). Если путь недоступен — тест пропускается (`○ skipped`), не падает. Подходит для info/validate навыков, которые не модифицируют файлы. ### Негативный кейс ```json { "name": "Ошибка: пустое имя", "input": { "type": "Catalog", "name": "" }, "expectError": true } ``` `expectError: true` — ожидается exitCode≠0. Строковое значение — проверит наличие в stderr. ### Все поля кейса | Поле | Обязательно | Описание | |---|---|---| | `name` | да | Название теста (отображается в отчёте) | | `input` | нет | JSON-объект, передаётся навыку через temp-файл | | `params` | нет | Параметры для `case.` и `workPath` маппинга | | `setup` | нет | Переопределение setup из `_skill.json` | | `outputPath` | нет | Относительный путь для навыков с `-OutputPath` | | `args_extra` | нет | Массив дополнительных CLI-аргументов | | `preRun` | нет | Массив шагов подготовки (см. ниже) | | `expect` | нет | Дополнительные проверки (см. ниже) | | `expectError` | нет | `true` или строка — ожидается ошибка | | `noSnapshot` | нет | Непустая строка с причиной — кейс объявляет, что эталон не нужен (см. «Эталоны») | | `idempotent` | нет | `true` — повторный прогон с теми же аргументами должен дать байт-в-байт тот же `workDir` | | `runtimeOnly` | нет | `"powershell"` / `"python"` — кейс имеет смысл только на одном порте, на другом `○ skipped` | | `skipValidation` | нет | `true` — не запускать `postValidate` из `_skill.json` (только при `--with-validation`) | ### Ключи `expect` | Ключ | Описание | |---|---| | `files` | Массив путей относительно `workDir` — каждый должен существовать после прогона | | `stdoutContains` | Строка или массив строк — все должны присутствовать в stdout | | `stdoutNotContains` | Строка или массив строк — ни одной не должно быть в stdout | | `preserves` | Объект (или массив объектов) — байтовые свойства файла, которые навык обязан сохранить | `preserves` проверяет то, что снэпшот-сравнение нормализует и потому увидеть не может: | Ключ | Описание | |---|---| | `file` | Путь к файлу относительно `workDir` (обязателен) | | `bom` | `true`/`false` — наличие UTF-8 BOM | | `eol` | `"crlf"` / `"lf"` | | `encoding` | Ожидаемое значение в XML-декларации, напр. `"UTF-8"` | | `finalNewline` | `true`/`false` — перевод строки в конце файла | | `noCR13` | `true` — в выходе не должно быть литерала ` ` | `preserves` и эталон **дополняют** друг друга: первый следит за байтовым стилем файла, второй — за структурой содержимого. Наличие одного не отменяет необходимости другого. ### Шаги `preRun` Массив шагов, выполняемых до запуска проверяемого навыка: | Форма шага | Описание | |---|---| | `{ "script": "<навык>/scripts/<файл>", "input": {...}, "args": { "-Flag": "{inputFile}" } }` | Прогон другого навыка для подготовки фикстуры. Плейсхолдеры: `{inputFile}`, `{workDir}` | | `{ "writeFile": { "path": "<путь>", "content": "<строка или объект>" } }` | Записать произвольный файл в `workDir` (объект сериализуется в JSON) | ## Эталоны (snapshots) Эталон — директория `snapshots/<имя-кейса>/` внутри папки навыка. Содержит ожидаемый выход навыка после нормализации. ### Когда эталон обязателен Всегда, кроме трёх случаев: - `expectError` — проверяется факт ошибки, выхода нет; - `setup: "external:"` — рабочая директория read-only, эталон физически не создать; - кейс объявил `noSnapshot` (см. ниже). Во всех остальных случаях отсутствующий (или пустой) эталон — **падение** с подсказкой, что делать. ### `noSnapshot` — когда эталон не нужен ```json { "name": "Валидатор: ссылочный тип разрешается", "noSnapshot": "meta-validate только читает и печатает — эталон зафиксировал бы выход preRun, а не проверяемого навыка; проверяется stdout", "expect": { "stdoutContains": "16. Reference types:" } } ``` Типичный случай — навык ничего не пишет в рабочую директорию (info/validate): эталон зафиксировал бы выход `preRun`, а не проверяемого навыка, и дублировал бы эталоны того навыка. **Причина обязательна** — непустая строка; `true` не принимается и валит кейс. Смысл в том, что отключение сверки должно стоить автору формулировки, а ревьюеру быть видно в diff'е: проверить осмысленность причины рантайм не может. Если у кейса стоит `noSnapshot`, но каталог эталона существует — тоже падение: такой эталон не сверяется и создаёт ложное впечатление покрытия. Удалите каталог либо снимите `noSnapshot`. ### Создание / обновление эталонов ```bash node tests/skills/runner.mjs cases/meta-compile/enum --update-snapshots # один кейс — предпочтительно node tests/skills/runner.mjs cases/meta-compile --update-snapshots # один навык node tests/skills/runner.mjs --update-snapshots # все кейсы ``` > Прогон по навыку/сюите **перезаписывает эталоны всех** кейсов сразу: если побочно поехал вывод > соседнего кейса, его эталон обновится вместе с целевым и непреднамеренная регрессия замаскируется. > Поэтому по умолчанию — точечно по кейсу, а после массового пересъёма обязательно проверяйте > `git diff` по `snapshots/`: каждая ± строка должна быть ожидаемой. > > Массовый пересъём легитимен, когда изменение вывода и правда затрагивает многих — например, правка > `meta-compile` меняет фикстуры ~20 навыков, чьи кейсы строятся его `preRun`-прогоном. > > Кейсы с `noSnapshot` пропускаются — эталон им не создаётся. ### Когда обновлять - После **намеренного** изменения логики навыка (новый выход — новый эталон) - После сертификации: загрузить результат в 1С (`db-load-xml`), убедиться что платформа приняла, затем `--update-snapshots` - **Не обновлять** если падение — неожиданный побочный эффект (это баг) ### Нормализация Перед сравнением (и при сохранении) применяется: - **UUID** → `UUID-001`, `UUID-002`... (по порядку появления, ссылочная целостность сохраняется) - **BOM** (U+FEFF) — удаляется - **Line endings** — `\r\n` → `\n` ## Структура ``` tests/skills/ runner.mjs # тест-раннер (snapshot-сравнение + интеграционные) verify-snapshots.mjs # платформенная верификация снапшотов README.md # этот файл .cache/ # кэш фикстур (в .gitignore) integration/ # многошаговые сценарии (*.test.mjs), в т.ч. движковая матрица 1cv8/ibcmd cases/ <навык>/ _skill.json # конфиг навыка <кейс>.json # тестовый случай snapshots/ <кейс>/ # эталон fixtures/ # broken-фикстуры (для validate-навыков) <имя>/ # сломанный XML, ссылка: "setup": "fixture:<имя>" ```