From d94aed8e64f9ab007a8f7caec6ce475a16d5a2ef Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 17 Sep 2026 10:27:06 -0700 Subject: [PATCH] Narrow gherkin mutation runner-worker to the changed scenario The DB soft Gherkin mutation re-ran the whole feature for every mutant; with a fresh LevelDB world per example, a 48-mutation feature exceeded 900s. The runner-worker now diffs the mutated IR against the base feature at /base/feature.json (derived from the mutation path, since job.work_dir may be mutation-specific) and runs only the changed scenario/example. Safe because each scenario uses an isolated world. The same feature now completes in ~74s. By architect. --- docs/architect-process-notes.md | 12 +++++++ psf-memo-db/acceptance/lib/runner-worker.js | 39 ++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/architect-process-notes.md b/docs/architect-process-notes.md index f415b8b..0f2a622 100644 --- a/docs/architect-process-notes.md +++ b/docs/architect-process-notes.md @@ -157,6 +157,18 @@ the captured output before the tool's `System/exit`; the JSON report with no spaces in the path. The tool writes its manifest (and, when clean, a `# mutation-stamp`) into the feature file; commit that tool-written change. +- **DB soft Gherkin mutation was impractically slow until the runner-worker + narrowed its scope.** Each mutant re-ran the whole feature (12 examples), and + DB acceptance creates a fresh LevelDB world per example, so a 48-mutation + feature took >900s (only 32 mutations reached after 15 min). `runner-worker.js` + now diffs the mutated IR against `/base/feature.json` — derived from the + mutation path `/mutations//feature.json`, because the mutator's + `job.work_dir` may be the mutation-specific directory — and runs only the + changed scenario/example. This is safe because every scenario uses an isolated + world. The same 48-mutation feature now completes in ~74s. The indexer and + client runner-workers still run the full feature; apply the same pattern if + their soft runs get slow. + - **`mutate-file.sh` works from every component dir, including the DB.** It defaults `MUTATE4JS_BIN` to the client's installed `node_modules/mutate4javascript`, and the tool's `npm test` baseline runs in diff --git a/psf-memo-db/acceptance/lib/runner-worker.js b/psf-memo-db/acceptance/lib/runner-worker.js index 326652f..caf669a 100644 --- a/psf-memo-db/acceptance/lib/runner-worker.js +++ b/psf-memo-db/acceptance/lib/runner-worker.js @@ -22,11 +22,43 @@ import readline from 'node:readline' import fs from 'node:fs' +import path from 'node:path' console.log = (...args) => console.error('[worker]', ...args) const { runFeature } = await import('./runtime.js') +// A gherkin mutation changes one example value in one scenario. Every scenario +// runs in its own isolated LevelDB world, so the other scenarios cannot be +// affected by the mutation. Narrow the IR to the changed scenario/example so a +// mutant does not re-run the whole feature. Falls back to the full IR when the +// base feature is missing or its shape differs. +function narrowToChanged (ir, basePath) { + try { + const base = JSON.parse(fs.readFileSync(basePath, 'utf8')) + if (!base || !Array.isArray(base.scenarios) || base.scenarios.length !== ir.scenarios.length) return ir + + for (let i = 0; i < ir.scenarios.length; i++) { + const scenario = ir.scenarios[i] + const original = base.scenarios[i] + if (JSON.stringify(scenario) === JSON.stringify(original)) continue + + const examples = Array.isArray(scenario.examples) ? scenario.examples : [] + const originalExamples = Array.isArray(original.examples) ? original.examples : [] + if (examples.length === originalExamples.length) { + const changed = examples.filter((ex, j) => JSON.stringify(ex) !== JSON.stringify(originalExamples[j])) + if (changed.length > 0) { + return { ...ir, scenarios: [{ ...scenario, examples: changed }] } + } + } + return { ...ir, scenarios: [scenario] } + } + return ir + } catch (err) { + return ir + } +} + const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -49,7 +81,12 @@ rl.on('line', async (line) => { try { const ir = JSON.parse(fs.readFileSync(job.feature_json, 'utf8')) - const report = await runFeature(ir) + // The mutated feature lives at /mutations//feature.json, so the + // base feature is two levels up. Do not trust job.work_dir (the mutator may + // pass the mutation-specific directory). + const basePath = path.resolve(path.dirname(job.feature_json), '..', '..', 'base', 'feature.json') + const narrowed = narrowToChanged(ir, basePath) + const report = await runFeature(narrowed) respond({ id: job.id, outcome: report.failures === 0 ? 'test_success' : 'test_failure',