Fold 94 issue-named command/module regression files into the canonical test file that owns each subject-under-test, across 52 existing suites (state, config, frontmatter, roadmap-parser, capability-registry, shell-command-projection-dispatch, plan-phase-drift-guard, health-validation, runtime-converters, commands, etc.). Verbatim block-scoped describe wrappers; 881 subtests conserved 1:1. No new test files. Host-env pre-check (per B2): the only GSD_WORKSTREAM/GSD_PROJECT-touching destinations (intel, planning-workspace) clear those vars hermetically, so folded CLI tests are safe. Regenerates regression-name allowlist (222->162), ratchets file-count allowlist across 8 buckets (validate entry removed after dropping <=2), makes 34 relocated allow-test-rule exemptions issue-ref-compliant (ADR-456; prunes 34 stale ids). Repoints CONTEXT.md + ADR-0002/443/1235/3524 test-file references. lint:ci green. Part of epic #1969. Closes #1972. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
212 lines
7.3 KiB
JavaScript
212 lines
7.3 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 { spawnSync } = require('node:child_process');
|
|
const { createTempProject, cleanup, TOOLS_PATH } = require('./helpers.cjs');
|
|
|
|
const TEST_ENV_BASE = {
|
|
GSD_SESSION_KEY: '',
|
|
CODEX_THREAD_ID: '',
|
|
CLAUDE_SESSION_ID: '',
|
|
CLAUDE_CODE_SSE_PORT: '',
|
|
OPENCODE_SESSION_ID: '',
|
|
GEMINI_SESSION_ID: '',
|
|
CURSOR_SESSION_ID: '',
|
|
WINDSURF_SESSION_ID: '',
|
|
TERM_SESSION_ID: '',
|
|
WT_SESSION: '',
|
|
TMUX_PANE: '',
|
|
ZELLIJ_SESSION_NAME: '',
|
|
TTY: '',
|
|
SSH_TTY: '',
|
|
};
|
|
|
|
function runMigrateConfig(cwd, extraArgs = [], env = {}) {
|
|
const result = spawnSync(process.execPath, [TOOLS_PATH, 'migrate-config', ...extraArgs], {
|
|
cwd,
|
|
encoding: 'utf-8',
|
|
env: { ...process.env, ...TEST_ENV_BASE, ...env },
|
|
});
|
|
return {
|
|
stdout: result.stdout || '',
|
|
stderr: result.stderr || '',
|
|
status: result.status,
|
|
};
|
|
}
|
|
|
|
// ─── 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];
|
|
}
|
|
});
|
|
});
|
|
}
|