mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-16 08:19:01 +03:00
test(skills): expect.stderrContains и захват stderr на успешном прогоне
execSkillAsync отдавал только stdout, а stderr сохранялся исключительно в ветке ошибки. Поэтому предупреждение навыка, который отработал успешно (exit 0), проверить было нечем: кейс мог убедиться лишь в том, что навык не упал, — то есть в молчании вместо текста. Резолв теперь отдаёт оба потока, добавлен ключ expect.stderrContains. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9949039d98
commit
35c8618813
@@ -49,6 +49,7 @@ node tests/skills/check-inline-drift.mjs --list # реестр: семья →
|
|||||||
| `check-uuid-invariant.mjs` | uuid объекта и сущностей не перегенерируются при правке |
|
| `check-uuid-invariant.mjs` | uuid объекта и сущностей не перегенерируются при правке |
|
||||||
| `check-inline-drift.mjs` | копии общих inline-утилит совпадают с эталонами своих вариантов |
|
| `check-inline-drift.mjs` | копии общих inline-утилит совпадают с эталонами своих вариантов |
|
||||||
| `check-type-maps.mjs` | карты типов метаданных согласованы с таблицей `ChildObjects` из `docs/1c-configuration-spec.md` |
|
| `check-type-maps.mjs` | карты типов метаданных согласованы с таблицей `ChildObjects` из `docs/1c-configuration-spec.md` |
|
||||||
|
| `check-format-versions.mjs` | проверенный диапазон версий формата: границы одинаковы во всех навыках и сходятся с лестницей из `docs/1c-configuration-spec.md` |
|
||||||
|
|
||||||
`check-inline-drift.mjs` держит реестр семей внутри себя: у каждой семьи перечислены варианты, у
|
`check-inline-drift.mjs` держит реестр семей внутри себя: у каждой семьи перечислены варианты, у
|
||||||
варианта — навык-эталон и список копий. Отклоняющийся вариант обязан иметь обоснование (`why`),
|
варианта — навык-эталон и список копий. Отклоняющийся вариант обязан иметь обоснование (`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»
|
`check-type-maps.mjs` сверяет словари типов метаданных с таблицей «Порядок типов в ChildObjects»
|
||||||
из спецификации: имя типа, каталог и порядок обязаны совпадать, а тип, которого в карте нет
|
из спецификации: имя типа, каталог и порядок обязаны совпадать, а тип, которого в карте нет
|
||||||
намеренно, объявляется исключением **с причиной**. Так проверка отличает намеренное ограничение
|
намеренно, объявляется исключением **с причиной**. Так проверка отличает намеренное ограничение
|
||||||
@@ -278,6 +286,7 @@ ibcmd-проход автоматически `○ skipped`, если рядом
|
|||||||
| `files` | Массив путей относительно `workDir` — каждый должен существовать после прогона |
|
| `files` | Массив путей относительно `workDir` — каждый должен существовать после прогона |
|
||||||
| `stdoutContains` | Строка или массив строк — все должны присутствовать в stdout |
|
| `stdoutContains` | Строка или массив строк — все должны присутствовать в stdout |
|
||||||
| `stdoutNotContains` | Строка или массив строк — ни одной не должно быть в stdout |
|
| `stdoutNotContains` | Строка или массив строк — ни одной не должно быть в stdout |
|
||||||
|
| `stderrContains` | Строка или массив строк — все должны присутствовать в stderr. Для предупреждений: навык печатает их в stderr и продолжает, поэтому `expectError` тут не годится |
|
||||||
| `preserves` | Объект (или массив объектов) — байтовые свойства файла, которые навык обязан сохранить |
|
| `preserves` | Объект (или массив объектов) — байтовые свойства файла, которые навык обязан сохранить |
|
||||||
|
|
||||||
`preserves` проверяет то, что снэпшот-сравнение нормализует и потому увидеть не может:
|
`preserves` проверяет то, что снэпшот-сравнение нормализует и потому увидеть не может:
|
||||||
|
|||||||
+24
-4
@@ -232,7 +232,9 @@ function execSkillAsync(runtime, scriptPath, args, cwd) {
|
|||||||
err.stderr = stderr || '';
|
err.stderr = stderr || '';
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} 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) —
|
// который молча ничего не проверяет (так уже было с 9 кейсами meta-edit) —
|
||||||
// поэтому он ошибка, а не игнор.
|
// поэтому он ошибка, а не игнор.
|
||||||
const KNOWN_EXPECT_KEYS = new Set([
|
const KNOWN_EXPECT_KEYS = new Set([
|
||||||
'files', 'stdoutContains', 'stdoutNotContains', 'preserves',
|
'files', 'stdoutContains', 'stdoutNotContains', 'stderrContains', 'preserves',
|
||||||
'fileContains', 'fileNotContains', 'filesEqual',
|
'fileContains', 'fileNotContains', 'filesEqual',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -781,7 +783,7 @@ async function runCaseAsync(testCase, opts) {
|
|||||||
let stdout = '', stderr = '', exitCode = 0;
|
let stdout = '', stderr = '', exitCode = 0;
|
||||||
try {
|
try {
|
||||||
const execCwd = (caseData.cwd || skillConfig.cwd) === 'workDir' ? workDir : undefined;
|
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) {
|
} catch (e) {
|
||||||
exitCode = e.status ?? 1;
|
exitCode = e.status ?? 1;
|
||||||
stdout = e.stdout || '';
|
stdout = e.stdout || '';
|
||||||
@@ -825,6 +827,15 @@ async function runCaseAsync(testCase, opts) {
|
|||||||
if (stdout.includes(needle)) errors.push(`stdout unexpectedly contains "${needle}"`);
|
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.expectError) {
|
||||||
if (caseData.expect?.preserves) {
|
if (caseData.expect?.preserves) {
|
||||||
@@ -1041,6 +1052,15 @@ function runCase(testCase, opts) {
|
|||||||
if (stdout.includes(needle)) errors.push(`stdout unexpectedly contains "${needle}"`);
|
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.expectError) {
|
||||||
@@ -1408,7 +1428,7 @@ async function runIntegrationOnce(test, opts, engine, labelEngine) {
|
|||||||
// Execute
|
// Execute
|
||||||
let stdout = '', stderr = '';
|
let stdout = '', stderr = '';
|
||||||
try {
|
try {
|
||||||
stdout = await execSkillAsync(opts.runtime, script, args);
|
({ stdout, stderr } = await execSkillAsync(opts.runtime, script, args));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const detail = e.stderr?.trim() || e.stdout?.trim() || e.message;
|
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)}` });
|
stepResults.push({ name: step.name, passed: false, error: `Step ${i + 1} failed: ${detail.substring(0, 1000)}` });
|
||||||
|
|||||||
Reference in New Issue
Block a user