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.
160 lines
6.5 KiB
JavaScript
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 },
|
|
);
|
|
});
|
|
});
|