fix(verify-snapshots): support case fixture setup and workDir cwd

Two harness gaps that masked real issues and leaked stray files:

1. Case-level `setup: "fixture:<name>"` 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) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-04-11 22:03:11 +03:00
parent 0d5d3451ff
commit 0b2c09f8d9
+20 -4
View File
@@ -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:<name>"` pointing to
// tests/skills/cases/<skill>/fixtures/<name> — 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');