diff --git a/tests/skills/cases/cf-info/_skill.json b/tests/skills/cases/cf-info/_skill.json index b93494a5..72be7039 100644 --- a/tests/skills/cases/cf-info/_skill.json +++ b/tests/skills/cases/cf-info/_skill.json @@ -6,6 +6,6 @@ ], "snapshot": { "root": "workDir", - "normalizeUuids": false + "normalizeUuids": true } } diff --git a/tests/skills/cases/cf-info/snapshots/brief-mode/Configuration.xml b/tests/skills/cases/cf-info/snapshots/brief-mode/Configuration.xml index e20f0c45..3435599c 100644 --- a/tests/skills/cases/cf-info/snapshots/brief-mode/Configuration.xml +++ b/tests/skills/cases/cf-info/snapshots/brief-mode/Configuration.xml @@ -1,34 +1,34 @@ - + - 9cd510cd-abfc-11d4-9434-004095e12fc7 - 8613c9b5-714f-4db3-ba57-92944d83f3e6 + UUID-002 + UUID-003 - 9fcd25a0-4822-11d4-9414-008048da11f9 - 52c8688f-9020-459e-b2ff-acf11197aa37 + UUID-004 + UUID-005 - e3687481-0a87-462c-a166-9f34594f9bba - 6302d246-62c8-4400-9fc2-16ec7bbb4c26 + UUID-006 + UUID-007 - 9de14907-ec23-4a07-96f0-85521cb6b53b - 8d278abf-2004-4056-abeb-af728d4fa9a7 + UUID-008 + UUID-009 - 51f2d5d8-ea4d-4064-8892-82951750031e - ae8c947e-0c04-49b2-b33a-a7914b52e5bd + UUID-010 + UUID-011 - e68182ea-4237-4383-967f-90c1e3370bc7 - 686d04c2-0d6f-4a5c-ad74-db5a8c6381f5 + UUID-012 + UUID-013 - fb282519-d103-4dd3-bc12-cb271d631dfc - dd62b6ff-7ce1-41eb-a111-48079672630c + UUID-014 + UUID-015 diff --git a/tests/skills/cases/cf-info/snapshots/brief-mode/Languages/Русский.xml b/tests/skills/cases/cf-info/snapshots/brief-mode/Languages/Русский.xml index 9e438bd5..37c60d78 100644 --- a/tests/skills/cases/cf-info/snapshots/brief-mode/Languages/Русский.xml +++ b/tests/skills/cases/cf-info/snapshots/brief-mode/Languages/Русский.xml @@ -1,6 +1,6 @@ - + Русский diff --git a/tests/skills/cases/cf-info/snapshots/config-with-objects/Configuration.xml b/tests/skills/cases/cf-info/snapshots/config-with-objects/Configuration.xml index dcf2fe2e..a2e25fd0 100644 --- a/tests/skills/cases/cf-info/snapshots/config-with-objects/Configuration.xml +++ b/tests/skills/cases/cf-info/snapshots/config-with-objects/Configuration.xml @@ -1,34 +1,34 @@ - + - 9cd510cd-abfc-11d4-9434-004095e12fc7 - 8613c9b5-714f-4db3-ba57-92944d83f3e6 + UUID-002 + UUID-003 - 9fcd25a0-4822-11d4-9414-008048da11f9 - 52c8688f-9020-459e-b2ff-acf11197aa37 + UUID-004 + UUID-005 - e3687481-0a87-462c-a166-9f34594f9bba - 6302d246-62c8-4400-9fc2-16ec7bbb4c26 + UUID-006 + UUID-007 - 9de14907-ec23-4a07-96f0-85521cb6b53b - 8d278abf-2004-4056-abeb-af728d4fa9a7 + UUID-008 + UUID-009 - 51f2d5d8-ea4d-4064-8892-82951750031e - ae8c947e-0c04-49b2-b33a-a7914b52e5bd + UUID-010 + UUID-011 - e68182ea-4237-4383-967f-90c1e3370bc7 - 686d04c2-0d6f-4a5c-ad74-db5a8c6381f5 + UUID-012 + UUID-013 - fb282519-d103-4dd3-bc12-cb271d631dfc - dd62b6ff-7ce1-41eb-a111-48079672630c + UUID-014 + UUID-015 diff --git a/tests/skills/cases/cf-info/snapshots/config-with-objects/Languages/Русский.xml b/tests/skills/cases/cf-info/snapshots/config-with-objects/Languages/Русский.xml index 9e438bd5..37c60d78 100644 --- a/tests/skills/cases/cf-info/snapshots/config-with-objects/Languages/Русский.xml +++ b/tests/skills/cases/cf-info/snapshots/config-with-objects/Languages/Русский.xml @@ -1,6 +1,6 @@ - + Русский diff --git a/tests/skills/cases/cf-info/snapshots/full-mode/Configuration.xml b/tests/skills/cases/cf-info/snapshots/full-mode/Configuration.xml index e20f0c45..3435599c 100644 --- a/tests/skills/cases/cf-info/snapshots/full-mode/Configuration.xml +++ b/tests/skills/cases/cf-info/snapshots/full-mode/Configuration.xml @@ -1,34 +1,34 @@ - + - 9cd510cd-abfc-11d4-9434-004095e12fc7 - 8613c9b5-714f-4db3-ba57-92944d83f3e6 + UUID-002 + UUID-003 - 9fcd25a0-4822-11d4-9414-008048da11f9 - 52c8688f-9020-459e-b2ff-acf11197aa37 + UUID-004 + UUID-005 - e3687481-0a87-462c-a166-9f34594f9bba - 6302d246-62c8-4400-9fc2-16ec7bbb4c26 + UUID-006 + UUID-007 - 9de14907-ec23-4a07-96f0-85521cb6b53b - 8d278abf-2004-4056-abeb-af728d4fa9a7 + UUID-008 + UUID-009 - 51f2d5d8-ea4d-4064-8892-82951750031e - ae8c947e-0c04-49b2-b33a-a7914b52e5bd + UUID-010 + UUID-011 - e68182ea-4237-4383-967f-90c1e3370bc7 - 686d04c2-0d6f-4a5c-ad74-db5a8c6381f5 + UUID-012 + UUID-013 - fb282519-d103-4dd3-bc12-cb271d631dfc - dd62b6ff-7ce1-41eb-a111-48079672630c + UUID-014 + UUID-015 diff --git a/tests/skills/cases/cf-info/snapshots/full-mode/Languages/Русский.xml b/tests/skills/cases/cf-info/snapshots/full-mode/Languages/Русский.xml index 9e438bd5..37c60d78 100644 --- a/tests/skills/cases/cf-info/snapshots/full-mode/Languages/Русский.xml +++ b/tests/skills/cases/cf-info/snapshots/full-mode/Languages/Русский.xml @@ -1,6 +1,6 @@ - + Русский diff --git a/tests/skills/cases/cf-info/snapshots/overview/Configuration.xml b/tests/skills/cases/cf-info/snapshots/overview/Configuration.xml index e20f0c45..3435599c 100644 --- a/tests/skills/cases/cf-info/snapshots/overview/Configuration.xml +++ b/tests/skills/cases/cf-info/snapshots/overview/Configuration.xml @@ -1,34 +1,34 @@ - + - 9cd510cd-abfc-11d4-9434-004095e12fc7 - 8613c9b5-714f-4db3-ba57-92944d83f3e6 + UUID-002 + UUID-003 - 9fcd25a0-4822-11d4-9414-008048da11f9 - 52c8688f-9020-459e-b2ff-acf11197aa37 + UUID-004 + UUID-005 - e3687481-0a87-462c-a166-9f34594f9bba - 6302d246-62c8-4400-9fc2-16ec7bbb4c26 + UUID-006 + UUID-007 - 9de14907-ec23-4a07-96f0-85521cb6b53b - 8d278abf-2004-4056-abeb-af728d4fa9a7 + UUID-008 + UUID-009 - 51f2d5d8-ea4d-4064-8892-82951750031e - ae8c947e-0c04-49b2-b33a-a7914b52e5bd + UUID-010 + UUID-011 - e68182ea-4237-4383-967f-90c1e3370bc7 - 686d04c2-0d6f-4a5c-ad74-db5a8c6381f5 + UUID-012 + UUID-013 - fb282519-d103-4dd3-bc12-cb271d631dfc - dd62b6ff-7ce1-41eb-a111-48079672630c + UUID-014 + UUID-015 diff --git a/tests/skills/cases/cf-info/snapshots/overview/Languages/Русский.xml b/tests/skills/cases/cf-info/snapshots/overview/Languages/Русский.xml index 9e438bd5..37c60d78 100644 --- a/tests/skills/cases/cf-info/snapshots/overview/Languages/Русский.xml +++ b/tests/skills/cases/cf-info/snapshots/overview/Languages/Русский.xml @@ -1,6 +1,6 @@ - + Русский diff --git a/tests/skills/cases/cf-validate/_skill.json b/tests/skills/cases/cf-validate/_skill.json index 15ebe484..15e2eae1 100644 --- a/tests/skills/cases/cf-validate/_skill.json +++ b/tests/skills/cases/cf-validate/_skill.json @@ -6,6 +6,6 @@ ], "snapshot": { "root": "workDir", - "normalizeUuids": false + "normalizeUuids": true } } diff --git a/tests/skills/cases/cf-validate/snapshots/modified-config/Configuration.xml b/tests/skills/cases/cf-validate/snapshots/modified-config/Configuration.xml index 77e00bd9..ee085a64 100644 --- a/tests/skills/cases/cf-validate/snapshots/modified-config/Configuration.xml +++ b/tests/skills/cases/cf-validate/snapshots/modified-config/Configuration.xml @@ -1,34 +1,34 @@ - + - 9cd510cd-abfc-11d4-9434-004095e12fc7 - 8613c9b5-714f-4db3-ba57-92944d83f3e6 + UUID-002 + UUID-003 - 9fcd25a0-4822-11d4-9414-008048da11f9 - 52c8688f-9020-459e-b2ff-acf11197aa37 + UUID-004 + UUID-005 - e3687481-0a87-462c-a166-9f34594f9bba - 6302d246-62c8-4400-9fc2-16ec7bbb4c26 + UUID-006 + UUID-007 - 9de14907-ec23-4a07-96f0-85521cb6b53b - 8d278abf-2004-4056-abeb-af728d4fa9a7 + UUID-008 + UUID-009 - 51f2d5d8-ea4d-4064-8892-82951750031e - ae8c947e-0c04-49b2-b33a-a7914b52e5bd + UUID-010 + UUID-011 - e68182ea-4237-4383-967f-90c1e3370bc7 - 686d04c2-0d6f-4a5c-ad74-db5a8c6381f5 + UUID-012 + UUID-013 - fb282519-d103-4dd3-bc12-cb271d631dfc - dd62b6ff-7ce1-41eb-a111-48079672630c + UUID-014 + UUID-015 diff --git a/tests/skills/cases/cf-validate/snapshots/modified-config/Languages/Русский.xml b/tests/skills/cases/cf-validate/snapshots/modified-config/Languages/Русский.xml index 9e438bd5..37c60d78 100644 --- a/tests/skills/cases/cf-validate/snapshots/modified-config/Languages/Русский.xml +++ b/tests/skills/cases/cf-validate/snapshots/modified-config/Languages/Русский.xml @@ -1,6 +1,6 @@ - + Русский diff --git a/tests/skills/cases/cf-validate/snapshots/valid-config/Configuration.xml b/tests/skills/cases/cf-validate/snapshots/valid-config/Configuration.xml index e20f0c45..3435599c 100644 --- a/tests/skills/cases/cf-validate/snapshots/valid-config/Configuration.xml +++ b/tests/skills/cases/cf-validate/snapshots/valid-config/Configuration.xml @@ -1,34 +1,34 @@ - + - 9cd510cd-abfc-11d4-9434-004095e12fc7 - 8613c9b5-714f-4db3-ba57-92944d83f3e6 + UUID-002 + UUID-003 - 9fcd25a0-4822-11d4-9414-008048da11f9 - 52c8688f-9020-459e-b2ff-acf11197aa37 + UUID-004 + UUID-005 - e3687481-0a87-462c-a166-9f34594f9bba - 6302d246-62c8-4400-9fc2-16ec7bbb4c26 + UUID-006 + UUID-007 - 9de14907-ec23-4a07-96f0-85521cb6b53b - 8d278abf-2004-4056-abeb-af728d4fa9a7 + UUID-008 + UUID-009 - 51f2d5d8-ea4d-4064-8892-82951750031e - ae8c947e-0c04-49b2-b33a-a7914b52e5bd + UUID-010 + UUID-011 - e68182ea-4237-4383-967f-90c1e3370bc7 - 686d04c2-0d6f-4a5c-ad74-db5a8c6381f5 + UUID-012 + UUID-013 - fb282519-d103-4dd3-bc12-cb271d631dfc - dd62b6ff-7ce1-41eb-a111-48079672630c + UUID-014 + UUID-015 diff --git a/tests/skills/cases/cf-validate/snapshots/valid-config/Languages/Русский.xml b/tests/skills/cases/cf-validate/snapshots/valid-config/Languages/Русский.xml index 9e438bd5..37c60d78 100644 --- a/tests/skills/cases/cf-validate/snapshots/valid-config/Languages/Русский.xml +++ b/tests/skills/cases/cf-validate/snapshots/valid-config/Languages/Русский.xml @@ -1,6 +1,6 @@ - + Русский diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index 5eaf2faa..9faab96a 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -1,12 +1,12 @@ #!/usr/bin/env node -// skill-test-runner v0.2 — Snapshot-based regression tests for 1C skill scripts -// Usage: node tests/skills/runner.mjs [filter] [--update-snapshots] [--runtime python] [--json report.json] +// skill-test-runner v0.3 — Snapshot-based regression tests for 1C skill scripts +// Usage: node tests/skills/runner.mjs [filter] [--update-snapshots] [--runtime python] [--json report.json] [--concurrency N] -import { execFileSync } from 'child_process'; +import { execFileSync, execFile } from 'child_process'; import { existsSync, mkdirSync, mkdtempSync, rmSync, readFileSync, writeFileSync, readdirSync, statSync, cpSync, copyFileSync } from 'fs'; import { join, resolve, dirname, relative, basename, extname } from 'path'; -import { tmpdir } from 'os'; +import { tmpdir, cpus } from 'os'; // ─── Paths ────────────────────────────────────────────────────────────────── @@ -19,7 +19,7 @@ const CACHE = resolve(ROOT, '.cache'); // ─── CLI args ─────────────────────────────────────────────────────────────── function parseArgs(argv) { - const args = { filter: null, updateSnapshots: false, runtime: 'powershell', jsonReport: null, verbose: false }; + const args = { filter: null, updateSnapshots: false, runtime: 'powershell', jsonReport: null, verbose: false, concurrency: cpus().length }; const rest = argv.slice(2); for (let i = 0; i < rest.length; i++) { const a = rest[i]; @@ -27,6 +27,7 @@ function parseArgs(argv) { if (a === '--runtime' && rest[i + 1]) { args.runtime = rest[++i]; continue; } if (a === '--json' && rest[i + 1]) { args.jsonReport = rest[++i]; continue; } if (a === '--verbose' || a === '-v') { args.verbose = true; continue; } + if (a === '--concurrency' && rest[i + 1]) { args.concurrency = parseInt(rest[++i], 10) || 1; continue; } if (!a.startsWith('--') && !args.filter) { args.filter = a.replace(/\\/g, '/'); continue; } } return args; @@ -154,6 +155,31 @@ function execSkillRaw(runtime, scriptPath, args, cwd) { }); } +function execSkillAsync(runtime, scriptPath, args, cwd) { + return new Promise((resolve, reject) => { + const execCwd = cwd || REPO_ROOT; + const cmd = runtime === 'python' + ? [process.env.PYTHON || 'python', [scriptPath, ...args]] + : ['powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', scriptPath, ...args]]; + + const child = execFile(cmd[0], cmd[1], { + encoding: 'utf8', + timeout: 60_000, + cwd: execCwd, + }, (error, stdout, stderr) => { + if (error) { + const err = new Error(error.message); + err.status = error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' ? 1 : (error.code ?? 1); + err.stdout = stdout || ''; + err.stderr = stderr || ''; + reject(err); + } else { + resolve(stdout); + } + }); + }); +} + // ─── Workspace ────────────────────────────────────────────────────────────── function createWorkspace(fixturePath, readOnly) { @@ -339,6 +365,119 @@ function updateSnapshot(workDir, snapshotDir, snapshotConfig) { // ─── Run a single case ────────────────────────────────────────────────────── +async function runCaseAsync(testCase, opts) { + const { skillConfig, caseData, snapshotDir } = testCase; + const t0 = performance.now(); + const setupName = caseData.setup || skillConfig.setup || 'none'; + let workspace = null; + let workDir = null; + let inputFile = null; + + try { + const skillCasesDir = join(CASES, testCase.skillDir); + const fixturePath = ensureSetup(setupName, opts.runtime, skillCasesDir); + if (fixturePath === SKIP) { + return { id: testCase.id, skill: testCase.skillDir, name: testCase.name, passed: true, skipped: true, errors: [], elapsed: '0.0s' }; + } + const isExternal = typeof setupName === 'string' && setupName.startsWith('external:'); + workspace = createWorkspace(fixturePath, isExternal); + workDir = workspace.path; + + // Pre-run steps + if (caseData.preRun) { + for (const step of caseData.preRun) { + const preScript = resolveScript(step.script, opts.runtime); + const preArgs = []; + for (const [flag, value] of Object.entries(step.args || {})) { + preArgs.push(flag); + if (value === true || value === '') continue; + preArgs.push(String(value).replace('{workDir}', workDir).replace('{inputFile}', '')); + } + let preInputFile = null; + if (step.input) { + preInputFile = join(workDir, '__pre_input.json'); + writeFileSync(preInputFile, JSON.stringify(step.input, null, 2), 'utf8'); + for (let i = 0; i < preArgs.length; i++) { + if (preArgs[i] === '') preArgs[i] = preInputFile; + } + } + try { + const preCwd = step.cwd === '{workDir}' ? workDir : undefined; + await execSkillAsync(opts.runtime, preScript, preArgs, preCwd); + } catch (e) { + throw new Error(`preRun step "${step.script}" failed: ${e.stderr || e.message}`); + } + if (preInputFile && existsSync(preInputFile)) rmSync(preInputFile); + } + } + + // Write input + if (caseData.input !== undefined) { + inputFile = join(workDir, '__input.json'); + writeFileSync(inputFile, JSON.stringify(caseData.input, null, 2), 'utf8'); + } + + // Execute + const { scriptPath, args } = buildArgs(skillConfig, caseData, workDir, inputFile, opts.runtime); + let stdout = '', stderr = '', exitCode = 0; + try { + const execCwd = skillConfig.cwd === 'workDir' ? workDir : undefined; + stdout = await execSkillAsync(opts.runtime, scriptPath, args, execCwd); + } catch (e) { + exitCode = e.status ?? 1; + stdout = e.stdout || ''; + stderr = e.stderr || ''; + } + + if (inputFile && existsSync(inputFile)) rmSync(inputFile); + + // Assertions + const errors = []; + if (caseData.expectError) { + if (exitCode === 0) errors.push('Expected error (non-zero exit) but got exitCode=0'); + if (typeof caseData.expectError === 'string' && !stderr.includes(caseData.expectError)) { + errors.push(`Expected stderr to contain "${caseData.expectError}", got: ${stderr.substring(0, 200)}`); + } + } else { + if (exitCode !== 0) { + errors.push(`exitCode=${exitCode}\nstdout: ${stdout.substring(0, 300)}\nstderr: ${stderr.substring(0, 300)}`); + } + if (caseData.expect?.files) { + for (const f of caseData.expect.files) { + if (!existsSync(join(workDir, f))) errors.push(`Expected file not found: ${f}`); + } + } + if (caseData.expect?.stdoutContains) { + if (!stdout.includes(caseData.expect.stdoutContains)) { + errors.push(`stdout does not contain "${caseData.expect.stdoutContains}"`); + } + } + if (errors.length === 0 && !caseData.expectError && !workspace.readOnly) { + const snapshotConfig = skillConfig.snapshot || {}; + if (opts.updateSnapshots) { + updateSnapshot(workDir, snapshotDir, snapshotConfig); + } else { + const cmp = compareSnapshot(workDir, snapshotDir, snapshotConfig); + if (!cmp.match && cmp.diffs) { + for (const d of cmp.diffs) { + if (d.type === 'missing') errors.push(`Snapshot: file missing — ${d.file}`); + else errors.push(`Snapshot: ${d.file}:${d.line} differs\n expected: ${d.expected}\n actual: ${d.actual}`); + } + } + } + } + } + + const elapsed = ((performance.now() - t0) / 1000).toFixed(1); + return { id: testCase.id, skill: testCase.skillDir, name: testCase.name, passed: errors.length === 0, errors, elapsed: `${elapsed}s`, snapshotUpdated: opts.updateSnapshots && !caseData.expectError && !workspace.readOnly }; + } catch (e) { + const elapsed = ((performance.now() - t0) / 1000).toFixed(1); + return { id: testCase.id, skill: testCase.skillDir, name: testCase.name, passed: false, errors: [`Runner error: ${e.message}`], elapsed: `${elapsed}s` }; + } finally { + if (workspace) cleanupWorkspace(workspace); + } +} + function runCase(testCase, opts) { const { skillConfig, caseData, snapshotDir } = testCase; const t0 = performance.now(); @@ -458,8 +597,8 @@ function runCase(testCase, opts) { } } - // Snapshot comparison - if (errors.length === 0 && !caseData.expectError) { + // Snapshot comparison (skip for external/read-only workspaces) + if (errors.length === 0 && !caseData.expectError && !workspace.readOnly) { const snapshotConfig = skillConfig.snapshot || {}; if (opts.updateSnapshots) { updateSnapshot(workDir, snapshotDir, snapshotConfig); @@ -486,7 +625,7 @@ function runCase(testCase, opts) { passed: errors.length === 0, errors, elapsed: `${elapsed}s`, - snapshotUpdated: opts.updateSnapshots && !caseData.expectError, + snapshotUpdated: opts.updateSnapshots && !caseData.expectError && !workspace.readOnly, }; } catch (e) { @@ -506,7 +645,7 @@ function runCase(testCase, opts) { // ─── Reporter ─────────────────────────────────────────────────────────────── -function printReport(results, opts) { +function printReport(results, opts, wallTime) { const skipped = results.filter(r => r.skipped); const passed = results.filter(r => r.passed && !r.skipped); const failed = results.filter(r => !r.passed); @@ -561,10 +700,11 @@ function printReport(results, opts) { } } - const totalTime = results.reduce((s, r) => s + parseFloat(r.elapsed), 0).toFixed(1); + const cpuTime = results.reduce((s, r) => s + parseFloat(r.elapsed), 0).toFixed(1); console.log(''); const skippedStr = skipped.length > 0 ? ` | Skipped: ${skipped.length}` : ''; - console.log(` Passed: ${passed.length} | Failed: ${failed.length}${skippedStr} | Total: ${results.length} | Time: ${totalTime}s`); + const timeStr = wallTime ? `${wallTime}s wall, ${cpuTime}s cpu` : `${cpuTime}s`; + console.log(` Passed: ${passed.length} | Failed: ${failed.length}${skippedStr} | Total: ${results.length} | Time: ${timeStr}`); console.log(''); if (opts.jsonReport) { @@ -589,9 +729,30 @@ function printReport(results, opts) { return failed.length === 0; } +// ─── Parallel pool ───────────────────────────────────────────────────────── + +async function runPool(cases, opts) { + const results = new Array(cases.length); + let next = 0; + + async function worker() { + while (next < cases.length) { + const idx = next++; + results[idx] = await runCaseAsync(cases[idx], opts); + } + } + + const workers = []; + for (let i = 0; i < Math.min(opts.concurrency, cases.length); i++) { + workers.push(worker()); + } + await Promise.all(workers); + return results; +} + // ─── Main ─────────────────────────────────────────────────────────────────── -function main() { +async function main() { const opts = parseArgs(process.argv); const cases = discoverCases(opts.filter); @@ -600,17 +761,35 @@ function main() { process.exit(0); } - console.log(`\nRunning ${cases.length} test(s)... [runtime: ${opts.runtime}]`); + const parallel = opts.concurrency > 1; + const modeStr = parallel ? `${opts.concurrency} workers` : 'sequential'; + console.log(`\nRunning ${cases.length} test(s)... [runtime: ${opts.runtime}, ${modeStr}]`); // Ensure cache dir exists mkdirSync(CACHE, { recursive: true }); - const results = []; - for (const tc of cases) { - results.push(runCase(tc, opts)); + // Pre-warm shared fixtures before parallel run + const setups = new Set(cases.map(c => c.caseData.setup || c.skillConfig.setup || 'none')); + for (const setup of setups) { + if (setup === 'empty-config' || setup === 'base-config') { + try { ensureSetup(setup, opts.runtime, CASES); } catch {} + } } - const allPassed = printReport(results, opts); + const wallStart = performance.now(); + let results; + + if (parallel) { + results = await runPool(cases, opts); + } else { + results = []; + for (const tc of cases) { + results.push(await runCaseAsync(tc, opts)); + } + } + + const wallTime = ((performance.now() - wallStart) / 1000).toFixed(1); + const allPassed = printReport(results, opts, wallTime); process.exit(allPassed ? 0 : 1); }