Files
msd-core/tests/mutation-tap-runner-wiring.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

190 lines
8.5 KiB
JavaScript

'use strict';
/**
* tests/mutation-tap-runner-wiring.test.cjs
*
* Pins the #3915 tap-runner wiring — the config contract AND the
* workflow<->config parity, because the injected env token lives on two
* surfaces (`.github/workflows/mutation.yml`'s per-shard `env:` block and
* `stryker.config.mjs`'s reader of it) and silent drift between them would
* make every shard silently fall back to running the FULL default test list
* instead of its own module's tests — slow, wrong, and (because Stryker
* would still find SOME test constraining each mutant) green.
*
* FAILING-FIRST (#3915): stryker.config.mjs still declares `testRunner:
* 'command'` with a `commandRunner.command` built from `MUTATION_TEST_CMD`,
* and mutation.yml still injects `MUTATION_TEST_CMD` / `matrix.isolation`.
* Every test below targets the tap-runner shape those files do not have yet.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs');
const { pathToFileURL } = require('node:url');
const yaml = require('js-yaml');
const REPO_ROOT = path.resolve(__dirname, '..');
const CONFIG_PATH = path.join(REPO_ROOT, 'stryker.config.mjs');
const WORKFLOW_PATH = path.join(REPO_ROOT, '.github', 'workflows', 'mutation.yml');
// Env keys this config is known to read. Every key is saved/restored so a
// test's env override can never leak into a sibling test.
const RELEVANT_ENV_KEYS = ['MUTATION_TEST_FILES', 'MUTATION_TEST_CMD', 'MUTATION_BREAK'];
// Cache-busting counter: importing the SAME file:// URL twice returns the
// SAME cached ES module record, so an env-dependent config test that reused
// one URL across calls would silently observe the FIRST call's env forever —
// every subsequent env-dependent assertion would pass or fail for the wrong
// reason. Incrementing this per call forces a fresh module evaluation.
let _importCounter = 0;
/**
* Load stryker.config.mjs with `env` applied on top of process.env for the
* duration of the import, then restore process.env exactly. `env` values of
* `undefined` delete the corresponding key rather than setting it.
*
* @param {Record<string, string|undefined>} env
* @returns {Promise<object>} the config module's default export
*/
async function loadConfig(env) {
// Save/restore the UNION of RELEVANT_ENV_KEYS and Object.keys(env), not just the fixed
// list: the PARITY test below discovers its env key NAME from the parsed workflow file
// rather than hardcoding it, so a key outside RELEVANT_ENV_KEYS can reach here — saving
// only the fixed list would let that discovered key leak into sibling tests.
const keysToRestore = new Set([...RELEVANT_ENV_KEYS, ...Object.keys(env)]);
const saved = {};
for (const key of keysToRestore) saved[key] = process.env[key];
for (const [key, value] of Object.entries(env)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
try {
const mod = await import(`${pathToFileURL(CONFIG_PATH).href}?v=${_importCounter++}`);
return mod.default;
} finally {
for (const key of keysToRestore) {
if (saved[key] === undefined) delete process.env[key];
else process.env[key] = saved[key];
}
}
}
describe('stryker.config.mjs: tap runner contract (#3915)', () => {
test("testRunner === 'tap'", async () => {
const config = await loadConfig({});
assert.strictEqual(config.testRunner, 'tap');
});
test("coverageAnalysis === 'perTest'", async () => {
const config = await loadConfig({});
assert.strictEqual(config.coverageAnalysis, 'perTest');
});
test("coverageAnalysis !== 'off' (the literal condition #3915 requires to stop being true)", async () => {
const config = await loadConfig({});
assert.notStrictEqual(config.coverageAnalysis, 'off');
});
test('no own-property commandRunner (the command runner is gone)', async () => {
const config = await loadConfig({});
assert.ok(!Object.prototype.hasOwnProperty.call(config, 'commandRunner'));
});
test('tap.forceBail === false', async () => {
const config = await loadConfig({});
assert.strictEqual(config.tap.forceBail, false);
});
test('no own-property buildCommand, and tap has no own-property nodeArgs (no rebuild step reintroduced, ADR-457)', async () => {
const config = await loadConfig({});
assert.ok(!Object.prototype.hasOwnProperty.call(config, 'buildCommand'));
assert.ok(!Object.prototype.hasOwnProperty.call(config.tap, 'nodeArgs'));
});
test('MUTATION_TEST_FILES set → tap.testFiles deep-equals the parsed entries', async () => {
const config = await loadConfig({
MUTATION_TEST_FILES: 'tests/frontmatter.unit.test.cjs tests/unusable-input.test.cjs',
});
assert.deepStrictEqual(config.tap.testFiles, [
'tests/frontmatter.unit.test.cjs',
'tests/unusable-input.test.cjs',
]);
});
test('MUTATION_BREAK set → thresholds.break reflects it unchanged (re-proved after the runner swap)', async () => {
const config = await loadConfig({ MUTATION_BREAK: '72' });
assert.strictEqual(config.thresholds.break, 72);
});
test('MUTATION_TEST_FILES set but empty → fail-closed survives all the way to config load', async () => {
await assert.rejects(() => loadConfig({ MUTATION_TEST_FILES: '' }));
});
test('mutate scope unchanged by the runner swap', async () => {
const config = await loadConfig({});
assert.ok(Array.isArray(config.mutate));
assert.ok(config.mutate.includes('msd-core/bin/lib/**/*.cjs'));
assert.ok(
config.mutate.some((entry) => entry.startsWith('!msd-core/bin/lib/')),
'mutate array must still carry at least one !msd-core/bin/lib/... exclusion'
);
});
});
describe('mutation.yml <-> stryker.config.mjs: injected env parity (#3915)', () => {
const workflowDoc = yaml.load(fs.readFileSync(WORKFLOW_PATH, 'utf8'));
const mutateJob = workflowDoc.jobs.mutate;
const runStrykerStep = mutateJob.steps.find(
(step) => typeof step.name === 'string' && step.name.startsWith('Run Stryker')
);
test("the 'Run Stryker' step exists", () => {
assert.ok(runStrykerStep, "no step in the mutate job's steps has a name starting with 'Run Stryker'");
});
test('step.env has key MUTATION_TEST_FILES', () => {
assert.ok(Object.prototype.hasOwnProperty.call(runStrykerStep.env, 'MUTATION_TEST_FILES'));
});
test('step.env does NOT have key MUTATION_TEST_CMD', () => {
assert.ok(!Object.prototype.hasOwnProperty.call(runStrykerStep.env, 'MUTATION_TEST_CMD'));
});
test('MUTATION_TEST_FILES value is exactly the ${{ matrix.tests }} expression (derived from mutation-matrix.cjs)', () => {
assert.strictEqual(String(runStrykerStep.env.MUTATION_TEST_FILES).trim(), '${{ matrix.tests }}');
});
test('MUTATION_BREAK still references matrix.minScore', () => {
assert.ok(Object.prototype.hasOwnProperty.call(runStrykerStep.env, 'MUTATION_BREAK'));
assert.ok(String(runStrykerStep.env.MUTATION_BREAK).includes('matrix.minScore'));
});
test('no value anywhere in the step env mentions --test-isolation', () => {
for (const value of Object.values(runStrykerStep.env)) {
assert.ok(!String(value).includes('--test-isolation'), `unexpected --test-isolation reference: ${value}`);
}
});
test('no value anywhere in the whole mutate job mentions matrix.isolation', () => {
const serialized = JSON.stringify(mutateJob);
assert.ok(!serialized.includes('matrix.isolation'), 'mutate job still references matrix.isolation');
});
test("the step's run string does not contain '${{' (no direct interpolation inside run:, CONTRIBUTING.md)", () => {
assert.ok(!runStrykerStep.run.includes('${{'), 'run: block contains a direct ${{ }} interpolation');
});
test('PARITY: the env key name discovered from the workflow drives the config test directly, so the two surfaces cannot drift', async () => {
// Find the key in step.env that starts with MUTATION_TEST_ — this is read
// from the PARSED workflow document, not hardcoded, so a rename on either
// surface (the workflow's env key, or stryker.config.mjs's reader of it)
// breaks this test instead of the two silently drifting apart.
const discoveredKey = Object.keys(runStrykerStep.env).find((k) => k.startsWith('MUTATION_TEST_'));
assert.ok(discoveredKey, 'no MUTATION_TEST_* key found in the Run Stryker step env');
const config = await loadConfig({ [discoveredKey]: 'tests/frontmatter.unit.test.cjs' });
assert.deepStrictEqual(config.tap.testFiles, ['tests/frontmatter.unit.test.cjs']);
});
});