mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-03 16:50:52 +03:00
fix(skills): round-trip сохранение EOL/BOM/encoding в py edit-портах (#44/#46/#47)
Py-порты (lxml) при точечном редактировании существующего 1С-XML переписывали весь файл: CRLF→LF, encoding="UTF-8"→"utf-8", добавляли финальный перенос, плодили литерал (сериализация \r из tail'ов). Результат — широкий шумовой diff и скрытый лишний текст-узел при exit 0 и зелёной валидации. PS1-порты (XmlDocument) багу не подвержены — эталон. Фикс во всех 13 round-trip re-serialize py-навыках: перед записью детектится стиль существующего файла (BOM / EOL / регистр encoding / финальный перенос) и восстанавливается при сохранении; переносы канонизируются к LF (убирает ), затем приводятся к EOL источника. Новый файл (путь не существует) → прежнее поведение, снапшоты не двигаются. Навыки: cf-edit, meta-edit, meta-remove, interface-edit, subsystem-edit, skd-edit, form-edit, form-add, help-add, template-add, template-remove, form-remove, cfe-borrow. Версии py+ps1 подняты синхронно. Harness (tests/skills/runner.mjs): снята маска в normalizeXmlContent (порты её больше не порождают → гвардия ловит регресс); добавлен expect.preserves — raw-байтовая проверка BOM/EOL/encoding/финального переноса/отсутствия в обход нормализации. Регрессионные round-trip кейсы на CRLF+BOM+UTF-8 фикстурах для cf-edit/meta-edit/subsystem-edit. Верификация: py 556/556, ps1 556/556; платформа 1С 8.3.24 (verify-snapshots) cf-edit 12/12, meta-edit/subsystem-edit round-trip загружаются; негатив-тест подтверждает, что harness ловит дефект на старом коде. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
fbdf07e18a
commit
57e99d144e
+42
-3
@@ -299,9 +299,7 @@ function normalizeXmlContent(text, opts = {}) {
|
||||
/<\?xml\s+version=['"]1\.0['"]\s+encoding=['"]([^'"]+)['"]\s*\?>/gi,
|
||||
(_, enc) => `<?xml version="1.0" encoding="${enc.toLowerCase()}"?>`
|
||||
);
|
||||
// 2. Remove (CR encoded as XML entity by Python etree)
|
||||
s = s.replace(/ /g, '');
|
||||
// 3. Strip xmlns declarations (Python etree strips unused ones).
|
||||
// 2. Strip xmlns declarations (Python etree strips unused ones).
|
||||
// Skipped for Configuration.xml: those declarations are load-bearing (they back
|
||||
// xsi:type values like app:ApplicationUsePurpose in UsePurposes) and dropping them
|
||||
// is exactly the corruption of issue #38 — keeping them lets the test guard against it.
|
||||
@@ -347,6 +345,37 @@ function normalizeContent(text, config, relFile) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// ─── Byte-style preservation check (round-trip #44/#46/#47) ─────────────────
|
||||
// Проверяет СЫРЫЕ байты файла (в обход normalizeContent): BOM / EOL / регистр
|
||||
// encoding / финальный перенос / отсутствие . spec: { file, bom, eol:"crlf"|"lf",
|
||||
// encoding, finalNewline, noCR13 }. Возвращает массив ошибок.
|
||||
function checkPreserves(workDir, spec) {
|
||||
const errs = [];
|
||||
const target = join(workDir, spec.file);
|
||||
if (!existsSync(target)) { errs.push(`preserves: file not found: ${spec.file}`); return errs; }
|
||||
const buf = readFileSync(target);
|
||||
const hasBom = buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF;
|
||||
const body = hasBom ? buf.subarray(3) : buf;
|
||||
const text = body.toString('utf8');
|
||||
if (spec.bom !== undefined && hasBom !== spec.bom)
|
||||
errs.push(`preserves: BOM expected ${spec.bom}, got ${hasBom}`);
|
||||
if (spec.eol) {
|
||||
const hasCR = body.includes(0x0d);
|
||||
const wantCR = spec.eol === 'crlf';
|
||||
if (hasCR !== wantCR) errs.push(`preserves: EOL expected ${spec.eol} (CR=${wantCR}), got CR=${hasCR}`);
|
||||
}
|
||||
if (spec.encoding) {
|
||||
const m = /encoding="([^"]+)"/.exec(text);
|
||||
if (!m || m[1] !== spec.encoding) errs.push(`preserves: encoding expected "${spec.encoding}", got "${m ? m[1] : '?'}"`);
|
||||
}
|
||||
if (spec.finalNewline !== undefined) {
|
||||
const endsNL = body.length > 0 && body[body.length - 1] === 0x0a;
|
||||
if (endsNL !== spec.finalNewline) errs.push(`preserves: finalNewline expected ${spec.finalNewline}, got ${endsNL}`);
|
||||
}
|
||||
if (spec.noCR13 && text.includes(' ')) errs.push(`preserves: unexpected literal in output`);
|
||||
return errs;
|
||||
}
|
||||
|
||||
// ─── Snapshot comparison ────────────────────────────────────────────────────
|
||||
|
||||
// Capture raw byte contents of every file in dir, keyed by relative path.
|
||||
@@ -608,6 +637,11 @@ async function runCaseAsync(testCase, opts) {
|
||||
if (stdout.includes(needle)) errors.push(`stdout unexpectedly contains "${needle}"`);
|
||||
}
|
||||
}
|
||||
if (caseData.expect?.preserves) {
|
||||
const specs = Array.isArray(caseData.expect.preserves)
|
||||
? caseData.expect.preserves : [caseData.expect.preserves];
|
||||
for (const spec of specs) errors.push(...checkPreserves(workDir, spec));
|
||||
}
|
||||
if (errors.length === 0 && !caseData.expectError && !workspace.readOnly) {
|
||||
const snapshotConfig = { ...skillConfig.snapshot, runtime: opts.runtime };
|
||||
if (opts.updateSnapshots) {
|
||||
@@ -785,6 +819,11 @@ function runCase(testCase, opts) {
|
||||
if (stdout.includes(needle)) errors.push(`stdout unexpectedly contains "${needle}"`);
|
||||
}
|
||||
}
|
||||
if (caseData.expect?.preserves) {
|
||||
const specs = Array.isArray(caseData.expect.preserves)
|
||||
? caseData.expect.preserves : [caseData.expect.preserves];
|
||||
for (const spec of specs) errors.push(...checkPreserves(workDir, spec));
|
||||
}
|
||||
|
||||
// Snapshot comparison (skip for external/read-only workspaces)
|
||||
if (errors.length === 0 && !caseData.expectError && !workspace.readOnly) {
|
||||
|
||||
Reference in New Issue
Block a user