Files
msd-core/tests/configuration-migrate-config.test.cjs
0xdhx bc5c362610 fix(#2665): replace the hand-synced TEST_ENV_BASE copies with the canonical import
Eight declarations were kept in sync by hand with no parity assertion. The drift
was already in the tree: api-coverage-gate-e2e and representative-corpus declared
TERM_SESSION, but the real variable is TERM_SESSION_ID, so both scrubbed nothing
for that slot and leaked TERM_SESSION_ID into every child. Deleting the copies
removes the dead key with them -- there is no longer a second place to get wrong.

run-tests-harness keeps its local session-identity literal: that helper mirrors
the production runGsdTools to prove its CONTRACT, so it must not re-import what
it is testing. The config-LOCATION keys are a safety scrub rather than part of
that contract, so it spreads the canonical derived set and keeps the rest local.

Addresses review findings: Blocker 2, Blocker 3.
2026-08-08 05:50:17 -05:00

196 lines
7.1 KiB
JavaScript

'use strict';
/**
* Tests for `gsd-tools migrate-config` subcommand (#3536).
*
* Covers the three acceptance-criteria cases:
* 1. No-op when config is already canonical (migrated: false)
* 2. Migrates when top-level branching_strategy is present (migrated: true)
* 3. Idempotent: running twice produces no-op the second time
*
* Also covers the --raw human-readable output path.
*/
const { describe, test, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempProject, cleanup, TOOLS_PATH, TEST_ENV_BASE } = require('./helpers.cjs');
const { runNode } = require('./helpers/process-seam.cjs');
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
function runMigrateConfig(cwd, extraArgs = [], env = {}) {
const result = runNode([TOOLS_PATH, 'migrate-config', ...extraArgs], {
cwd,
env: { ...process.env, ...TEST_ENV_BASE, ...env },
timeoutMs: PROBE_TIMEOUT_MS,
});
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
status: result.exitCode,
};
}
// ─── Test 1: No-op when config is already canonical ──────────────────────────
describe('migrate-config — no-op on already-canonical config', () => {
let tmpDir;
afterEach(() => {
if (tmpDir) cleanup(tmpDir);
tmpDir = null;
});
test('returns migrated: false when no legacy keys present', () => {
tmpDir = createTempProject('gsd-migrate-noop-');
const configPath = path.join(tmpDir, '.planning', 'config.json');
fs.writeFileSync(
configPath,
JSON.stringify({
git: { branching_strategy: 'phase', base_branch: 'main' },
workflow: { research: true },
}, null, 2),
'utf-8'
);
const result = runMigrateConfig(tmpDir);
assert.equal(
result.status,
0,
`migrate-config must exit 0 on no-op — status ${result.status}, stderr: ${result.stderr}`
);
assert.equal(result.stderr.trim(), '', `No stderr expected — got: ${result.stderr}`);
const parsed = JSON.parse(result.stdout);
assert.equal(parsed.migrated, false, 'migrated must be false for canonical config');
assert.deepEqual(parsed.normalizations, [], 'normalizations must be empty for canonical config');
assert.equal(parsed.wrote, null, 'wrote must be null for no-op');
});
});
// ─── Test 2: Migrates when top-level branching_strategy is present ────────────
describe('migrate-config — migrates legacy branching_strategy', () => {
let tmpDir;
afterEach(() => {
if (tmpDir) cleanup(tmpDir);
tmpDir = null;
});
test('returns migrated: true and normalizations for top-level branching_strategy', () => {
tmpDir = createTempProject('gsd-migrate-bs-');
const configPath = path.join(tmpDir, '.planning', 'config.json');
fs.writeFileSync(
configPath,
JSON.stringify({
branching_strategy: 'milestone',
git: { base_branch: 'main' },
}, null, 2),
'utf-8'
);
const result = runMigrateConfig(tmpDir);
assert.equal(
result.status,
0,
`migrate-config must exit 0 — status ${result.status}, stderr: ${result.stderr}`
);
assert.equal(result.stderr.trim(), '', `No stderr expected — got: ${result.stderr}`);
const parsed = JSON.parse(result.stdout);
assert.equal(parsed.migrated, true, 'migrated must be true when legacy key present');
assert.ok(
parsed.normalizations.some(n => n.from === 'branching_strategy' && n.to === 'git.branching_strategy'),
`normalizations must include branching_strategy→git.branching_strategy entry. Got: ${JSON.stringify(parsed.normalizations)}`
);
assert.ok(typeof parsed.wrote === 'string', 'wrote must be a file path string');
// Verify on-disk result
const onDisk = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
assert.equal(
onDisk.git?.branching_strategy,
'milestone',
'On-disk config must have git.branching_strategy = "milestone" after migration'
);
assert.equal(
onDisk.branching_strategy,
undefined,
'On-disk config must not have top-level branching_strategy after migration'
);
});
});
// ─── Test 3: Idempotent ───────────────────────────────────────────────────────
describe('migrate-config — idempotent (running twice produces no-op)', () => {
let tmpDir;
afterEach(() => {
if (tmpDir) cleanup(tmpDir);
tmpDir = null;
});
test('second run is a no-op after first run migrated the config', () => {
tmpDir = createTempProject('gsd-migrate-idem-');
const configPath = path.join(tmpDir, '.planning', 'config.json');
fs.writeFileSync(
configPath,
JSON.stringify({
branching_strategy: 'phase',
git: { base_branch: 'main' },
}, null, 2),
'utf-8'
);
// First run — must migrate
const first = runMigrateConfig(tmpDir);
assert.equal(first.status, 0, `First run must exit 0 — status ${first.status}`);
const firstParsed = JSON.parse(first.stdout);
assert.equal(firstParsed.migrated, true, 'First run must migrate');
// Second run — must be no-op
const second = runMigrateConfig(tmpDir);
assert.equal(second.status, 0, `Second run must exit 0 — status ${second.status}`);
const secondParsed = JSON.parse(second.stdout);
assert.equal(secondParsed.migrated, false, 'Second run must be a no-op (idempotent)');
assert.deepEqual(secondParsed.normalizations, [], 'Second run normalizations must be empty');
});
});
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-321-config-defaults-clone-strategy.test.cjs — consolidation epic #1969 (B3 #1972)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-321-config-defaults-clone-strategy (consolidation epic #1969 B3 #1972)", () => {
'use strict';
const { test } = require('node:test');
const assert = require('node:assert/strict');
const configuration = require('../gsd-core/bin/lib/configuration.cjs');
test('mergeDefaults clones defaults without JSON serialization fragility (#321)', () => {
const sentinelKey = '__bug321_bigint_sentinel__';
const sentinelValue = BigInt('9007199254740993001');
configuration.CONFIG_DEFAULTS[sentinelKey] = sentinelValue;
try {
const merged = configuration.mergeDefaults({});
assert.equal(
merged[sentinelKey],
sentinelValue,
'mergeDefaults must preserve non-JSON scalar defaults when cloning'
);
}
finally {
delete configuration.CONFIG_DEFAULTS[sentinelKey];
}
});
});
}