mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-01 07:50:50 +03:00
fix(tests): verify-snapshots понимает inputRaw, inputEncoding и версию формата EPF/ERF
Полный прогон верификации давал шесть падений; четыре из них — дефекты самого инструмента. Вход кейса. Файл писался только из caseData.input и только UTF-8, поэтому кейсы, задающие вход через inputRaw (сырая строка) и inputEncoding (UTF-16, cp1251), оставляли навык без входного файла: meta-compile/encoding-utf16-bom и interface-edit/bad-definition-file падали на «Cannot bind argument to parameter». Хуже того, кейс, у которого нет ни input, ни preRun, ни params, отбрасывался в discoverCases как «ничего не подающий навыку» — inputRaw там тоже не учитывался, и такие кейсы в верификацию не попадали вовсе (meta-compile/bad-encoding-cp1251, subsystem-compile/bad-definition-file). Теперь оба ключа поддержаны, encodeInput перенесён копией из runner.mjs — общего модуля у раннеров нет, и README предписывает добавлять ключ в оба. Это ровно та «тихая дыра», о которой README предупреждает: ключ, известный лишь одному раннеру, даёт кейс, зелёный в функциональном прогоне и не доезжающий до платформы в верификации. Версия формата. Гейт вызывался только при наличии каталога конфигурации, поэтому кейсы epf-init/format-221 и erf-init/format-221 доходили до сборки и падали на стенде без 8.5 — красным, хотя это свойство стенда. Ядро гейта выделено и читает версию из шапки любого MetaDataObject, включая исходник обработки и отчёта; в EPF-ветке проверка выполняется до epf-build. Проверено точечно: два кейса с inputRaw проходят, два format-221 честно пропускаются по версии платформы, два ранее невидимых кейса берутся в прогон и проходят. Функциональный раннер не задет: role-compile 26/26, meta-compile 85/85. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9c66e89372
commit
66a11d1dba
@@ -732,6 +732,39 @@ function compatibilityGap(configDir, v8path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Вход кейса: те же ключи, что понимает runner.mjs ────────────────────────
|
||||
// DSL один, реализации две, и ключ, известный лишь одному раннеру, даёт тихую дыру: кейс зелёный
|
||||
// в функциональном прогоне и не доезжает до платформы в верификации. Так и вышло с inputRaw —
|
||||
// verify писал вход только из caseData.input и только UTF-8, поэтому навык оставался без входного
|
||||
// файла. Копия encodeInput из runner.mjs (общего модуля у раннеров нет) — держать одинаковыми.
|
||||
// Байты входного файла в заданной кодировке. Нужно для кейсов про кодировку: writeFileSync
|
||||
// пишет только UTF-8, а навык обязан одинаково вести себя на UTF-16 с BOM (принять) и на
|
||||
// cp1251 (отвергнуть, а не молча подменить кириллицу на U+FFFD). cp1251 в Node нет — кодируем
|
||||
// формулой по диапазонам, которые встречаются в кейсах (ASCII + кириллица); прочее — ошибка кейса.
|
||||
function encodeInput(text, encoding) {
|
||||
if (!encoding || encoding === 'utf-8' || encoding === 'utf8') return Buffer.from(text, 'utf8');
|
||||
if (encoding === 'utf-16le') return Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(text, 'utf16le')]);
|
||||
if (encoding === 'utf-16be') {
|
||||
const le = Buffer.from(text, 'utf16le');
|
||||
const be = Buffer.alloc(le.length);
|
||||
for (let i = 0; i < le.length; i += 2) { be[i] = le[i + 1]; be[i + 1] = le[i]; }
|
||||
return Buffer.concat([Buffer.from([0xfe, 0xff]), be]);
|
||||
}
|
||||
if (encoding === 'cp1251') {
|
||||
const out = Buffer.alloc(text.length);
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text.codePointAt(i);
|
||||
if (c < 0x80) out[i] = c;
|
||||
else if (c >= 0x410 && c <= 0x44f) out[i] = c - 0x350;
|
||||
else if (c === 0x401) out[i] = 0xa8;
|
||||
else if (c === 0x451) out[i] = 0xb8;
|
||||
else throw new Error(`inputEncoding cp1251: символ U+${c.toString(16)} вне поддержанного набора (ASCII + кириллица)`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
throw new Error(`inputEncoding: неизвестная кодировка "${encoding}"`);
|
||||
}
|
||||
|
||||
// Версия формата выгрузки против платформы: формат 2.21 не загрузится на 8.3.27, даже если
|
||||
// режим совместимости платформе подходит. Без этой проверки кейс на 2.21 скипался только на
|
||||
// 8.3.24 (по совместимости), а на 8.3.27 доходил до загрузки и падал — постоянный ложный
|
||||
@@ -740,9 +773,16 @@ function compatibilityGap(configDir, v8path) {
|
||||
// Эталон — таблица «Лестница версий» из docs/1c-configuration-spec.md (§7.1); разбор — копия
|
||||
// такого же в check-format-versions.mjs, держать одинаковыми.
|
||||
function formatGap(configDir, v8path) {
|
||||
const cfgFile = join(configDir, 'Configuration.xml');
|
||||
if (!existsSync(cfgFile)) return null;
|
||||
const fm = /<MetaDataObject[^>]*\sversion="(\d+\.\d+)"/.exec(readFileSync(cfgFile, 'utf8'));
|
||||
return formatGapForXml(join(configDir, 'Configuration.xml'), v8path);
|
||||
}
|
||||
|
||||
// Ядро гейта: версия формата берётся из шапки любого MetaDataObject — Configuration.xml для
|
||||
// конфигурации, исходник обработки/отчёта для EPF/ERF. Раньше проверка вызывалась только при
|
||||
// наличии каталога конфигурации, поэтому кейсы epf-init/erf-init на формате 2.21 доходили до
|
||||
// сборки и падали на стенде без 8.5 — красным, хотя это свойство стенда.
|
||||
function formatGapForXml(xmlFile, v8path) {
|
||||
if (!existsSync(xmlFile)) return null;
|
||||
const fm = /<MetaDataObject[^>]*\sversion="(\d+\.\d+)"/.exec(readFileSync(xmlFile, 'utf8'));
|
||||
const pm = /(\d+\.\d+\.\d+)/.exec(v8path || '');
|
||||
if (!fm || !pm) return null;
|
||||
|
||||
@@ -1008,9 +1048,14 @@ async function verifyCase(skillName, caseName, skillConfig, caseData, opts) {
|
||||
|
||||
// ── Step 4: Main skill script ──
|
||||
let inputFile = null;
|
||||
if (caseData.input !== undefined) {
|
||||
if (caseData.inputRaw !== undefined) {
|
||||
// inputRaw пишется дословно: негативный кейс про битый JSON через input невыразим —
|
||||
// JSON.stringify всегда даёт валидный документ.
|
||||
inputFile = join(workDir, '__input.json');
|
||||
writeFileSync(inputFile, JSON.stringify(caseData.input, null, 2), 'utf8');
|
||||
writeFileSync(inputFile, encodeInput(caseData.inputRaw, caseData.inputEncoding));
|
||||
} else if (caseData.input !== undefined) {
|
||||
inputFile = join(workDir, '__input.json');
|
||||
writeFileSync(inputFile, encodeInput(JSON.stringify(caseData.input, null, 2), caseData.inputEncoding));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1198,6 +1243,13 @@ async function verifyCase(skillName, caseName, skillConfig, caseData, opts) {
|
||||
result.errors.push(`EPF/ERF source not found: ${sourceFile}`);
|
||||
return result;
|
||||
}
|
||||
const versionSkip = formatGapForXml(sourceFile, opts.v8ctx.v8path);
|
||||
if (versionSkip) {
|
||||
result.skipped = true;
|
||||
result.skipReason = versionSkip;
|
||||
log('epf-build', true, `skipped (${versionSkip})`);
|
||||
return result;
|
||||
}
|
||||
const outDir = join(workDir, '__build');
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const outFile = join(outDir, `${name}${epfExt}`);
|
||||
@@ -1531,7 +1583,10 @@ function discoverCases(skillFilter, caseFilter) {
|
||||
// без этой ветки такие кейсы молча выпадали из платформенной проверки — то есть ровно
|
||||
// из той, ради которой этот файл и существует.
|
||||
const hasFixtureSetup = typeof caseData.setup === 'string' && caseData.setup.startsWith('fixture:');
|
||||
if (caseData.input === undefined && !caseData.preRun && !caseData.params && !hasFixtureSetup) continue;
|
||||
// inputRaw — такой же вход, как input: кейс, подающий навыку сырую строку, отбрасывался
|
||||
// здесь как «ничего не подаёт» и в верификацию не попадал вовсе.
|
||||
const hasInput = caseData.input !== undefined || caseData.inputRaw !== undefined;
|
||||
if (!hasInput && !caseData.preRun && !caseData.params && !hasFixtureSetup) continue;
|
||||
|
||||
results.push({ skill: skillDir, caseName, caseData, skillConfig });
|
||||
}
|
||||
@@ -1653,25 +1708,6 @@ async function main() {
|
||||
const passed = results.filter(r => r.passed).length;
|
||||
const skipped = results.filter(r => r.skipped).length;
|
||||
const failed = results.filter(r => !r.passed && !r.skipped).length;
|
||||
console.log(`\n${'='.repeat(60)}`);
|
||||
console.log(`Results: ${passed} passed, ${failed} failed`
|
||||
+ (skipped ? `, ${skipped} skipped` : '') + ` out of ${results.length}`);
|
||||
// «Прошло» и «проверено платформой» — разные вещи: часть кейсов законно идёт мимо неё
|
||||
// (навык отказал, выход не конфигурация, external). Пока эта доля не названа, отчёт читается
|
||||
// как «всё проверено», хотя платформу спрашивали не у всех.
|
||||
const onPlatform = results.filter(r => r.platformChecked).length;
|
||||
// Только успешные: у падения обращение к платформе было — оно и не удалось, мешать их
|
||||
// с «мимо платформы» значит завышать долю непроверенного.
|
||||
const offPlatform = results.filter(r => r.passed && !r.skipped && !r.platformChecked);
|
||||
console.log(` из них проверено платформой: ${onPlatform}; без обращения к платформе: ${offPlatform.length}`);
|
||||
const byReason = {};
|
||||
for (const r of offPlatform) {
|
||||
const key = r.noPlatformReason || 'причина не указана';
|
||||
(byReason[key] = byReason[key] || []).push(`${r.skill}/${r.case}`);
|
||||
}
|
||||
for (const [reason, list] of Object.entries(byReason)) {
|
||||
console.log(` • ${list.length} — ${reason}`);
|
||||
}
|
||||
for (const r of results.filter(x => x.skipped)) {
|
||||
console.log(` \u25cb ${r.skill}/${r.case} \u2014 ${r.skipReason}`);
|
||||
}
|
||||
@@ -1683,6 +1719,26 @@ async function main() {
|
||||
}
|
||||
|
||||
writeReport(results);
|
||||
|
||||
console.log(`\n${'='.repeat(60)}`);
|
||||
console.log(`Results: ${passed} passed, ${failed} failed`
|
||||
+ (skipped ? `, ${skipped} skipped` : '') + ` out of ${results.length}`);
|
||||
// «Прошло» и «проверено платформой» — разные вещи: часть кейсов законно идёт мимо неё
|
||||
// (навык отказал, выход не конфигурация, external). Пока эта доля не названа, отчёт читается
|
||||
// как «всё проверено», хотя платформу спрашивали не у всех.
|
||||
const onPlatform = results.filter(r => r.platformChecked).length;
|
||||
// Только успешные: у падения обращение к платформе было — оно и не удалось, мешать его
|
||||
// с «мимо платформы» значит завышать долю непроверенного.
|
||||
const offPlatform = results.filter(r => r.passed && !r.skipped && !r.platformChecked);
|
||||
const byReason = {};
|
||||
for (const r of offPlatform) {
|
||||
const key = r.noPlatformReason || 'причина не указана';
|
||||
(byReason[key] = byReason[key] || []).push(`${r.skill}/${r.case}`);
|
||||
}
|
||||
console.log(` из них проверено платформой: ${onPlatform}; прошло без обращения к платформе: ${offPlatform.length}`);
|
||||
for (const [reason, list] of Object.entries(byReason)) {
|
||||
console.log(` • ${list.length} — ${reason}`);
|
||||
}
|
||||
const unverified = opts.strict ? skipped : 0;
|
||||
if (unverified) console.error(`\n--strict: ${unverified} кейс(ов) пропущено — считаем падением`);
|
||||
process.exit(failed + unverified > 0 ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user