test(8 навыков): байтовые проверки канона там, где их не было (#57)

Из ~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="" — получалась
   пустая пара <DefaultObjectForm></DefaultObjectForm>. Платформа пустых пар
   не пишет (0 на 476 942 XML). Теперь IsEmpty / text=None.

2. Опустевший <ChildObjects> оставался парой, разнесённой по строкам, —
   в PS-порту form-remove и template-remove. Корпус: 1394 самозакрывающихся
   <ChildObjects/> на acc+erp, пустых пар 0 в обеих формах.

3. template-add писал пустой макет парой <SpreadsheetDocument></...>.
   Во всей выгрузке acc_8.3.27 (65 040 XML) многострочных пустых пар нет
   ни для одного тега.

Заодно усилена сама проверка noEmptyPairs: она ловила только СМЕЖНЫЕ теги,
поэтому дефект №2 проходил мимо неё. Добавлен вариант с переводом строки
внутри; дискриминатором служит сам перевод строки — значащий пробельный
текст-узел (<xr:FillValue xsi:type="xs:string">   </xr:FillValue>) его не
содержит и под проверку не попадает.

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) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-06 13:05:27 +03:00
co-authored by Claude Opus 5
parent a3f2a18dbf
commit a7406d0982
20 changed files with 141 additions and 31 deletions
+14 -2
View File
@@ -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) || [];
// Плюс пара, разнесённая по строкам: опустевший контейнер выглядит как
// `<ChildObjects>\n\t\t</ChildObjects>` и смежной проверкой НЕ ловился — так
// прошёл незамеченным дефект form-remove/template-remove. Платформа пишет
// только `<ChildObjects/>` (1394 на acc+erp, пустых пар 0 в обеих формах).
// Дискриминатор — перевод строки внутри: значащий пробельный текст-узел
// (`<xr:FillValue xsi:type="xs:string"> </xr:FillValue>`) его не содержит,
// поэтому под проверку не попадает.
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;
}