From 0b2c09f8d9b482925887ecf4fc11027ee1bc36ea Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 11 Apr 2026 22:03:11 +0300 Subject: [PATCH] fix(verify-snapshots): support case fixture setup and workDir cwd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two harness gaps that masked real issues and leaked stray files: 1. Case-level `setup: "fixture:"` was ignored — runner.mjs handled it, verify-snapshots did not. skd-edit/add-drilldown silently failed with "File not found: Template.xml" because the fixture never reached workDir. Added Step 0 fixture copy mirroring runner.mjs behavior. 2. `skillConfig.cwd === "workDir"` was ignored — main skill always ran with cwd=REPO_ROOT. mxl-compile cases pass relative -OutputPath "Template.xml", which landed in the repo root on every run. Plumb cwd through execSkill and set mainCwd from skillConfig.cwd. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/skills/verify-snapshots.mjs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/skills/verify-snapshots.mjs b/tests/skills/verify-snapshots.mjs index d514c4fc..c93dd416 100644 --- a/tests/skills/verify-snapshots.mjs +++ b/tests/skills/verify-snapshots.mjs @@ -60,17 +60,17 @@ function resolveScript(relPath, runtime) { return full; } -function execSkill(runtime, scriptRelPath, args, timeout = 60_000) { +function execSkill(runtime, scriptRelPath, args, timeout = 60_000, cwd = REPO_ROOT) { const scriptPath = resolveScript(scriptRelPath, runtime); if (runtime === 'python') { return execFileSync(process.env.PYTHON || 'python', [scriptPath, ...args], { - encoding: 'utf8', timeout, stdio: ['pipe', 'pipe', 'pipe'], cwd: REPO_ROOT, + encoding: 'utf8', timeout, stdio: ['pipe', 'pipe', 'pipe'], cwd, }); } return execFileSync('powershell.exe', [ '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', scriptPath, ...args - ], { encoding: 'utf8', timeout, stdio: ['pipe', 'pipe', 'pipe'], cwd: REPO_ROOT }); + ], { encoding: 'utf8', timeout, stdio: ['pipe', 'pipe', 'pipe'], cwd }); } // ─── Dependency resolution ────────────────────────────────────────────────── @@ -363,6 +363,21 @@ async function verifyCase(skillName, caseName, skillConfig, caseData, opts) { const configDir = (setupType === 'empty-config' || isCfInit) ? workDir : null; try { + // ── Step 0: Case-level fixture copy (runner.mjs compatibility) ── + // A case may declare `"setup": "fixture:"` pointing to + // tests/skills/cases//fixtures/ — copy its contents into workDir + // so the skill script finds them at the expected relative path. + if (typeof caseData.setup === 'string' && caseData.setup.startsWith('fixture:')) { + const fixtureName = caseData.setup.slice('fixture:'.length); + const fixturePath = join(CASES, skillName, 'fixtures', fixtureName); + if (!existsSync(fixturePath)) { + result.errors.push(`Fixture not found: ${fixturePath}`); + return result; + } + cpSync(fixturePath, workDir, { recursive: true }); + log(`fixture: ${fixtureName}`, true); + } + // ── Step 1: Setup (cf-init for empty-config, nothing for 'none') ── // Skip setup for cf-init skill — the test itself creates the config if (configDir && setupType === 'empty-config' && !CONFIG_INIT_SKILLS.has(skillName)) { @@ -468,7 +483,8 @@ async function verifyCase(skillName, caseName, skillConfig, caseData, opts) { try { const { args } = buildSkillArgs(skillConfig, caseData, workDir, inputFile, opts.runtime); - const output = execSkill(opts.runtime, skillConfig.script, args); + const mainCwd = 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) { log(skillName, false, 'expected non-zero exit but got success');