From a7406d098290dd66bf744c4aaa5fc1f30a00f8ca Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 6 Aug 2026 13:05:27 +0300 Subject: [PATCH] =?UTF-8?q?test(8=20=D0=BD=D0=B0=D0=B2=D1=8B=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2):=20=D0=B1=D0=B0=D0=B9=D1=82=D0=BE=D0=B2=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=BD=D0=B0=20=D1=82=D0=B0=D0=BC,=20=D0=B3=D0=B4?= =?UTF-8?q?=D0=B5=20=D0=B8=D1=85=20=D0=BD=D0=B5=20=D0=B1=D1=8B=D0=BB=D0=BE?= =?UTF-8?q?=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Из ~46 навыков-эмиттеров XML восемь не имели ни одной проверки `preserves`: form-edit, form-remove, meta-remove, skd-edit, template-remove, xdto-edit, support-edit, cfe-patch-method. Шесть из них правились в предыдущем коммите, и канон у них держался на том, что правка была механической, а не на проверке. Ожидания писались ПО КАНОНУ, а не по текущему поведению — и это сразу вскрыло три дефекта: 1. form-remove очищал слот формы через InnerText="" / text="" — получалась пустая пара . Платформа пустых пар не пишет (0 на 476 942 XML). Теперь IsEmpty / text=None. 2. Опустевший оставался парой, разнесённой по строкам, — в PS-порту form-remove и template-remove. Корпус: 1394 самозакрывающихся на acc+erp, пустых пар 0 в обеих формах. 3. template-add писал пустой макет парой . Во всей выгрузке acc_8.3.27 (65 040 XML) многострочных пустых пар нет ни для одного тега. Заодно усилена сама проверка noEmptyPairs: она ловила только СМЕЖНЫЕ теги, поэтому дефект №2 проходил мимо неё. Добавлен вариант с переводом строки внутри; дискриминатором служит сам перевод строки — значащий пробельный текст-узел ( ) его не содержит и под проверку не попадает. support-edit покрыт частично (BOM у ParentConfigurations.bin — проверено, что платформа пишет его с BOM во всех трёх выгрузках), cfe-patch-method — BOM+EOL у .bsl: хвостовой перевод строки у модулей неканоничен (1235 с ним, 766 без), поэтому не утверждается. Регресс: 647/647 ps1, 644/647 py (3 skipped). Эталоны переснятые: три места, каждое — ровно ожидаемая пара строк. Co-Authored-By: Claude Opus 5 (1M context) --- .../skills/form-remove/scripts/remove-form.ps1 | 10 ++++++++-- .../skills/form-remove/scripts/remove-form.py | 10 ++++++++-- .../skills/template-add/scripts/add-template.ps1 | 8 +++++--- .../skills/template-add/scripts/add-template.py | 8 +++++--- .../template-remove/scripts/remove-template.ps1 | 6 +++++- .../template-remove/scripts/remove-template.py | 6 +++++- .../cases/cfe-patch-method/before-handler.json | 9 ++++++++- .../Templates/Макет/Ext/Template.xml | 3 +-- tests/skills/cases/form-edit/add-attribute.json | 12 ++++++++++++ tests/skills/cases/form-remove/basic.json | 12 +++++++++++- .../snapshots/basic/Catalogs/Товары.xml | 5 ++--- tests/skills/cases/meta-remove/basic.json | 14 +++++++++++++- tests/skills/cases/skd-edit/add-field.json | 12 ++++++++++++ .../skills/cases/support-edit/set-editable.json | 6 +++++- .../Templates/Макет/Ext/Template.xml | 3 +-- tests/skills/cases/template-remove/basic.json | 14 +++++++++++++- .../snapshots/basic/МояОбработка.xml | 3 +-- .../snapshots/remove-from-report/МойОтчёт.xml | 3 +-- tests/skills/cases/xdto-edit/set-synonym.json | 12 +++++++++++- tests/skills/runner.mjs | 16 ++++++++++++++-- 20 files changed, 141 insertions(+), 31 deletions(-) diff --git a/.claude/skills/form-remove/scripts/remove-form.ps1 b/.claude/skills/form-remove/scripts/remove-form.ps1 index b3dd0673..3795e04b 100644 --- a/.claude/skills/form-remove/scripts/remove-form.ps1 +++ b/.claude/skills/form-remove/scripts/remove-form.ps1 @@ -1,4 +1,4 @@ -# form-remove v1.7 — Remove form from 1C object +# form-remove v1.8 — Remove form from 1C object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -64,6 +64,10 @@ foreach ($node in $formNodes) { $parent.RemoveChild($prev) | Out-Null } $parent.RemoveChild($node) | Out-Null + # Опустевший контейнер: остаётся отступ-whitespace, и XmlWriter пишет пару + # \n\t\t. Платформа пишет только + # (1394 самозакрывающихся на acc+erp, пустых пар ни в одной форме — 0). + if ($parent.SelectNodes("*").Count -eq 0) { $parent.IsEmpty = $true } break } } @@ -74,7 +78,9 @@ foreach ($node in $formNodes) { $formRefRe = "Form\.$([regex]::Escape($FormName))$" foreach ($node in $xmlDoc.SelectNodes("//md:*", $nsMgr)) { if ($node.LocalName -like "*Form" -and $node.InnerText -and $node.InnerText -match $formRefRe) { - $node.InnerText = "" + # IsEmpty, а не InnerText="": пустая строка сериализуется парой , а + # Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен . + $node.IsEmpty = $true } } diff --git a/.claude/skills/form-remove/scripts/remove-form.py b/.claude/skills/form-remove/scripts/remove-form.py index 924f28b3..a433f25b 100644 --- a/.claude/skills/form-remove/scripts/remove-form.py +++ b/.claude/skills/form-remove/scripts/remove-form.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# form-remove v1.7 — Remove form from 1C object +# form-remove v1.8 — Remove form from 1C object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -120,6 +120,10 @@ def main(): if parent.text and parent.text.strip() == "": parent.text = "" parent.remove(node) + # Опустевший контейнер: text="" сериализуется парой , + # а нужен — PS-порт через DOM даёт именно его. + if len(parent) == 0 and not (parent.text or "").strip(): + parent.text = None break # Clear any Default*/Auxiliary* form slot that pointed to the removed form @@ -130,7 +134,9 @@ def main(): if not isinstance(el.tag, str): continue if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text): - el.text = "" + # text=None, а не "": пустая строка сериализуется парой , а + # Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен . + el.text = None # Save with BOM save_xml_with_bom(tree, root_xml_full) diff --git a/.claude/skills/template-add/scripts/add-template.ps1 b/.claude/skills/template-add/scripts/add-template.ps1 index 44b015d9..5a6fbb19 100644 --- a/.claude/skills/template-add/scripts/add-template.ps1 +++ b/.claude/skills/template-add/scripts/add-template.ps1 @@ -1,4 +1,4 @@ -# template-add v1.17 — Add template to 1C object +# template-add v1.18 — Add template to 1C object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -296,10 +296,12 @@ switch ($TemplateType) { [System.IO.File]::WriteAllText($templateFilePath, "", $encBom) } "SpreadsheetDocument" { + # Пустой макет — самозакрывающимся корнем: пустых пар платформа не пишет + # ни в одной форме (0 на 65 040 XML выгрузки acc_8.3.27, включая разнесённые + # по строкам). Для XML `` и `` тождественны по спецификации. $content = @" - - + "@ Write-XmlFile $templateFilePath $content $encBom } diff --git a/.claude/skills/template-add/scripts/add-template.py b/.claude/skills/template-add/scripts/add-template.py index ec874598..639b69d5 100644 --- a/.claude/skills/template-add/scripts/add-template.py +++ b/.claude/skills/template-add/scripts/add-template.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# template-add v1.17 — Add template to 1C object +# template-add v1.18 — Add template to 1C object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -418,13 +418,15 @@ def main(): write_text_with_bom(template_file_path, "") elif template_type == "SpreadsheetDocument": + # Пустой макет — самозакрывающимся корнем: пустых пар платформа не пишет + # ни в одной форме (0 на 65 040 XML выгрузки acc_8.3.27, включая разнесённые + # по строкам). Для XML `` и `` тождественны по спецификации. content = ( '\n' '\n' - '' + ' xmlns:xs="http://www.w3.org/2001/XMLSchema"/>' ) write_xml_file(template_file_path, content) diff --git a/.claude/skills/template-remove/scripts/remove-template.ps1 b/.claude/skills/template-remove/scripts/remove-template.ps1 index 02543908..721fa4b6 100644 --- a/.claude/skills/template-remove/scripts/remove-template.ps1 +++ b/.claude/skills/template-remove/scripts/remove-template.ps1 @@ -1,4 +1,4 @@ -# template-remove v1.6 — Remove template from 1C object +# template-remove v1.7 — Remove template from 1C object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -64,6 +64,10 @@ foreach ($node in $templateNodes) { $parent.RemoveChild($prev) | Out-Null } $parent.RemoveChild($node) | Out-Null + # Опустевший контейнер: остаётся отступ-whitespace, и XmlWriter пишет пару + # \n\t\t. Платформа пишет только + # (1394 самозакрывающихся на acc+erp, пустых пар ни в одной форме — 0). + if ($parent.SelectNodes("*").Count -eq 0) { $parent.IsEmpty = $true } break } } diff --git a/.claude/skills/template-remove/scripts/remove-template.py b/.claude/skills/template-remove/scripts/remove-template.py index ba1212e8..5b910fc5 100644 --- a/.claude/skills/template-remove/scripts/remove-template.py +++ b/.claude/skills/template-remove/scripts/remove-template.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# template-remove v1.6 — Remove template from 1C object +# template-remove v1.7 — Remove template from 1C object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -120,6 +120,10 @@ def main(): if parent.text and parent.text.strip() == "": parent.text = "" parent.remove(node) + # Опустевший контейнер: text="" сериализуется парой , + # а нужен — PS-порт через DOM даёт именно его. + if len(parent) == 0 and not (parent.text or "").strip(): + parent.text = None break # Clear MainDataCompositionSchema if it pointed to this template diff --git a/tests/skills/cases/cfe-patch-method/before-handler.json b/tests/skills/cases/cfe-patch-method/before-handler.json index b69e31a2..80a7dcd3 100644 --- a/tests/skills/cases/cfe-patch-method/before-handler.json +++ b/tests/skills/cases/cfe-patch-method/before-handler.json @@ -27,5 +27,12 @@ "methodName": "ПриЗаписи", "interceptorType": "Before" }, - "expect": { "stdoutContains": "&Перед(\"ПриЗаписи\")" } + "expect": { + "stdoutContains": "&Перед(\"ПриЗаписи\")", + "preserves": { + "file": "Ext/Catalogs/Товары/Ext/ObjectModule.bsl", + "bom": true, + "eol": "crlf" + } + } } diff --git a/tests/skills/cases/epf-validate/snapshots/valid-with-template/МояОбработка/Templates/Макет/Ext/Template.xml b/tests/skills/cases/epf-validate/snapshots/valid-with-template/МояОбработка/Templates/Макет/Ext/Template.xml index fe6c3ab9..d16dc67b 100644 --- a/tests/skills/cases/epf-validate/snapshots/valid-with-template/МояОбработка/Templates/Макет/Ext/Template.xml +++ b/tests/skills/cases/epf-validate/snapshots/valid-with-template/МояОбработка/Templates/Макет/Ext/Template.xml @@ -1,3 +1,2 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/tests/skills/cases/form-edit/add-attribute.json b/tests/skills/cases/form-edit/add-attribute.json index 42df59f7..d95973ac 100644 --- a/tests/skills/cases/form-edit/add-attribute.json +++ b/tests/skills/cases/form-edit/add-attribute.json @@ -25,5 +25,17 @@ { "name": "СуммаИтого", "type": "decimal(15,2)" }, { "name": "ДатаНачала", "type": "date" } ] + }, + "expect": { + "preserves": { + "file": "DataProcessors/Реквизиты/Forms/Форма/Ext/Form.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/cases/form-remove/basic.json b/tests/skills/cases/form-remove/basic.json index cb1c1505..7d7bc8b4 100644 --- a/tests/skills/cases/form-remove/basic.json +++ b/tests/skills/cases/form-remove/basic.json @@ -14,6 +14,16 @@ "objectName": "Catalogs/Товары", "formName": "ФормаЭлемента", "expect": { - "stdoutContains": "Форма ФормаЭлемента удалена" + "stdoutContains": "Форма ФормаЭлемента удалена", + "preserves": { + "file": "Catalogs/Товары.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/cases/form-remove/snapshots/basic/Catalogs/Товары.xml b/tests/skills/cases/form-remove/snapshots/basic/Catalogs/Товары.xml index 5742f3a0..d56cd7a0 100644 --- a/tests/skills/cases/form-remove/snapshots/basic/Catalogs/Товары.xml +++ b/tests/skills/cases/form-remove/snapshots/basic/Catalogs/Товары.xml @@ -60,7 +60,7 @@ Begin DontUse Directly - + @@ -86,7 +86,6 @@ false false - - + \ No newline at end of file diff --git a/tests/skills/cases/meta-remove/basic.json b/tests/skills/cases/meta-remove/basic.json index 4bc80a70..34d8e4ad 100644 --- a/tests/skills/cases/meta-remove/basic.json +++ b/tests/skills/cases/meta-remove/basic.json @@ -8,5 +8,17 @@ } ], "params": { "object": "Catalog.Удалить" }, - "args_extra": ["-Force"] + "args_extra": ["-Force"], + "expect": { + "preserves": { + "file": "Configuration.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } + } } diff --git a/tests/skills/cases/skd-edit/add-field.json b/tests/skills/cases/skd-edit/add-field.json index 65b1a479..ce78d118 100644 --- a/tests/skills/cases/skd-edit/add-field.json +++ b/tests/skills/cases/skd-edit/add-field.json @@ -17,5 +17,17 @@ "templatePath": "Template.xml", "operation": "add-field", "value": "Цена: decimal(15,2)" + }, + "expect": { + "preserves": { + "file": "Template.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/cases/support-edit/set-editable.json b/tests/skills/cases/support-edit/set-editable.json index 23961e4d..319c7549 100644 --- a/tests/skills/cases/support-edit/set-editable.json +++ b/tests/skills/cases/support-edit/set-editable.json @@ -9,6 +9,10 @@ "editable" ], "expect": { - "stdoutContains": "редактируется с сохранением поддержки" + "stdoutContains": "редактируется с сохранением поддержки", + "preserves": { + "file": "Ext/ParentConfigurations.bin", + "bom": true + } } } diff --git a/tests/skills/cases/template-add/snapshots/basic/МояОбработка/Templates/Макет/Ext/Template.xml b/tests/skills/cases/template-add/snapshots/basic/МояОбработка/Templates/Макет/Ext/Template.xml index fe6c3ab9..d16dc67b 100644 --- a/tests/skills/cases/template-add/snapshots/basic/МояОбработка/Templates/Макет/Ext/Template.xml +++ b/tests/skills/cases/template-add/snapshots/basic/МояОбработка/Templates/Макет/Ext/Template.xml @@ -1,3 +1,2 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/tests/skills/cases/template-remove/basic.json b/tests/skills/cases/template-remove/basic.json index f77f76a1..7df0f78c 100644 --- a/tests/skills/cases/template-remove/basic.json +++ b/tests/skills/cases/template-remove/basic.json @@ -10,5 +10,17 @@ "args": { "-ObjectName": "МояОбработка", "-TemplateName": "Макет", "-TemplateType": "SpreadsheetDocument", "-SrcDir": "{workDir}" } } ], - "params": { "objectName": "МояОбработка", "templateName": "Макет" } + "params": { "objectName": "МояОбработка", "templateName": "Макет" }, + "expect": { + "preserves": { + "file": "МояОбработка.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } + } } diff --git a/tests/skills/cases/template-remove/snapshots/basic/МояОбработка.xml b/tests/skills/cases/template-remove/snapshots/basic/МояОбработка.xml index 1d067969..7074d760 100644 --- a/tests/skills/cases/template-remove/snapshots/basic/МояОбработка.xml +++ b/tests/skills/cases/template-remove/snapshots/basic/МояОбработка.xml @@ -23,7 +23,6 @@ - - + \ No newline at end of file diff --git a/tests/skills/cases/template-remove/snapshots/remove-from-report/МойОтчёт.xml b/tests/skills/cases/template-remove/snapshots/remove-from-report/МойОтчёт.xml index a205f8eb..cfd5023f 100644 --- a/tests/skills/cases/template-remove/snapshots/remove-from-report/МойОтчёт.xml +++ b/tests/skills/cases/template-remove/snapshots/remove-from-report/МойОтчёт.xml @@ -29,7 +29,6 @@ - - + \ No newline at end of file diff --git a/tests/skills/cases/xdto-edit/set-synonym.json b/tests/skills/cases/xdto-edit/set-synonym.json index bdf9fea8..953c7227 100644 --- a/tests/skills/cases/xdto-edit/set-synonym.json +++ b/tests/skills/cases/xdto-edit/set-synonym.json @@ -25,6 +25,16 @@ "expect": { "files": [ "XDTOPackages/types.xml" - ] + ], + "preserves": { + "file": "XDTOPackages/types.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index f1e716bb..6256b073 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -404,8 +404,20 @@ function checkPreserves(workDir, spec) { if (spaced) errs.push(`preserves: expected tight self-closing, got ${spaced.length}× spaced (e.g. ${spaced[0].slice(0, 60)})`); } if (spec.noEmptyPairs) { - const pairs = text.match(/<([\w:.]+)([^<>]*)><\/\1>/g); - if (pairs) errs.push(`preserves: expected self-closing, got ${pairs.length}× empty pair (e.g. ${pairs[0].slice(0, 60)})`); + const pairs = text.match(/<([\w:.]+)([^<>]*)><\/\1>/g) || []; + // Плюс пара, разнесённая по строкам: опустевший контейнер выглядит как + // `\n\t\t` и смежной проверкой НЕ ловился — так + // прошёл незамеченным дефект form-remove/template-remove. Платформа пишет + // только `` (1394 на acc+erp, пустых пар 0 в обеих формах). + // Дискриминатор — перевод строки внутри: значащий пробельный текст-узел + // (` `) его не содержит, + // поэтому под проверку не попадает. + const multiline = text.match(/<([\w:.]+)([^<>]*)>[ \t]*\r?\n\s*<\/\1>/g) || []; + const all = [...pairs, ...multiline]; + if (all.length) { + const sample = all[0].replace(/\s+/g, ' ').slice(0, 60); + errs.push(`preserves: expected self-closing, got ${all.length}× empty pair (e.g. ${sample})`); + } } return errs; }