xdto-compile терял свойства без единого слова: на реалистичной чужой схеме из шести объявленных доезжало одно. Вложенные xs:sequence/xs:choice теперь уплощаются (модель хранит плоский список), xs:all трактуется как последовательность, xs:group и xs:attributeGroup раскрываются по ссылке — и о каждом приближении навык пишет предупреждение. Молчаливая потеря — тот же класс дефекта, что мы ловим у платформы, лечится так же: сообщением, не отказом. xdto-validate получил проверки на грабли, найденные при разработке: порядок элементов верхнего уровня (платформа отвергает пакет, не называя причины), конфликты объявлений (name+ref, type+вложенный тип, тип без разновидности), несовпадение рода базового типа, дубли имён свойств. Новые правила прогнаны по всем 760 пакетам выгрузок: всё, что породила платформа, валидно по определению, поэтому каждая ошибка там — ошибка правила. Первый прогон дал 7, и все три класса оказались реальным поведением платформы: length вместе с minLength/maxLength встречается, два пакета делят один targetNamespace (Envelope и SOAP_Envelope_1_1 в БП), form="Text" называется не только __content. Правила понижены до предупреждений либо сняты. Заодно убран шум: предупреждение о неиспользуемом import срабатывало на четверти корпуса — теперь только вместе с anyType, где оно и означает проблему. Итог: 0 ошибок на корпусе, предупреждений 53 вместо 242. Инструкции переписаны под читателя-исполнителя: убраны детали реализации и наши мерки, каталог проверок валидатора (его вывод самодостаточен), локальные пути в примерах заменены нейтральными. Таблица соответствий XSD и справочник аннотаций вынесены в xdto-compile/xsd-reference.md. Round-trip 760/760 сохранён, паритет PS/PY сохранён. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Регресс-тесты навыков
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) |
Что делать при падении
- Смотри case id в выводе — это путь к файлу кейса (можно перезапустить:
node runner.mjs <case-id>) - Открой
.jsonкейса — посмотри что на входе - Открой
snapshots/<кейс>/— посмотри эталон - Если изменение намеренное (доработка навыка) — обнови эталон:
node runner.mjs <case-id> --update-snapshots - Если баг — починить скрипт навыка и перезапустить тест
Как добавить навык
- Создать папку
tests/skills/cases/<имя-навыка>/ - Положить
_skill.json— описание навыка для раннера - Добавить кейсы — по одному
.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 — в выходе не должно быть литерала |
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 - Не обновлять если падение — неожиданный побочный эффект (это баг)
Нормализация
Перед сравнением (и при сохранении) применяется:
- 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:<имя>"