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.
196 lines
7.1 KiB
JavaScript
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];
|
|
}
|
|
});
|
|
});
|
|
}
|