Files
msd-core/tests/configuration-normalize-legacy-keys.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

160 lines
6.5 KiB
JavaScript

'use strict';
/**
* Tests for `normalizeLegacyKeys` block 5 — top-level `base_branch` →
* `git.base_branch` (#3648).
*
* Block 5 is new in this PR and is a fifth trigger for the write-on-normalize
* path, so it must obey the same contract #3760 established for blocks 1-3:
* hoist into an absent or object section, and REFUSE — preserving the legacy
* key, pushing no `Normalization`, and reporting via `skipped` — when the
* destination section is present but is not an object.
*
* #3760's own suite (tests/configuration-migrate-config.test.cjs) locks that
* contract for blocks 1, 2 and 3. This file locks it for block 5, which did not
* exist when that suite was written, plus block 5's ordinary hoist semantics.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fc = require('./helpers/fast-check-setup.cjs');
const { normalizeLegacyKeys } = require('../msd-core/bin/lib/configuration.cjs');
/** Keys only a string/array spread can produce. */
function numericKeys(obj) {
return Object.keys(obj).filter((k) => /^\d+$/.test(k));
}
describe('#3648 normalizeLegacyKeys block 5 — ordinary hoist semantics', () => {
test('an absent `git` section is created carrying the hoisted value', () => {
const { parsed, normalizations, skipped } = normalizeLegacyKeys({ base_branch: 'release' });
assert.deepStrictEqual(parsed.git, { base_branch: 'release' });
assert.strictEqual(parsed.base_branch, undefined, 'the stale flat key must be dropped');
assert.deepStrictEqual(skipped, []);
assert.deepStrictEqual(
normalizations.find((n) => n.from === 'base_branch'),
{ from: 'base_branch', to: 'git.base_branch', value: 'release' },
);
});
test('an existing `git` section is merged into, not replaced', () => {
const { parsed } = normalizeLegacyKeys({
git: { phase_branch_template: 'x' },
base_branch: 'release',
});
assert.deepStrictEqual(parsed.git, { phase_branch_template: 'x', base_branch: 'release' });
});
test('a null `git` section still means "absent" and is created', () => {
const { parsed, skipped } = normalizeLegacyKeys({ git: null, base_branch: 'release' });
assert.deepStrictEqual(parsed.git, { base_branch: 'release' });
assert.deepStrictEqual(skipped, [], 'null is absence, not a malformed section');
});
test('a canonical `git.base_branch` outranks the flat key, which is dropped', () => {
const { parsed, normalizations } = normalizeLegacyKeys({
git: { base_branch: 'nested' },
base_branch: 'flat',
});
assert.strictEqual(parsed.git.base_branch, 'nested', 'canonical value must win');
assert.strictEqual(parsed.base_branch, undefined);
assert.ok(normalizations.find((n) => n.from === 'base_branch'),
'an entry is still recorded so the stale flat key is written away');
});
});
describe('#3648 normalizeLegacyKeys block 5 — a non-object `git` section blocks the migration', () => {
// Same contract as #3760 blocks 1-3: preserve, refuse, report. Anything else
// is destructive, because config-loader persists whatever normalization
// produced whenever `normalizations` is non-empty.
for (const [label, section] of [
['string', 'main'],
['number', 42],
['boolean', true],
['array', ['main']],
]) {
test(`a ${label} \`git\` section: value preserved, nothing normalized, refusal reported`, () => {
const input = { git: section, base_branch: 'release' };
const { parsed, normalizations, skipped } = normalizeLegacyKeys(input);
assert.deepStrictEqual(parsed.git, section, 'the section value must survive verbatim');
assert.strictEqual(parsed.base_branch, 'release', 'the legacy key must NOT be consumed');
assert.deepStrictEqual(
normalizations.filter((n) => n.from === 'base_branch'), [],
'pushing a Normalization is what makes config-loader write the file back',
);
assert.deepStrictEqual(skipped, [{
from: 'base_branch',
to: 'git.base_branch',
section: 'git',
reason: 'non_object_section',
value: 'release',
sectionType: label,
}]);
});
}
test('negative control: the refusal is scoped to block 5, not to the whole call', () => {
// A non-object `git` must not stop an unrelated block from normalizing —
// otherwise the guard is a blanket bail-out rather than a per-section one.
const { parsed, normalizations } = normalizeLegacyKeys({
git: 'main',
base_branch: 'release',
depth: 'quick',
});
assert.strictEqual(parsed.granularity, 'coarse', 'block 4 must still run');
assert.ok(normalizations.find((n) => n.from === 'depth'));
});
});
describe('#3648 normalizeLegacyKeys block 5 — property: hoist or refuse, never corrupt', () => {
test('across arbitrary `git` values the two outcomes are exhaustive and exclusive', () => {
fc.assert(
fc.property(
fc.oneof(
fc.string(),
fc.integer(),
fc.boolean(),
fc.constant(null),
fc.array(fc.string()),
fc.dictionary(fc.string(), fc.string()),
),
fc.string({ minLength: 1 }),
(gitValue, branch) => {
const receivable = gitValue === null
|| (typeof gitValue === 'object' && !Array.isArray(gitValue));
const carriedIn = receivable && gitValue !== null ? numericKeys(gitValue) : [];
const { parsed, normalizations, skipped } =
normalizeLegacyKeys({ git: gitValue, base_branch: branch });
const hoisted = normalizations.some((n) => n.from === 'base_branch');
const refused = skipped.some((s) => s.from === 'base_branch');
assert.notStrictEqual(hoisted, refused,
'exactly one of the two outcomes must be reported for this key');
assert.strictEqual(hoisted, receivable);
if (receivable) {
assert.ok(parsed.git && typeof parsed.git === 'object' && !Array.isArray(parsed.git));
assert.strictEqual(parsed.git.base_branch, branch);
assert.strictEqual(parsed.base_branch, undefined);
// No numeric key beyond whatever the input section already had: a
// spread of a non-object is what would manufacture them.
assert.deepStrictEqual(numericKeys(parsed.git).sort(), carriedIn.sort());
} else {
assert.deepStrictEqual(parsed.git, gitValue);
assert.strictEqual(parsed.base_branch, branch);
}
},
),
{ numRuns: 300 },
);
});
});