diff --git a/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/КолонкиСТакимЖеРазмером.xml b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/КолонкиСТакимЖеРазмером.xml
new file mode 100644
index 00000000..e017eec5
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/КолонкиСТакимЖеРазмером.xml
@@ -0,0 +1,96 @@
+
+
+
+ ru
+ ru
+
+ ru
+ Русский
+ Русский
+
+
+ en
+ Английский
+ Английский
+
+
+
+ 1
+
+ 0
+
+ 1
+
+
+
+
+ eb5ee63c-905d-4d8b-ab55-dc5ec80c067a
+ 2
+
+ 0
+
+ 1
+
+
+
+ 1
+
+ 1
+
+
+
+
+ 0
+
+
+
+ 0
+
+
+ ru
+ Ширина 20
+
+
+
+
+
+
+
+ 1
+
+ eb5ee63c-905d-4d8b-ab55-dc5ec80c067a
+
+
+ 0
+
+
+ ru
+ Ширина 20
+
+
+
+
+
+
+ 0
+
+
+ ru
+ Ширина 20
+
+
+
+
+
+
+ true
+ 2
+ 2
+ 2
+
+ 160
+
+
+ 72
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/МакетСоскрытойСтрокойИСтрокойСЗаданнымШрифтом.xml b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/МакетСоскрытойСтрокойИСтрокойСЗаданнымШрифтом.xml
new file mode 100644
index 00000000..2faddddc
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/МакетСоскрытойСтрокойИСтрокойСЗаданнымШрифтом.xml
@@ -0,0 +1,126 @@
+
+
+
+ ru
+ ru
+
+ ru
+ Русский
+ Русский
+
+
+ en
+ Английский
+ Английский
+
+
+
+ 1
+
+
+ 0
+
+
+
+ 0
+
+
+ ru
+ 1 строка
+
+
+
+
+
+
+
+ 1
+
+ 1
+
+
+ 0
+
+
+ ru
+ 2 скрыта
+
+
+
+
+
+
+
+ 2
+
+
+
+ 0
+
+
+ ru
+ 3 строка
+
+
+
+
+
+
+
+ 3
+
+ true
+
+
+
+ 4
+
+
+
+ 2
+
+
+ ru
+ задан полужирный шрифт
+
+
+
+
+
+
+
+ 5
+
+ 3
+
+
+ 3
+
+
+ ru
+ задан шрифт строки
+
+
+
+
+
+
+ true
+ 4
+ 6
+ 6
+
+
+
+ true
+
+
+ 0
+
+
+ 1
+
+
+ 72
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Простой2КолонкиШиринаПоУмолчанию.xml b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Простой2КолонкиШиринаПоУмолчанию.xml
new file mode 100644
index 00000000..98d85d21
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Простой2КолонкиШиринаПоУмолчанию.xml
@@ -0,0 +1,54 @@
+
+
+
+ ru
+ ru
+
+ ru
+ Русский
+ Русский
+
+
+ en
+ Английский
+ Английский
+
+
+
+ 2
+
+
+ 0
+
+
+
+ 0
+
+
+ ru
+ 1 (Ширина по умолчанию)
+
+
+
+
+
+
+ 0
+
+
+ ru
+ 2 (Ширина по умолчанию)
+
+
+
+
+
+
+ true
+ 1
+ 1
+ 1
+
+ 72
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Простой2Строки.xml b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Простой2Строки.xml
new file mode 100644
index 00000000..e037b2c0
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Простой2Строки.xml
@@ -0,0 +1,99 @@
+
+
+
+ ru
+ ru
+
+ ru
+ Русский
+ Русский
+
+
+ en
+ Английский
+ Английский
+
+
+
+ 2
+
+ 0
+
+ 1
+
+
+
+ 1
+
+ 2
+
+
+
+
+ 0
+
+
+
+ 0
+
+
+ ru
+ ширина 20
+
+
+
+
+
+
+ 0
+
+
+ ru
+ шинина 40
+
+
+
+
+
+
+
+ 1
+
+
+
+ 0
+
+
+ ru
+ 2. Ширина 20
+
+
+
+
+
+
+ 0
+
+
+ ru
+ 2. Ширина 40
+
+
+
+
+
+
+ true
+ 3
+ 2
+ 2
+
+ 160
+
+
+ 320
+
+
+ 72
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Простой3КолонкиСРазнойШириной.xml b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Простой3КолонкиСРазнойШириной.xml
new file mode 100644
index 00000000..604aa807
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Простой3КолонкиСРазнойШириной.xml
@@ -0,0 +1,92 @@
+
+
+
+ ru
+ ru
+
+ ru
+ Русский
+ Русский
+
+
+ en
+ Английский
+ Английский
+
+
+
+ 3
+
+ 0
+
+ 1
+
+
+
+ 1
+
+ 2
+
+
+
+ 2
+
+ 3
+
+
+
+
+ 0
+
+
+
+ 0
+
+
+ ru
+ Ширина 10
+
+
+
+
+
+
+ 0
+
+
+ ru
+ Ширина 20
+
+
+
+
+
+
+ 0
+
+
+ ru
+ Ширина 30
+
+
+
+
+
+
+ true
+ 4
+ 1
+ 1
+
+ 80
+
+
+ 160
+
+
+ 240
+
+
+ 72
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Простой3КолонкиСмешанный.xml b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Простой3КолонкиСмешанный.xml
new file mode 100644
index 00000000..2732bfff
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Простой3КолонкиСмешанный.xml
@@ -0,0 +1,83 @@
+
+
+
+ ru
+ ru
+
+ ru
+ Русский
+ Русский
+
+
+ en
+ Английский
+ Английский
+
+
+
+ 3
+
+ 0
+
+ 1
+
+
+
+ 2
+
+ 2
+
+
+
+
+ 0
+
+
+
+ 0
+
+
+ ru
+ Ширина 20
+
+
+
+
+
+
+ 0
+
+
+ ru
+ Ширина по умолчанию
+
+
+
+
+
+
+ 0
+
+
+ ru
+ Ширина 40
+
+
+
+
+
+
+ true
+ 3
+ 1
+ 1
+
+ 160
+
+
+ 320
+
+
+ 72
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/ЯчейкиСОформлением.xml b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/ЯчейкиСОформлением.xml
new file mode 100644
index 00000000..fc459a7a
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/ЯчейкиСОформлением.xml
@@ -0,0 +1,90 @@
+
+
+
+ ru
+ ru
+
+ ru
+ Русский
+ Русский
+
+
+ en
+ Английский
+ Английский
+
+
+
+ 1
+
+
+ 0
+
+
+
+ 1
+
+
+ ru
+ рамка
+
+
+
+
+
+
+
+ 1
+
+
+
+ 2
+
+
+ ru
+ курсив, красный
+
+
+
+
+
+
+
+ 2
+
+
+
+ 3
+
+
+ ru
+ Полужирный
+
+
+
+
+
+
+ true
+ 4
+ 3
+ 3
+
+ Solid
+
+
+
+
+ 0
+
+
+ 0
+ style:SpecialTextColor
+
+
+ 1
+
+
+ 72
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/platform-default-widths.json b/tests/skills/cases/mxl-compile/platform-default-widths.json
new file mode 100644
index 00000000..eac5b519
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/platform-default-widths.json
@@ -0,0 +1,25 @@
+{
+ "name": "Раундтрип макета ПЛАТФОРМЫ байт в байт: Простой2КолонкиШиринаПоУмолчанию",
+ "setup": "fixture:platform-roundtrip",
+ "preRun": [
+ {
+ "script": "mxl-decompile/scripts/mxl-decompile",
+ "args": {
+ "-TemplatePath": "Простой2КолонкиШиринаПоУмолчанию.xml",
+ "-OutputPath": "back.json"
+ },
+ "cwd": "{workDir}"
+ }
+ ],
+ "params": {
+ "outputPath": "out.xml"
+ },
+ "noSnapshot": "эталон — файл платформы, сверяется expect.filesEqual",
+ "expect": {
+ "filesEqual": {
+ "actual": "out.xml",
+ "expected": "Простой2КолонкиШиринаПоУмолчанию.xml"
+ }
+ },
+ "inputFrom": "back.json"
+}
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/platform-hidden-row-and-fonts.json b/tests/skills/cases/mxl-compile/platform-hidden-row-and-fonts.json
new file mode 100644
index 00000000..a942fa42
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/platform-hidden-row-and-fonts.json
@@ -0,0 +1,25 @@
+{
+ "name": "Раундтрип макета ПЛАТФОРМЫ байт в байт: МакетСоскрытойСтрокойИСтрокойСЗаданнымШрифтом",
+ "setup": "fixture:platform-roundtrip",
+ "preRun": [
+ {
+ "script": "mxl-decompile/scripts/mxl-decompile",
+ "args": {
+ "-TemplatePath": "МакетСоскрытойСтрокойИСтрокойСЗаданнымШрифтом.xml",
+ "-OutputPath": "back.json"
+ },
+ "cwd": "{workDir}"
+ }
+ ],
+ "params": {
+ "outputPath": "out.xml"
+ },
+ "noSnapshot": "эталон — файл платформы, сверяется expect.filesEqual",
+ "expect": {
+ "filesEqual": {
+ "actual": "out.xml",
+ "expected": "МакетСоскрытойСтрокойИСтрокойСЗаданнымШрифтом.xml"
+ }
+ },
+ "inputFrom": "back.json"
+}
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/platform-mixed.json b/tests/skills/cases/mxl-compile/platform-mixed.json
new file mode 100644
index 00000000..fc38bf07
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/platform-mixed.json
@@ -0,0 +1,25 @@
+{
+ "name": "Раундтрип макета ПЛАТФОРМЫ байт в байт: Простой3КолонкиСмешанный",
+ "setup": "fixture:platform-roundtrip",
+ "preRun": [
+ {
+ "script": "mxl-decompile/scripts/mxl-decompile",
+ "args": {
+ "-TemplatePath": "Простой3КолонкиСмешанный.xml",
+ "-OutputPath": "back.json"
+ },
+ "cwd": "{workDir}"
+ }
+ ],
+ "params": {
+ "outputPath": "out.xml"
+ },
+ "noSnapshot": "эталон — файл платформы, сверяется expect.filesEqual",
+ "expect": {
+ "filesEqual": {
+ "actual": "out.xml",
+ "expected": "Простой3КолонкиСмешанный.xml"
+ }
+ },
+ "inputFrom": "back.json"
+}
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/platform-same-width-columns.json b/tests/skills/cases/mxl-compile/platform-same-width-columns.json
new file mode 100644
index 00000000..f182937f
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/platform-same-width-columns.json
@@ -0,0 +1,25 @@
+{
+ "name": "Раундтрип макета ПЛАТФОРМЫ байт в байт: КолонкиСТакимЖеРазмером",
+ "setup": "fixture:platform-roundtrip",
+ "preRun": [
+ {
+ "script": "mxl-decompile/scripts/mxl-decompile",
+ "args": {
+ "-TemplatePath": "КолонкиСТакимЖеРазмером.xml",
+ "-OutputPath": "back.json"
+ },
+ "cwd": "{workDir}"
+ }
+ ],
+ "params": {
+ "outputPath": "out.xml"
+ },
+ "noSnapshot": "эталон — файл платформы, сверяется expect.filesEqual",
+ "expect": {
+ "filesEqual": {
+ "actual": "out.xml",
+ "expected": "КолонкиСТакимЖеРазмером.xml"
+ }
+ },
+ "inputFrom": "back.json"
+}
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/platform-styled-cells.json b/tests/skills/cases/mxl-compile/platform-styled-cells.json
new file mode 100644
index 00000000..b46fae39
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/platform-styled-cells.json
@@ -0,0 +1,25 @@
+{
+ "name": "Раундтрип макета ПЛАТФОРМЫ байт в байт: ЯчейкиСОформлением",
+ "setup": "fixture:platform-roundtrip",
+ "preRun": [
+ {
+ "script": "mxl-decompile/scripts/mxl-decompile",
+ "args": {
+ "-TemplatePath": "ЯчейкиСОформлением.xml",
+ "-OutputPath": "back.json"
+ },
+ "cwd": "{workDir}"
+ }
+ ],
+ "params": {
+ "outputPath": "out.xml"
+ },
+ "noSnapshot": "эталон — файл платформы, сверяется expect.filesEqual",
+ "expect": {
+ "filesEqual": {
+ "actual": "out.xml",
+ "expected": "ЯчейкиСОформлением.xml"
+ }
+ },
+ "inputFrom": "back.json"
+}
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/platform-three-widths.json b/tests/skills/cases/mxl-compile/platform-three-widths.json
new file mode 100644
index 00000000..6d85611f
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/platform-three-widths.json
@@ -0,0 +1,25 @@
+{
+ "name": "Раундтрип макета ПЛАТФОРМЫ байт в байт: Простой3КолонкиСРазнойШириной",
+ "setup": "fixture:platform-roundtrip",
+ "preRun": [
+ {
+ "script": "mxl-decompile/scripts/mxl-decompile",
+ "args": {
+ "-TemplatePath": "Простой3КолонкиСРазнойШириной.xml",
+ "-OutputPath": "back.json"
+ },
+ "cwd": "{workDir}"
+ }
+ ],
+ "params": {
+ "outputPath": "out.xml"
+ },
+ "noSnapshot": "эталон — файл платформы, сверяется expect.filesEqual",
+ "expect": {
+ "filesEqual": {
+ "actual": "out.xml",
+ "expected": "Простой3КолонкиСРазнойШириной.xml"
+ }
+ },
+ "inputFrom": "back.json"
+}
\ No newline at end of file
diff --git a/tests/skills/cases/mxl-compile/platform-two-rows.json b/tests/skills/cases/mxl-compile/platform-two-rows.json
new file mode 100644
index 00000000..0a55dff3
--- /dev/null
+++ b/tests/skills/cases/mxl-compile/platform-two-rows.json
@@ -0,0 +1,25 @@
+{
+ "name": "Раундтрип макета ПЛАТФОРМЫ байт в байт: Простой2Строки",
+ "setup": "fixture:platform-roundtrip",
+ "preRun": [
+ {
+ "script": "mxl-decompile/scripts/mxl-decompile",
+ "args": {
+ "-TemplatePath": "Простой2Строки.xml",
+ "-OutputPath": "back.json"
+ },
+ "cwd": "{workDir}"
+ }
+ ],
+ "params": {
+ "outputPath": "out.xml"
+ },
+ "noSnapshot": "эталон — файл платформы, сверяется expect.filesEqual",
+ "expect": {
+ "filesEqual": {
+ "actual": "out.xml",
+ "expected": "Простой2Строки.xml"
+ }
+ },
+ "inputFrom": "back.json"
+}
\ No newline at end of file
diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs
index a3b4f29a..860dd237 100644
--- a/tests/skills/runner.mjs
+++ b/tests/skills/runner.mjs
@@ -275,7 +275,10 @@ function buildArgs(skillConfig, caseData, workDir, inputFilePath, runtime) {
switch (mapping.from) {
case 'inputFile':
- args.push(inputFilePath);
+ // inputFrom: взять вход из файла в workDir, а не из case.input. Нужно, когда вход
+ // производит preRun (например, декомпилятор) — case.input пишется ПОСЛЕ preRun и
+ // затёр бы его.
+ args.push(caseData.inputFrom ? join(workDir, caseData.inputFrom) : inputFilePath);
break;
case 'workDir':
args.push(workDir);
@@ -407,7 +410,7 @@ function checkFileContains(workDir, spec, expectPresent) {
// поэтому он ошибка, а не игнор.
const KNOWN_EXPECT_KEYS = new Set([
'files', 'stdoutContains', 'stdoutNotContains', 'preserves',
- 'fileContains', 'fileNotContains',
+ 'fileContains', 'fileNotContains', 'filesEqual',
]);
function checkExpectKeys(caseData) {
@@ -416,6 +419,29 @@ function checkExpectKeys(caseData) {
return unknown.map(k => `expect.${k}: раннер такого ключа не знает — кейс ничего не проверяет`);
}
+// ─── Побайтовое равенство двух файлов ───────────────────────────────────────
+// Нужно там, где эталон — не наш снэпшот, а файл, произведённый ПЛАТФОРМОЙ: снэпшот
+// такую проверку не заменяет, потому что --update-snapshots молча принял бы дрейф.
+// spec: { actual, expected }. Пути относительно workDir.
+function checkFilesEqual(workDir, spec) {
+ const errs = [];
+ const a = join(workDir, spec.actual);
+ const b = join(workDir, spec.expected);
+ if (!existsSync(a)) { errs.push(`filesEqual: нет файла ${spec.actual}`); return errs; }
+ if (!existsSync(b)) { errs.push(`filesEqual: нет файла ${spec.expected}`); return errs; }
+ const bufA = readFileSync(a);
+ const bufB = readFileSync(b);
+ if (bufA.equals(bufB)) return errs;
+ const linesA = bufA.toString('utf8').split('\n');
+ const linesB = bufB.toString('utf8').split('\n');
+ let i = 0;
+ while (i < linesA.length && i < linesB.length && linesA[i] === linesB[i]) i++;
+ errs.push(`filesEqual: ${spec.actual} != ${spec.expected}, первое расхождение в строке ${i + 1}`
+ + `\n ожидалось: ${(linesB[i] ?? '<конец файла>').trim()}`
+ + `\n получено: ${(linesA[i] ?? '<конец файла>').trim()}`);
+ return errs;
+}
+
// ─── Byte-style preservation check (round-trip #44/#46/#47, канон #57) ──────
// Проверяет СЫРЫЕ байты файла (в обход normalizeContent): BOM / EOL / регистр
// encoding / финальный перенос / отсутствие
/ форма пустого элемента.
@@ -806,6 +832,11 @@ async function runCaseAsync(testCase, opts) {
? caseData.expect.preserves : [caseData.expect.preserves];
for (const spec of specs) errors.push(...checkPreserves(workDir, spec));
}
+ if (caseData.expect?.filesEqual) {
+ const specs = Array.isArray(caseData.expect.filesEqual)
+ ? caseData.expect.filesEqual : [caseData.expect.filesEqual];
+ for (const spec of specs) errors.push(...checkFilesEqual(workDir, spec));
+ }
if (caseData.expect?.fileContains) {
const specs = Array.isArray(caseData.expect.fileContains)
? caseData.expect.fileContains : [caseData.expect.fileContains];