From 7a8f437e77047371ab3ef79183af98fc9c3b7bc5 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 11 Apr 2026 19:15:07 +0300 Subject: [PATCH] test(subsystem-compile): cover bottom-up -Parent flow; hide children shortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bottom-up flow (compile child with -Parent pointing at parent's XML — skill creates the real child file AND registers it in parent's ChildObjects) has been the documented canonical way to build nested subsystems since forever. It's in SKILL.md Примеры:58 and implemented in subsystem-compile.ps1:430-506. But zero test cases exercised it — all 7 pre-existing cases used the top-down `children: [...]` shortcut that aa93031 made honest with stubs. Two problems with the status quo: 1. A model reading SKILL.md saw `"children": ["ДочерняяА", "ДочерняяБ"]` right in the main JSON-definition example and took it as the canonical way to create nested structure. It's a trap — the shortcut creates placeholder stubs with empty Synonym/Content that the model almost never actually wants. The natural flow (one subsystem-compile call per real subsystem) wasn't visible where the model looks first. 2. The canonical flow had no test safety net — nothing caught regressions in the register-in-parent code path (lines 430-506). Fix, minimal surface: - SKILL.md: remove `"children": [...]` from the JSON-definition example. Leave the `-Parent` example in the Примеры section (already there). The children field stays fully supported in the scripts (aa93031 stub behavior unchanged) for legacy JSON — just not advertised. - New test case `nested-parent.json`: preRun compiles "Продажи" parent, main run compiles "Настройки" child with `-Parent Subsystems/Продажи.xml`. Verifies the real bottom-up flow: snapshot shows full child file with real Synonym/Explanation AND parent's `` updated to reference the child. verify-snapshots confirms platform accepts it. - Runner plumbing: `_skill.json` gains `{ "flag": "-Parent", "from": "workPath", "field": "parent", "optional": true }`. Required extending both `tests/skills/runner.mjs` and `tests/skills/verify-snapshots.mjs` (they each have their own copy of buildArgs) to support `optional: true` on workPath mappings — otherwise existing cases without params.parent would get the flag pushed with an empty value. Verification: - runner --filter subsystem-compile (PS1): 8/8 (was 7/7 +1) - runner --filter subsystem-compile --runtime python: 8/8 (dual-port clean) - verify-snapshots --skill subsystem-compile: 8/8 Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/subsystem-compile/SKILL.md | 3 +- .../cases/subsystem-compile/_skill.json | 3 +- .../subsystem-compile/nested-parent.json | 24 ++ .../snapshots/nested-parent/Configuration.xml | 252 ++++++++++++++++++ .../nested-parent/Languages/Русский.xml | 16 ++ .../nested-parent/Subsystems/Продажи.xml | 24 ++ .../Продажи/Subsystems/Настройки.xml | 27 ++ tests/skills/runner.mjs | 12 +- tests/skills/verify-snapshots.mjs | 12 +- 9 files changed, 366 insertions(+), 7 deletions(-) create mode 100644 tests/skills/cases/subsystem-compile/nested-parent.json create mode 100644 tests/skills/cases/subsystem-compile/snapshots/nested-parent/Configuration.xml create mode 100644 tests/skills/cases/subsystem-compile/snapshots/nested-parent/Languages/Русский.xml create mode 100644 tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи.xml create mode 100644 tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи/Subsystems/Настройки.xml diff --git a/.claude/skills/subsystem-compile/SKILL.md b/.claude/skills/subsystem-compile/SKILL.md index ce9d4af5..8923a1ff 100644 --- a/.claude/skills/subsystem-compile/SKILL.md +++ b/.claude/skills/subsystem-compile/SKILL.md @@ -38,8 +38,7 @@ powershell.exe -NoProfile -File '.claude/skills/subsystem-compile/scripts/subsys "useOneCommand": false, "explanation": "Описание раздела", "picture": "CommonPicture.МояКартинка", - "content": ["Catalog.Товары", "Document.Заказ"], - "children": ["ДочерняяА", "ДочерняяБ"] + "content": ["Catalog.Товары", "Document.Заказ"] } ``` diff --git a/tests/skills/cases/subsystem-compile/_skill.json b/tests/skills/cases/subsystem-compile/_skill.json index 5c9bf7a8..5db9ab8c 100644 --- a/tests/skills/cases/subsystem-compile/_skill.json +++ b/tests/skills/cases/subsystem-compile/_skill.json @@ -3,7 +3,8 @@ "setup": "empty-config", "args": [ { "flag": "-DefinitionFile", "from": "inputFile" }, - { "flag": "-OutputDir", "from": "workDir" } + { "flag": "-OutputDir", "from": "workDir" }, + { "flag": "-Parent", "from": "workPath", "field": "parent", "optional": true } ], "snapshot": { "root": "workDir", diff --git a/tests/skills/cases/subsystem-compile/nested-parent.json b/tests/skills/cases/subsystem-compile/nested-parent.json new file mode 100644 index 00000000..d64e2d27 --- /dev/null +++ b/tests/skills/cases/subsystem-compile/nested-parent.json @@ -0,0 +1,24 @@ +{ + "name": "Вложенная подсистема через -Parent (bottom-up flow)", + "preRun": [ + { + "script": "subsystem-compile/scripts/subsystem-compile", + "input": { "name": "Продажи", "synonym": "Продажи" }, + "args": { "-DefinitionFile": "{inputFile}", "-OutputDir": "{workDir}" } + } + ], + "params": { "parent": "Subsystems/Продажи.xml" }, + "input": { + "name": "Настройки", + "synonym": "Настройки раздела", + "explanation": "Настройки подсистемы продаж", + "includeInCommandInterface": true + }, + "validatePath": "Subsystems/Продажи/Subsystems/Настройки", + "expect": { + "files": [ + "Subsystems/Продажи.xml", + "Subsystems/Продажи/Subsystems/Настройки.xml" + ] + } +} diff --git a/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Configuration.xml b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Configuration.xml new file mode 100644 index 00000000..2e6aa527 --- /dev/null +++ b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Configuration.xml @@ -0,0 +1,252 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + UUID-012 + UUID-013 + + + UUID-014 + UUID-015 + + + + TestConfig + + + ru + TestConfig + + + + + Version8_3_24 + ManagedApplication + + PlatformApplication + + Russian + + + + + false + false + false + + + + + + + + + + + + + + + + + + + + + + Biometrics + true + + + Location + false + + + BackgroundLocation + false + + + BluetoothPrinters + false + + + WiFiPrinters + false + + + Contacts + false + + + Calendars + false + + + PushNotifications + false + + + LocalNotifications + false + + + InAppPurchases + false + + + PersonalComputerFileExchange + false + + + Ads + false + + + NumberDialing + false + + + CallProcessing + false + + + CallLog + false + + + AutoSendSMS + false + + + ReceiveSMS + false + + + SMSLog + false + + + Camera + false + + + Microphone + false + + + MusicLibrary + false + + + PictureAndVideoLibraries + false + + + AudioPlaybackAndVibration + false + + + BackgroundAudioPlaybackAndVibration + false + + + InstallPackages + false + + + OSBackup + true + + + ApplicationUsageStatistics + false + + + BarcodeScanning + false + + + BackgroundAudioRecording + false + + + AllFilesAccess + false + + + Videoconferences + false + + + NFC + false + + + DocumentScanning + false + + + SpeechToText + false + + + Geofences + false + + + IncomingShareRequests + false + + + AllIncomingShareRequestsTypesProcessing + false + + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + TaxiEnableVersion8_2 + DontUse + Version8_3_24 + + + + Русский + Продажи + + + \ No newline at end of file diff --git a/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Languages/Русский.xml b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Languages/Русский.xml new file mode 100644 index 00000000..37c60d78 --- /dev/null +++ b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Languages/Русский.xml @@ -0,0 +1,16 @@ + + + + + Русский + + + ru + Русский + + + + ru + + + \ No newline at end of file diff --git a/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи.xml b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи.xml new file mode 100644 index 00000000..e2384567 --- /dev/null +++ b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи.xml @@ -0,0 +1,24 @@ + + + + + Продажи + + + ru + Продажи + + + + true + true + false + + + + + + Настройки + + + diff --git a/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи/Subsystems/Настройки.xml b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи/Subsystems/Настройки.xml new file mode 100644 index 00000000..516eb081 --- /dev/null +++ b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи/Subsystems/Настройки.xml @@ -0,0 +1,27 @@ + + + + + Настройки + + + ru + Настройки раздела + + + + true + true + false + + + ru + Настройки подсистемы продаж + + + + + + + + diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index 18ffec03..cafe95df 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -223,8 +223,16 @@ function buildArgs(skillConfig, caseData, workDir, inputFilePath, runtime) { case 'workPath': // workDir + value from case.params or case (specified in mapping.field) const wpField = mapping.field || 'objectPath'; - const wpVal = caseData.params?.[wpField] ?? caseData[wpField] ?? ''; - args.push(join(workDir, wpVal)); + const wpVal = caseData.params?.[wpField] ?? caseData[wpField]; + if (wpVal === undefined || wpVal === null || wpVal === '') { + if (mapping.optional) { + args.pop(); // remove the flag we pushed at the top of the loop + break; + } + args.push(join(workDir, '')); + } else { + args.push(join(workDir, wpVal)); + } break; case 'switch': // flag already pushed, no value needed — remove the flag and re-push conditionally diff --git a/tests/skills/verify-snapshots.mjs b/tests/skills/verify-snapshots.mjs index 87a9313f..9b459a17 100644 --- a/tests/skills/verify-snapshots.mjs +++ b/tests/skills/verify-snapshots.mjs @@ -250,8 +250,16 @@ function buildSkillArgs(skillConfig, caseData, workDir, inputFile, runtime) { break; case 'workPath': { const field = mapping.field || 'objectPath'; - const val = caseData.params?.[field] ?? caseData[field] ?? ''; - args.push(join(workDir, val)); + const val = caseData.params?.[field] ?? caseData[field]; + if (val === undefined || val === null || val === '') { + if (mapping.optional) { + args.pop(); // remove flag pushed above + break; + } + args.push(join(workDir, '')); + } else { + args.push(join(workDir, val)); + } break; } case 'switch':