diff --git a/tests/skills/README.md b/tests/skills/README.md index d70d047b..357c9201 100644 --- a/tests/skills/README.md +++ b/tests/skills/README.md @@ -260,6 +260,7 @@ ibcmd-проход автоматически `○ skipped`, если рядом | `outputPath` | нет | Относительный путь для навыков с `-OutputPath` | | `args_extra` | нет | Массив дополнительных CLI-аргументов | | `preRun` | нет | Массив шагов подготовки (см. ниже) | +| `inputFrom` | нет | Имя файла в `workDir` — вход берётся из него, а не из `input`. Нужно, когда вход производит `preRun` (напр. декомпилятор): `input` пишется ПОСЛЕ preRun и затёр бы его | | `expect` | нет | Дополнительные проверки (см. ниже) | | `expectError` | нет | `true` или строка — ожидается ошибка | | `noSnapshot` | нет | Непустая строка с причиной — кейс объявляет, что эталон не нужен (см. «Эталоны») | @@ -309,6 +310,16 @@ ibcmd-проход автоматически `○ skipped`, если рядом | `{ "script": "<навык>/scripts/<файл>", "input": {...}, "args": { "-Flag": "{inputFile}" } }` | Прогон другого навыка для подготовки фикстуры. Плейсхолдеры: `{inputFile}`, `{workDir}` | | `{ "writeFile": { "path": "<путь>", "content": "<строка или объект>" } }` | Записать произвольный файл в `workDir` (объект сериализуется в JSON) | +Дополнительно у шага-скрипта: `"cwd": "{workDir}"` — запустить шаг из рабочего каталога. Нужно, +когда относительный путь в `args` (напр. `-OutputPath: "Template.xml"`) должен лечь в фикстуру, а +не в текущий каталог процесса. Записи различаются: у шага — `{workDir}` в скобках, у кейса — +`"cwd": "workDir"` без них. + +Оба раннера (`runner.mjs` и `verify-snapshots.mjs`) читают этот DSL **каждый своей реализацией**. +Добавляя ключ, добавляй его в оба и в эту таблицу: недокументированный ключ, который понимает +только один раннер, даёт тихую дыру — кейс зелёный в функциональном прогоне и не доезжает до +платформы в верификации. + ## Эталоны (snapshots) Эталон — директория `snapshots/<имя-кейса>/` внутри папки навыка. Содержит ожидаемый выход навыка после нормализации. diff --git a/tests/skills/verify-snapshots.mjs b/tests/skills/verify-snapshots.mjs index f2dd6ad3..0017b394 100644 --- a/tests/skills/verify-snapshots.mjs +++ b/tests/skills/verify-snapshots.mjs @@ -563,11 +563,17 @@ function buildSkillArgs(skillConfig, caseData, workDir, inputFile, runtime) { args.push(mapping.flag); switch (mapping.from) { case 'inputFile': - args.push(inputFile || ''); + // inputFrom: вход берётся из файла в workDir, а не из case.input. Нужно, когда вход + // производит preRun (например, декомпилятор) — case.input пишется ПОСЛЕ preRun и + // затёр бы его. Как в runner.mjs. + args.push(caseData.inputFrom ? join(workDir, caseData.inputFrom) : (inputFile || '')); break; case 'workDir': args.push(workDir); break; + case 'outputPath': + args.push(join(workDir, caseData.outputPath || '')); + break; case 'workPath': { const field = mapping.field || 'objectPath'; const val = caseData.params?.[field] ?? caseData[field]; @@ -592,10 +598,19 @@ function buildSkillArgs(skillConfig, caseData, workDir, inputFile, runtime) { args.push(String(caseData.params?.[field] ?? caseData[field] ?? '')); } else if (mapping.from === 'literal') { args.push(mapping.value || ''); + } else { + // Незнакомый from раньше молча не давал значения — флаг уходил без аргумента, и + // это выглядело как дефект навыка. DSL читают два раннера, поэтому расхождение + // должно быть громким. + throw new Error(`_skill.json: неизвестный "from": "${mapping.from}" у флага ${mapping.flag}` + + ' — verify-snapshots.mjs не знает этого маппинга (см. buildSkillArgs)'); } } } - if (caseData.args_extra) args.push(...caseData.args_extra); + // Плейсхолдер {workDir} раскрывается и в args_extra — как в runner.mjs. + if (caseData.args_extra) { + args.push(...caseData.args_extra.map(a => typeof a === 'string' ? a.replace('{workDir}', workDir) : a)); + } return { scriptPath, args }; } @@ -631,7 +646,10 @@ function runPreSteps(preRun, workDir, runtime, log) { } const stepName = step.script.split('/').pop(); try { - execSkill(runtime, step.script, preArgs); + // cwd: "{workDir}" — шаг запускается из рабочего каталога, чтобы относительные + // пути в его args (напр. -OutputPath Template.xml) легли в фикстуру, а не в репозиторий. + const preCwd = step.cwd === '{workDir}' ? workDir : REPO_ROOT; + execSkill(runtime, step.script, preArgs, 60_000, preCwd); log(`preRun: ${stepName}`, true); } catch (e) { log(`preRun: ${stepName}`, false, e.stderr || e.message); @@ -875,7 +893,7 @@ async function verifyCase(skillName, caseName, skillConfig, caseData, opts) { try { const { args } = buildSkillArgs(skillConfig, caseData, workDir, inputFile, opts.runtime); - const mainCwd = skillConfig.cwd === 'workDir' ? workDir : REPO_ROOT; + const mainCwd = (caseData.cwd || skillConfig.cwd) === 'workDir' ? workDir : REPO_ROOT; const output = execSkill(opts.runtime, skillConfig.script, args, 60_000, mainCwd); const lastLine = output.trim().split('\n').pop(); if (caseData.expectError) {