From 35c86188132dca8906614b5f082d7fa20104d9bb Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 13 Aug 2026 14:20:19 +0300 Subject: [PATCH] =?UTF-8?q?test(skills):=20expect.stderrContains=20=D0=B8?= =?UTF-8?q?=20=D0=B7=D0=B0=D1=85=D0=B2=D0=B0=D1=82=20stderr=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D1=83=D1=81=D0=BF=D0=B5=D1=88=D0=BD=D0=BE=D0=BC=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B3=D0=BE=D0=BD=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit execSkillAsync отдавал только stdout, а stderr сохранялся исключительно в ветке ошибки. Поэтому предупреждение навыка, который отработал успешно (exit 0), проверить было нечем: кейс мог убедиться лишь в том, что навык не упал, — то есть в молчании вместо текста. Резолв теперь отдаёт оба потока, добавлен ключ expect.stderrContains. Co-Authored-By: Claude Opus 5 (1M context) --- tests/skills/README.md | 9 +++++++++ tests/skills/runner.mjs | 28 ++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/tests/skills/README.md b/tests/skills/README.md index 8d6b03a0..e43d8154 100644 --- a/tests/skills/README.md +++ b/tests/skills/README.md @@ -49,6 +49,7 @@ node tests/skills/check-inline-drift.mjs --list # реестр: семья → | `check-uuid-invariant.mjs` | uuid объекта и сущностей не перегенерируются при правке | | `check-inline-drift.mjs` | копии общих inline-утилит совпадают с эталонами своих вариантов | | `check-type-maps.mjs` | карты типов метаданных согласованы с таблицей `ChildObjects` из `docs/1c-configuration-spec.md` | +| `check-format-versions.mjs` | проверенный диапазон версий формата: границы одинаковы во всех навыках и сходятся с лестницей из `docs/1c-configuration-spec.md` | `check-inline-drift.mjs` держит реестр семей внутри себя: у каждой семьи перечислены варианты, у варианта — навык-эталон и список копий. Отклоняющийся вариант обязан иметь обоснование (`why`), @@ -57,6 +58,13 @@ node tests/skills/check-inline-drift.mjs --list # реестр: семья → Правишь общую утилиту — правишь эталон, копируешь во всех потребителей варианта, прогоняешь гард. +`check-format-versions.mjs` держит границы проверенного диапазона версий формата выгрузки. Раньше +допустимый список версий был независимым литералом в каждом валидаторе, и сверять его было не с +чем: волна 2.21 прошла по четырём валидаторам и молча обошла пятый — `form-validate` остался на +2.17–2.20 и ругался на форму, которую сам же создавал (issue #63). Эталон верхней границы — последняя +**замеренная** ступень таблицы «Лестница версий» из спецификации, поэтому расхождение спеки и кода +падает здесь, а не на чужой выгрузке. + `check-type-maps.mjs` сверяет словари типов метаданных с таблицей «Порядок типов в ChildObjects» из спецификации: имя типа, каталог и порядок обязаны совпадать, а тип, которого в карте нет намеренно, объявляется исключением **с причиной**. Так проверка отличает намеренное ограничение @@ -278,6 +286,7 @@ ibcmd-проход автоматически `○ skipped`, если рядом | `files` | Массив путей относительно `workDir` — каждый должен существовать после прогона | | `stdoutContains` | Строка или массив строк — все должны присутствовать в stdout | | `stdoutNotContains` | Строка или массив строк — ни одной не должно быть в stdout | +| `stderrContains` | Строка или массив строк — все должны присутствовать в stderr. Для предупреждений: навык печатает их в stderr и продолжает, поэтому `expectError` тут не годится | | `preserves` | Объект (или массив объектов) — байтовые свойства файла, которые навык обязан сохранить | `preserves` проверяет то, что снэпшот-сравнение нормализует и потому увидеть не может: diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index 860dd237..194e384b 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -232,7 +232,9 @@ function execSkillAsync(runtime, scriptPath, args, cwd) { err.stderr = stderr || ''; reject(err); } else { - resolve(stdout); + // Оба потока, а не только stdout: предупреждение навыка уходит в stderr при exit 0, + // и на успешном прогоне оно раньше терялось — expect.stderrContains не мог сработать. + resolve({ stdout, stderr: stderr || '' }); } }); }); @@ -409,7 +411,7 @@ function checkFileContains(workDir, spec, expectPresent) { // который молча ничего не проверяет (так уже было с 9 кейсами meta-edit) — // поэтому он ошибка, а не игнор. const KNOWN_EXPECT_KEYS = new Set([ - 'files', 'stdoutContains', 'stdoutNotContains', 'preserves', + 'files', 'stdoutContains', 'stdoutNotContains', 'stderrContains', 'preserves', 'fileContains', 'fileNotContains', 'filesEqual', ]); @@ -781,7 +783,7 @@ async function runCaseAsync(testCase, opts) { let stdout = '', stderr = '', exitCode = 0; try { const execCwd = (caseData.cwd || skillConfig.cwd) === 'workDir' ? workDir : undefined; - stdout = await execSkillAsync(opts.runtime, scriptPath, args, execCwd); + ({ stdout, stderr } = await execSkillAsync(opts.runtime, scriptPath, args, execCwd)); } catch (e) { exitCode = e.status ?? 1; stdout = e.stdout || ''; @@ -825,6 +827,15 @@ async function runCaseAsync(testCase, opts) { if (stdout.includes(needle)) errors.push(`stdout unexpectedly contains "${needle}"`); } } + // Предупреждение — не отказ: навык печатает его в stderr и продолжает работу. Без + // отдельного ключа такой кейс проверял бы только exit 0, то есть молчание вместо текста. + if (caseData.expect?.stderrContains) { + const needles = Array.isArray(caseData.expect.stderrContains) + ? caseData.expect.stderrContains : [caseData.expect.stderrContains]; + for (const needle of needles) { + if (!stderr.includes(needle)) errors.push(`stderr does not contain "${needle}"`); + } + } } if (!caseData.expectError) { if (caseData.expect?.preserves) { @@ -1041,6 +1052,15 @@ function runCase(testCase, opts) { if (stdout.includes(needle)) errors.push(`stdout unexpectedly contains "${needle}"`); } } + // Предупреждение — не отказ: навык печатает его в stderr и продолжает работу. Без + // отдельного ключа такой кейс проверял бы только exit 0, то есть молчание вместо текста. + if (caseData.expect?.stderrContains) { + const needles = Array.isArray(caseData.expect.stderrContains) + ? caseData.expect.stderrContains : [caseData.expect.stderrContains]; + for (const needle of needles) { + if (!stderr.includes(needle)) errors.push(`stderr does not contain "${needle}"`); + } + } } if (!caseData.expectError) { @@ -1408,7 +1428,7 @@ async function runIntegrationOnce(test, opts, engine, labelEngine) { // Execute let stdout = '', stderr = ''; try { - stdout = await execSkillAsync(opts.runtime, script, args); + ({ stdout, stderr } = await execSkillAsync(opts.runtime, script, args)); } catch (e) { const detail = e.stderr?.trim() || e.stdout?.trim() || e.message; stepResults.push({ name: step.name, passed: false, error: `Step ${i + 1} failed: ${detail.substring(0, 1000)}` });