Files
cc-1c-skills/tests/skills
Nick ShirokovandClaude Opus 5 8232cbeaaa test(verify-snapshots): поддержка caseFiles и навыков xdto
Харнесс платформенной верификации не знал про caseFiles — механизм файлового
входа кейса, добавленный в runner.mjs. Та же функция перенесена сюда,
xdto-compile и xdto-edit добавлены в список проверяемых навыков.

Первый прогон отвергает 4 кейса из 9 — разбор в debug/xdto/FINDINGS.md §15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:12:02 +03:00
..

Регресс-тесты навыков

Snapshot-тестирование скриптов навыков: навык получает вход → генерирует файлы → результат сравнивается с эталоном.

Быстрые, файловые, без зависимости от платформы 1С.

Запуск

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 = есть падения.

Платформенная верификация снапшотов

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…), проверяющая что навыки работают вместе. Запуск:

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 <case-id>)
  2. Открой .json кейса — посмотри что на входе
  3. Открой snapshots/<кейс>/ — посмотри эталон
  4. Если изменение намеренное (доработка навыка) — обнови эталон: node runner.mjs <case-id> --update-snapshots
  5. Если баг — починить скрипт навыка и перезапустить тест

Как добавить навык

  1. Создать папку tests/skills/cases/<имя-навыка>/
  2. Положить _skill.json — описание навыка для раннера
  3. Добавить кейсы — по одному .json файлу на кейс

Формат _skill.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:<name>" (из fixtures/ папки навыка), "external:<path>" (реальная выгрузка, read-only, skip если недоступна)
args Маппинг параметров навыка (см. ниже)
snapshot Настройки сравнения: root ("workDir" или "outputPath") и normalizeUuids

Значения from в args

Значение Что подставляется
"inputFile" Путь к temp-файлу с case.input (JSON)
"workDir" Рабочая директория (копия фикстуры)
"outputPath" workDir + case.outputPath
"workPath" workDir + значение из params.<field>. Поле указывается в mapping.field (по умолчанию objectPath)
"case.<field>" Значение из params.<field> (приоритет) или корня кейса
"switch" Флаг без значения (напр. -Detailed)
"literal" Фиксированное значение из mapping.value

Как добавить кейс

Положить .json файл в папку навыка. Имя файла = имя кейса.

Позитивный кейс (минимальный)

{
  "name": "Простой справочник",
  "input": { "type": "Catalog", "name": "Валюты" }
}

Раннер проверит: exitCode=0 + выход совпадает с эталоном.

Эталон обязателен: если его нет и кейс не объявил noSnapshot, тест падает. Иначе потерянный (или не созданный при добавлении кейса) эталон неотличим от намеренного отсутствия — тест зелёный, хотя выход не проверяется.

С параметрами навыка

{
  "name": "Обзор справочника",
  "params": { "objectPath": "Catalogs/Номенклатура" },
  "expect": { "stdoutContains": "Номенклатура" }
}

params — параметры для навыка. Используются через case.<field> и workPath в _skill.json.

expect.stdoutContains / expect.stdoutNotContains — строка или массив строк. Каждая подстрока проверяется на наличие (stdoutContains) или отсутствие (stdoutNotContains) в stdout навыка. Удобно для info-навыков: проверить, что нужная строка есть, а лишней — нет.

{
  "name": "Представление типа у ПВХ",
  "setup": "external:C:/WS/tasks/cfsrc/erp_8.3.24",
  "params": { "objectPath": "ChartsOfCharacteristicTypes/ВидыСубконтоХозрасчетные" },
  "expect": {
    "stdoutContains": ["Представление типа: Вид субконто", "Представление объекта: Вид субконто"],
    "stdoutNotContains": "Представление списка:"
  }
}

С дополнительными CLI-аргументами

{
  "name": "Конфигурация с поставщиком",
  "params": { "name": "Бухгалтерия" },
  "args_extra": ["-Vendor", "Тест", "-Version", "2.0.1"]
}

args_extra — дополнительные аргументы, не описанные в _skill.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} плейсхолдерами).

Кейс с реальной выгрузкой

{
  "name": "Реальный справочник Номенклатура (БП)",
  "setup": "external:C:/WS/tasks/cfsrc/acc_8.3.24",
  "params": { "objectPath": "Catalogs/Номенклатура" },
  "expect": { "stdoutContains": "Номенклатура" }
}

setup: "external:<path>" — использует реальную выгрузку конфигурации 1С как read-only рабочую директорию (без копирования). Если путь недоступен — тест пропускается (○ skipped), не падает. Подходит для info/validate навыков, которые не модифицируют файлы.

Негативный кейс

{
  "name": "Ошибка: пустое имя",
  "input": { "type": "Catalog", "name": "" },
  "expectError": true
}

expectError: true — ожидается exitCode≠0. Строковое значение — проверит наличие в stderr.

Все поля кейса

Поле Обязательно Описание
name да Название теста (отображается в отчёте)
input нет JSON-объект, передаётся навыку через temp-файл
params нет Параметры для case.<field> и 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 — в выходе не должно быть литерала &#13;

preserves и эталон дополняют друг друга: первый следит за байтовым стилем файла, второй — за структурой содержимого. Наличие одного не отменяет необходимости другого.

Шаги preRun

Массив шагов, выполняемых до запуска проверяемого навыка:

Форма шага Описание
{ "script": "<навык>/scripts/<файл>", "input": {...}, "args": { "-Flag": "{inputFile}" } } Прогон другого навыка для подготовки фикстуры. Плейсхолдеры: {inputFile}, {workDir}
{ "writeFile": { "path": "<путь>", "content": "<строка или объект>" } } Записать произвольный файл в workDir (объект сериализуется в JSON)

Эталоны (snapshots)

Эталон — директория snapshots/<имя-кейса>/ внутри папки навыка. Содержит ожидаемый выход навыка после нормализации.

Когда эталон обязателен

Всегда, кроме трёх случаев:

  • expectError — проверяется факт ошибки, выхода нет;
  • setup: "external:<path>" — рабочая директория read-only, эталон физически не создать;
  • кейс объявил noSnapshot (см. ниже).

Во всех остальных случаях отсутствующий (или пустой) эталон — падение с подсказкой, что делать.

noSnapshot — когда эталон не нужен

{
  "name": "Валидатор: ссылочный тип разрешается",
  "noSnapshot": "meta-validate только читает и печатает — эталон зафиксировал бы выход preRun, а не проверяемого навыка; проверяется stdout",
  "expect": { "stdoutContains": "16. Reference types:" }
}

Типичный случай — навык ничего не пишет в рабочую директорию (info/validate): эталон зафиксировал бы выход preRun, а не проверяемого навыка, и дублировал бы эталоны того навыка.

Причина обязательна — непустая строка; true не принимается и валит кейс. Смысл в том, что отключение сверки должно стоить автору формулировки, а ревьюеру быть видно в diff'е: проверить осмысленность причины рантайм не может.

Если у кейса стоит noSnapshot, но каталог эталона существует — тоже падение: такой эталон не сверяется и создаёт ложное впечатление покрытия. Удалите каталог либо снимите noSnapshot.

Создание / обновление эталонов

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
  • Не обновлять если падение — неожиданный побочный эффект (это баг)

Нормализация

Перед сравнением (и при сохранении) применяется:

  • UUIDUUID-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:<имя>"