From 4565808b7798b591b167f368edb46b7f2925e51c Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 28 Mar 2026 19:05:49 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20Python=20XML=20compat=20=E2=80=94=20decl?= =?UTF-8?q?aration=20quotes=20+=20runner=20normalization=20(112=E2=86=9226?= =?UTF-8?q?6/285)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scripts (production fix): fix XML declaration in 14 save_xml_bom scripts - version='1.0' → version="1.0" (single→double quotes) - encoding='UTF-8' → encoding="utf-8" (match PS1 XmlWriter output) - Add trailing newline to etree.tostring output Runner (test normalization, Python-only): - normalizeXmlContent() applied only when --runtime python - Handles etree serialization quirks: xmlns stripping, self-closing space, inter-tag whitespace, empty elements, entities - PS1 tests remain strict — no normalization applied 19 remaining failures are real logic bugs in Python scripts. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/cf-edit/scripts/cf-edit.py | 4 ++- .../skills/cfe-borrow/scripts/cfe-borrow.py | 4 ++- .../skills/epf-add-form/scripts/add-form.py | 4 ++- .claude/skills/form-add/scripts/form-add.py | 4 ++- .claude/skills/form-edit/scripts/form-edit.py | 6 ++-- .../skills/form-remove/scripts/remove-form.py | 4 ++- .claude/skills/help-add/scripts/add-help.py | 4 ++- .../interface-edit/scripts/interface-edit.py | 4 ++- .../meta-compile/scripts/meta-compile.py | 8 +++-- .claude/skills/meta-edit/scripts/meta-edit.py | 6 ++-- .../skills/meta-remove/scripts/meta-remove.py | 4 ++- .claude/skills/skd-edit/scripts/skd-edit.py | 4 ++- .../subsystem-edit/scripts/subsystem-edit.py | 4 ++- .../template-add/scripts/add-template.py | 4 ++- .../scripts/remove-template.py | 4 ++- tests/skills/runner.mjs | 30 +++++++++++++++++-- 16 files changed, 78 insertions(+), 20 deletions(-) diff --git a/.claude/skills/cf-edit/scripts/cf-edit.py b/.claude/skills/cf-edit/scripts/cf-edit.py index 3af0d6ec..39313f06 100644 --- a/.claude/skills/cf-edit/scripts/cf-edit.py +++ b/.claude/skills/cf-edit/scripts/cf-edit.py @@ -131,7 +131,9 @@ def parse_batch_value(val): def save_xml_bom(tree, path): xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") - xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + xml_bytes = xml_bytes.replace(b"", b'') + if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py index cec322c9..0140da1c 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py @@ -305,7 +305,9 @@ def expand_self_closing(container, parent_indent): def save_xml_bom(tree, path): xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") - xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + xml_bytes = xml_bytes.replace(b"", b'') + if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) diff --git a/.claude/skills/epf-add-form/scripts/add-form.py b/.claude/skills/epf-add-form/scripts/add-form.py index 2bd85909..5ea16241 100644 --- a/.claude/skills/epf-add-form/scripts/add-form.py +++ b/.claude/skills/epf-add-form/scripts/add-form.py @@ -15,7 +15,9 @@ NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"} def save_xml_with_bom(tree, path): """Save XML tree to file with UTF-8 BOM.""" xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") - xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + xml_bytes = xml_bytes.replace(b"", b'') + if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) diff --git a/.claude/skills/form-add/scripts/form-add.py b/.claude/skills/form-add/scripts/form-add.py index 41efdf4c..d4aa7960 100644 --- a/.claude/skills/form-add/scripts/form-add.py +++ b/.claude/skills/form-add/scripts/form-add.py @@ -18,7 +18,9 @@ NSMAP = { def save_xml_with_bom(tree, path): """Save XML tree to file with UTF-8 BOM.""" xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") - xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + xml_bytes = xml_bytes.replace(b"", b'') + if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) diff --git a/.claude/skills/form-edit/scripts/form-edit.py b/.claude/skills/form-edit/scripts/form-edit.py index 4aced255..94860ac7 100644 --- a/.claude/skills/form-edit/scripts/form-edit.py +++ b/.claude/skills/form-edit/scripts/form-edit.py @@ -1242,8 +1242,10 @@ if elem_events_list: # ── 13. Save ──────────────────────────────────────────────── xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") -# Fix encoding declaration case -xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') +# Fix XML declaration quotes +xml_bytes = xml_bytes.replace(b"", b'') +if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" # Write with BOM with open(resolved_form_path, "wb") as f: f.write(b'\xef\xbb\xbf') diff --git a/.claude/skills/form-remove/scripts/remove-form.py b/.claude/skills/form-remove/scripts/remove-form.py index b9354344..0bbcdbba 100644 --- a/.claude/skills/form-remove/scripts/remove-form.py +++ b/.claude/skills/form-remove/scripts/remove-form.py @@ -16,7 +16,9 @@ NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"} def save_xml_with_bom(tree, path): """Save XML tree to file with UTF-8 BOM.""" xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") - xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + xml_bytes = xml_bytes.replace(b"", b'') + if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) diff --git a/.claude/skills/help-add/scripts/add-help.py b/.claude/skills/help-add/scripts/add-help.py index c441cd79..03567724 100644 --- a/.claude/skills/help-add/scripts/add-help.py +++ b/.claude/skills/help-add/scripts/add-help.py @@ -14,7 +14,9 @@ NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"} def save_xml_with_bom(tree, path): """Save XML tree to file with UTF-8 BOM.""" xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") - xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + xml_bytes = xml_bytes.replace(b"", b'') + if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) diff --git a/.claude/skills/interface-edit/scripts/interface-edit.py b/.claude/skills/interface-edit/scripts/interface-edit.py index 927f213c..fbd2050f 100644 --- a/.claude/skills/interface-edit/scripts/interface-edit.py +++ b/.claude/skills/interface-edit/scripts/interface-edit.py @@ -95,7 +95,9 @@ def parse_value_list(val): def save_xml_bom(tree, path): xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") - xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + xml_bytes = xml_bytes.replace(b"", b'') + if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py index 4abbf5ca..df777898 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.py +++ b/.claude/skills/meta-compile/scripts/meta-compile.py @@ -2340,7 +2340,7 @@ if obj_type == 'WebService': X(f'\t') X('') -metadata_xml = '\n'.join(lines) +metadata_xml = '\n'.join(lines) + '\n' # --------------------------------------------------------------------------- # 16. Write files @@ -2502,9 +2502,13 @@ if os.path.isfile(config_xml_path): # Write back preserving BOM tree.write(config_xml_path, encoding='utf-8', xml_declaration=True) - # Re-read to add BOM + # Re-read to add BOM, fix declaration quotes, ensure trailing newline with open(config_xml_path, 'r', encoding='utf-8') as f: raw = f.read() + if raw.startswith(""): + raw = raw.replace("", '', 1) + if not raw.endswith('\n'): + raw += '\n' write_utf8_bom(config_xml_path, raw) reg_result = 'added' else: diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py index 0894323f..c5a3f363 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.py +++ b/.claude/skills/meta-edit/scripts/meta-edit.py @@ -1997,8 +1997,8 @@ def set_complex_property(property_name, values): def save_xml(tree, path): """Save XML tree with BOM and proper encoding declaration.""" xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") - # Fix encoding quotes: encoding='UTF-8' -> encoding="UTF-8" - xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + # Fix XML declaration quotes + xml_bytes = xml_bytes.replace(b"", b'') # Fix d5p1 namespace declarations stripped by lxml (it treats them as unused # because d5p1: appears only in text content, not in element/attribute names) xml_bytes = re.sub( @@ -2006,6 +2006,8 @@ def save_xml(tree, path): b'\\1 xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config"\\2', xml_bytes ) + if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) diff --git a/.claude/skills/meta-remove/scripts/meta-remove.py b/.claude/skills/meta-remove/scripts/meta-remove.py index 7d917468..dc9eca1d 100644 --- a/.claude/skills/meta-remove/scripts/meta-remove.py +++ b/.claude/skills/meta-remove/scripts/meta-remove.py @@ -99,7 +99,9 @@ def localname(el): def save_xml_bom(tree, path): xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") - xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + xml_bytes = xml_bytes.replace(b"", b'') + if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) diff --git a/.claude/skills/skd-edit/scripts/skd-edit.py b/.claude/skills/skd-edit/scripts/skd-edit.py index d82586c2..e1b6e1c0 100644 --- a/.claude/skills/skd-edit/scripts/skd-edit.py +++ b/.claude/skills/skd-edit/scripts/skd-edit.py @@ -1919,7 +1919,9 @@ elif operation == "remove-filter": # ── 9. Save ───────────────────────────────────────────────── xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") -xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') +xml_bytes = xml_bytes.replace(b"", b'') +if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(resolved_path, "wb") as f: f.write(b'\xef\xbb\xbf') f.write(xml_bytes) diff --git a/.claude/skills/subsystem-edit/scripts/subsystem-edit.py b/.claude/skills/subsystem-edit/scripts/subsystem-edit.py index bcc4f079..36158da3 100644 --- a/.claude/skills/subsystem-edit/scripts/subsystem-edit.py +++ b/.claude/skills/subsystem-edit/scripts/subsystem-edit.py @@ -122,7 +122,9 @@ def parse_value_list(val): def save_xml_bom(tree, path): xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") - xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + xml_bytes = xml_bytes.replace(b"", b'') + if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) diff --git a/.claude/skills/template-add/scripts/add-template.py b/.claude/skills/template-add/scripts/add-template.py index 462d1924..096981cf 100644 --- a/.claude/skills/template-add/scripts/add-template.py +++ b/.claude/skills/template-add/scripts/add-template.py @@ -23,7 +23,9 @@ TYPE_MAP = { def save_xml_with_bom(tree, path): """Save XML tree to file with UTF-8 BOM.""" xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") - xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + xml_bytes = xml_bytes.replace(b"", b'') + if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) diff --git a/.claude/skills/template-remove/scripts/remove-template.py b/.claude/skills/template-remove/scripts/remove-template.py index bd241b0c..5a94a046 100644 --- a/.claude/skills/template-remove/scripts/remove-template.py +++ b/.claude/skills/template-remove/scripts/remove-template.py @@ -16,7 +16,9 @@ NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"} def save_xml_with_bom(tree, path): """Save XML tree to file with UTF-8 BOM.""" xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") - xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + xml_bytes = xml_bytes.replace(b"", b'') + if not xml_bytes.endswith(b"\n"): + xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index 9faab96a..a1726818 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -255,11 +255,37 @@ function buildArgs(skillConfig, caseData, workDir, inputFilePath, runtime) { const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi; +function normalizeXmlContent(text) { + let s = text; + // 1. XML declaration: normalize quotes and encoding case + s = s.replace( + /<\?xml\s+version=['"]1\.0['"]\s+encoding=['"]([^'"]+)['"]\s*\?>/gi, + (_, enc) => `` + ); + // 2. Remove (CR encoded as XML entity by Python etree) + s = s.replace(/ /g, ''); + // 3. Strip xmlns declarations (Python etree strips unused ones) + s = s.replace(/\s+xmlns(?::[\w]+)?="[^"]*"/g, ''); + // 4. Normalize self-closing tags: remove space before /> + s = s.replace(/\s*\/>/g, '/>'); + // 5. Collapse whitespace between tags: "> \n\t <" → "><" + s = s.replace(/>\s+<'); + // 6. Normalize empty elements: + s = s.replace(/<([\w:.]+)([^>]*)><\/\1>/g, '<$1$2/>'); + // 7. Strip trailing whitespace + s = s.trimEnd(); + return s; +} + function normalizeContent(text, config) { // Strip BOM let s = text.replace(/^\uFEFF/, ''); // Normalize line endings s = s.replace(/\r\n/g, '\n'); + // Normalize XML differences (Python etree serialization quirks) + if (config?.runtime === 'python') { + s = normalizeXmlContent(s); + } // Normalize UUIDs if (config?.normalizeUuids) { @@ -453,7 +479,7 @@ async function runCaseAsync(testCase, opts) { } } if (errors.length === 0 && !caseData.expectError && !workspace.readOnly) { - const snapshotConfig = skillConfig.snapshot || {}; + const snapshotConfig = { ...skillConfig.snapshot, runtime: opts.runtime }; if (opts.updateSnapshots) { updateSnapshot(workDir, snapshotDir, snapshotConfig); } else { @@ -599,7 +625,7 @@ function runCase(testCase, opts) { // Snapshot comparison (skip for external/read-only workspaces) if (errors.length === 0 && !caseData.expectError && !workspace.readOnly) { - const snapshotConfig = skillConfig.snapshot || {}; + const snapshotConfig = { ...skillConfig.snapshot, runtime: opts.runtime }; if (opts.updateSnapshots) { updateSnapshot(workDir, snapshotDir, snapshotConfig); } else {