Files
msd-core/tests/config.test.cjs
Tom Boucher de3ba45d00 test(#1971): consolidate 48 gsd-tools CLI regression tests into subcommand suites
Fold 48 issue-named gsd-tools CLI regression files into the canonical test file
that owns each subcommand subject (state, roadmap, phase, milestone, audit, config,
router/dispatch, stats, verify, health, etc.), preserving every assertion and its
origin issue number as provenance (block-scoped describe wrappers, 299 subtests
conserved 1:1). No monolithic gsd-tools.test.cjs created — routes into 18 existing
per-subject suites.

Removes 48 tests/ files. Regenerates regression-name allowlist (271->231), ratchets
the file-count allowlist across 6 buckets (audit/milestone/phase/roadmap/state/verify),
and makes 10 relocated allow-test-rule exemptions issue-ref-compliant (ADR-456; prunes
10 stale ids). Repoints one CONTEXT.md symptom ref and ADR-3524's parity-test ref.
lint:ci green.

Part of epic #1969. Closes #1971.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 01:55:48 -04:00

2047 lines
81 KiB
JavaScript

/**
* GSD Tools Tests - config.cjs
*
* CLI integration tests for config-ensure-section, config-set, and config-get
* commands exercised through gsd-tools.cjs via execSync.
*
* Requirements: TEST-13
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup, delay } = require('./helpers.cjs');
// ─── helpers ──────────────────────────────────────────────────────────────────
function readConfig(tmpDir) {
const configPath = path.join(tmpDir, '.planning', 'config.json');
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
function writeConfig(tmpDir, obj) {
const configPath = path.join(tmpDir, '.planning', 'config.json');
fs.writeFileSync(configPath, JSON.stringify(obj, null, 2), 'utf-8');
}
async function runConfigEnsureSectionWithRetry(tmpDir, attempts = 4) {
let last;
for (let i = 0; i < attempts; i += 1) {
last = runGsdTools('config-ensure-section', tmpDir);
if (last.success) return last;
const detail = `${last.error || ''}\n${last.output || ''}`;
const transient = /(EPERM|EBUSY|EACCES|ENOTEMPTY|resource busy|used by another process|permission denied)/i.test(detail);
if (!transient || i === attempts - 1) return last;
await delay(150 * (i + 1));
}
return last;
}
/**
* Seed `.planning/config.json` for a test and guarantee it lands on disk
* before the test body runs.
*
* `config-ensure-section` is invoked through a spawned `gsd-tools.cjs` child.
* On the scoped CI lane (`--test-concurrency=4`, config.test.cjs scheduled
* alongside the heavy install/tarball suites) that child can be transiently
* killed under resource pressure — surfacing as a non-zero exit with empty
* stderr (an OS-level kill, not a gsd-tools application error; see the
* `runGsdTools` catch). A bare `runGsdTools('config-ensure-section')` in
* `beforeEach` swallows that failure, leaving config.json absent so the first
* subtest's `readConfig()` throws a confusing ENOENT (#770 scoped-lane flake).
*
* This retries on ANY failure or missing file (not just the EPERM/EBUSY class
* `runConfigEnsureSectionWithRetry` covers) and throws a clear diagnostic if it
* still cannot create the file, so setup is deterministic under load.
*/
async function ensureConfigReady(tmpDir, attempts = 5) {
const configPath = path.join(tmpDir, '.planning', 'config.json');
let last;
for (let i = 0; i < attempts; i += 1) {
last = runGsdTools('config-ensure-section', tmpDir);
if (last.success && fs.existsSync(configPath)) return last;
if (i < attempts - 1) await delay(150 * (i + 1));
}
throw new Error(
`config-ensure-section failed to create ${configPath} after ${attempts} attempts: ` +
`${(last && last.error) || 'unknown error'}`,
);
}
// ─── config-ensure-section ───────────────────────────────────────────────────
describe('config-ensure-section command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('creates config.json with expected structure and types', () => {
const result = runGsdTools('config-ensure-section', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.created, true);
const config = readConfig(tmpDir);
// Verify structure and types — exact values may vary if ~/.gsd/defaults.json exists
assert.strictEqual(typeof config.model_profile, 'string');
assert.strictEqual(typeof config.commit_docs, 'boolean');
assert.strictEqual(typeof config.parallelization, 'boolean');
assert.ok(config.git && typeof config.git === 'object', 'git should be an object');
assert.strictEqual(typeof config.git.branching_strategy, 'string');
assert.ok(config.workflow && typeof config.workflow === 'object', 'workflow should be an object');
assert.strictEqual(typeof config.workflow.research, 'boolean');
assert.strictEqual(typeof config.workflow.plan_check, 'boolean');
assert.strictEqual(typeof config.workflow.verifier, 'boolean');
assert.strictEqual(typeof config.workflow.nyquist_validation, 'boolean');
// These hardcoded defaults are always present (may be overridden by user defaults)
assert.ok('model_profile' in config, 'model_profile should exist');
assert.ok('brave_search' in config, 'brave_search should exist');
assert.ok('search_gitignored' in config, 'search_gitignored should exist');
});
test('is idempotent — returns already_exists on second call', async () => {
const first = await runConfigEnsureSectionWithRetry(tmpDir);
assert.ok(first.success, `First call failed: ${first.error}`);
const firstOutput = JSON.parse(first.output);
assert.strictEqual(firstOutput.created, true);
const second = await runConfigEnsureSectionWithRetry(tmpDir);
assert.ok(second.success, `Second call failed: ${second.error}`);
const secondOutput = JSON.parse(second.output);
assert.strictEqual(secondOutput.created, false);
assert.strictEqual(secondOutput.reason, 'already_exists');
});
test('detects Brave Search from file-based key', () => {
// runGsdTools sandboxes HOME=tmpDir, so brave_api_key is written there —
// no real filesystem side effects, cleanup happens via afterEach.
const gsdDir = path.join(tmpDir, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(path.join(gsdDir, 'brave_api_key'), 'test-key', 'utf-8');
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.brave_search, true);
});
test('detects Tavily Search from env var', () => {
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir, TAVILY_API_KEY: 'test-key' });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.tavily_search, true);
});
test('tavily_search is false when env var absent and no key file', () => {
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir, TAVILY_API_KEY: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.tavily_search, false);
});
test('detects Tavily Search from file-based key', () => {
const gsdDir = path.join(tmpDir, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(path.join(gsdDir, 'tavily_api_key'), 'test-key', 'utf-8');
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir, TAVILY_API_KEY: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.tavily_search, true);
});
test('detects Ref Search from env var', () => {
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir, REF_API_KEY: 'test-key' });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.ref_search, true);
});
test('ref_search is false when env var absent and no key file', () => {
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir, REF_API_KEY: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.ref_search, false);
});
test('detects Ref Search from file-based key', () => {
const gsdDir = path.join(tmpDir, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(path.join(gsdDir, 'ref_api_key'), 'test-key', 'utf-8');
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir, REF_API_KEY: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.ref_search, true);
});
test('detects Perplexity from env var', () => {
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir, PERPLEXITY_API_KEY: 'test-key' });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.perplexity, true);
});
test('perplexity is false when env var absent and no key file', () => {
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir, PERPLEXITY_API_KEY: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.perplexity, false);
});
test('detects Perplexity from file-based key', () => {
const gsdDir = path.join(tmpDir, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(path.join(gsdDir, 'perplexity_api_key'), 'test-key', 'utf-8');
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir, PERPLEXITY_API_KEY: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.perplexity, true);
});
test('detects Jina from env var', () => {
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir, JINA_API_KEY: 'test-key' });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.jina, true);
});
test('jina is false when env var absent and no key file', () => {
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir, JINA_API_KEY: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.jina, false);
});
test('detects Jina from file-based key', () => {
const gsdDir = path.join(tmpDir, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(path.join(gsdDir, 'jina_api_key'), 'test-key', 'utf-8');
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir, JINA_API_KEY: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.jina, true);
});
test('merges user defaults from defaults.json', () => {
// runGsdTools sandboxes HOME=tmpDir, so defaults.json is written there —
// no real filesystem side effects, cleanup happens via afterEach.
const gsdDir = path.join(tmpDir, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(path.join(gsdDir, 'defaults.json'), JSON.stringify({
model_profile: 'quality',
commit_docs: false,
}), 'utf-8');
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.model_profile, 'quality', 'model_profile should be overridden');
assert.strictEqual(config.commit_docs, false, 'commit_docs should be overridden');
assert.ok(config.git && typeof config.git === 'object', 'git should be an object');
assert.strictEqual(typeof config.git.branching_strategy, 'string', 'git.branching_strategy should be a string');
});
test('merges nested workflow keys from defaults.json preserving unset keys', () => {
// runGsdTools sandboxes HOME=tmpDir, so defaults.json is written there —
// no real filesystem side effects, cleanup happens via afterEach.
const gsdDir = path.join(tmpDir, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(path.join(gsdDir, 'defaults.json'), JSON.stringify({
workflow: { research: false },
}), 'utf-8');
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.research, false, 'research should be overridden');
assert.strictEqual(typeof config.workflow.plan_check, 'boolean', 'plan_check should be a boolean');
assert.strictEqual(typeof config.workflow.verifier, 'boolean', 'verifier should be a boolean');
});
});
// ─── config-set ──────────────────────────────────────────────────────────────
describe('config-set command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
// Create initial config
runGsdTools('config-ensure-section', tmpDir);
});
afterEach(() => {
cleanup(tmpDir);
});
test('sets a top-level string value', () => {
const result = runGsdTools('config-set model_profile quality', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.updated, true);
assert.strictEqual(output.key, 'model_profile');
assert.strictEqual(output.value, 'quality');
const config = readConfig(tmpDir);
assert.strictEqual(config.model_profile, 'quality');
});
test('coerces true to boolean', () => {
const result = runGsdTools('config-set commit_docs true', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.commit_docs, true);
assert.strictEqual(typeof config.commit_docs, 'boolean');
});
test('coerces false to boolean', () => {
const result = runGsdTools('config-set commit_docs false', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.commit_docs, false);
assert.strictEqual(typeof config.commit_docs, 'boolean');
});
test('coerces numeric strings to numbers', () => {
const result = runGsdTools('config-set granularity 42', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.granularity, 42);
assert.strictEqual(typeof config.granularity, 'number');
});
test('preserves plain strings', () => {
const result = runGsdTools('config-set model_profile hello', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.model_profile, 'hello');
assert.strictEqual(typeof config.model_profile, 'string');
});
test('sets nested values via dot-notation', () => {
const result = runGsdTools('config-set workflow.research false', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.research, false);
});
test('auto-creates nested objects for dot-notation', () => {
// Start with empty config
writeConfig(tmpDir, {});
const result = runGsdTools('config-set workflow.research false', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.research, false);
assert.strictEqual(typeof config.workflow, 'object');
});
test('rejects unknown config keys', () => {
const result = runGsdTools('config-set workflow.nyquist_validation_enabled false', tmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('Unknown config key'),
`Expected "Unknown config key" in error: ${result.error}`
);
});
test('sets workflow.text_mode for remote session support', () => {
writeConfig(tmpDir, {});
const result = runGsdTools('config-set workflow.text_mode true', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.text_mode, true);
});
test('sets workflow.use_worktrees to disable worktree isolation', () => {
writeConfig(tmpDir, {});
const result = runGsdTools('config-set workflow.use_worktrees false', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.use_worktrees, false);
});
test('sets git.base_branch for non-main default branches', () => {
writeConfig(tmpDir, {});
const result = runGsdTools('config-set git.base_branch master', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.git.base_branch, 'master');
});
test('sets intel.enabled to opt into the intel subsystem', () => {
writeConfig(tmpDir, {});
const result = runGsdTools('config-set intel.enabled true', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.intel.enabled, true);
});
test('errors when no key path provided', () => {
const result = runGsdTools('config-set', tmpDir);
assert.strictEqual(result.success, false);
});
test('rejects known invalid nyquist alias keys with a suggestion', () => {
const result = runGsdTools('config-set workflow.nyquist_validation_enabled false', tmpDir);
assert.strictEqual(result.success, false);
assert.match(result.error, /Unknown config key: workflow\.nyquist_validation_enabled/);
assert.match(result.error, /workflow\.nyquist_validation/);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.nyquist_validation_enabled, undefined);
assert.strictEqual(config.workflow.nyquist_validation, true);
});
});
// ─── config-get ──────────────────────────────────────────────────────────────
describe('config-get command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
// Create config with known values — sandbox HOME to avoid global defaults
runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
});
afterEach(() => {
cleanup(tmpDir);
});
test('gets a top-level value', () => {
const result = runGsdTools('config-get model_profile', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output, 'balanced');
});
test('gets a nested value via dot-notation', () => {
const result = runGsdTools('config-get workflow.research', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output, true);
});
test('errors for nonexistent key', () => {
const result = runGsdTools('config-get nonexistent_key', tmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('Key not found'),
`Expected "Key not found" in error: ${result.error}`
);
});
test('errors for deeply nested nonexistent key', () => {
const result = runGsdTools('config-get workflow.nonexistent', tmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('Key not found'),
`Expected "Key not found" in error: ${result.error}`
);
});
describe('when config.json does not exist', () => {
let emptyTmpDir;
beforeEach(() => {
emptyTmpDir = createTempProject();
});
afterEach(() => {
cleanup(emptyTmpDir);
});
test('errors when config.json does not exist', () => {
const result = runGsdTools('config-get model_profile', emptyTmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('No config.json'),
`Expected "No config.json" in error: ${result.error}`
);
});
});
test('gets git.base_branch after it is set', () => {
runGsdTools('config-set git.base_branch master', tmpDir);
const result = runGsdTools('config-get git.base_branch', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output, 'master');
});
test('errors for git.base_branch when not explicitly set', () => {
// Default config from config-ensure-section does not include git.base_branch,
// so config-get should return "Key not found" — this triggers auto-detect
// fallback in the workflow (origin/HEAD detection).
const result = runGsdTools('config-get git.base_branch', tmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('Key not found'),
`Expected "Key not found" in error: ${result.error}`
);
});
test('errors when no key path provided', () => {
const result = runGsdTools('config-get', tmpDir);
assert.strictEqual(result.success, false);
});
});
// ─── config-new-project ───────────────────────────────────────────────────────
describe('config-new-project command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('creates full config with all expected keys', () => {
const choices = JSON.stringify({
mode: 'interactive',
granularity: 'standard',
parallelization: true,
commit_docs: true,
model_profile: 'balanced',
workflow: { research: true, plan_check: true, verifier: true, nyquist_validation: true },
});
const result = runGsdTools(['config-new-project', choices], tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
// User choices present
assert.strictEqual(config.mode, 'interactive');
assert.strictEqual(config.granularity, 'standard');
assert.strictEqual(config.parallelization, true);
assert.strictEqual(config.commit_docs, true);
assert.strictEqual(config.model_profile, 'balanced');
// Defaults materialized — these were silently missing before
assert.strictEqual(typeof config.search_gitignored, 'boolean');
assert.strictEqual(typeof config.brave_search, 'boolean');
// git section present with all three keys
assert.ok(config.git && typeof config.git === 'object', 'git section should exist');
assert.strictEqual(config.git.branching_strategy, 'none');
assert.strictEqual(config.git.phase_branch_template, 'gsd/phase-{phase}-{slug}');
assert.strictEqual(config.git.milestone_branch_template, 'gsd/{milestone}-{slug}');
// workflow section present with all keys
assert.ok(config.workflow && typeof config.workflow === 'object', 'workflow section should exist');
assert.strictEqual(config.workflow.research, true);
assert.strictEqual(config.workflow.plan_check, true);
assert.strictEqual(config.workflow.verifier, true);
assert.strictEqual(config.workflow.nyquist_validation, true);
assert.strictEqual(config.workflow.auto_advance, false);
assert.strictEqual(config.workflow.node_repair, true);
assert.strictEqual(config.workflow.node_repair_budget, 2);
assert.strictEqual(config.workflow.ui_phase, true);
assert.strictEqual(config.workflow.ui_safety_gate, true);
// hooks section present
assert.ok(config.hooks && typeof config.hooks === 'object', 'hooks section should exist');
assert.strictEqual(config.hooks.context_warnings, true);
});
test('user choices override defaults', () => {
const choices = JSON.stringify({
mode: 'yolo',
granularity: 'coarse',
parallelization: false,
commit_docs: false,
model_profile: 'quality',
workflow: { research: false, plan_check: false, verifier: true, nyquist_validation: false },
});
const result = runGsdTools(['config-new-project', choices], tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.mode, 'yolo');
assert.strictEqual(config.granularity, 'coarse');
assert.strictEqual(config.parallelization, false);
assert.strictEqual(config.commit_docs, false);
assert.strictEqual(config.model_profile, 'quality');
assert.strictEqual(config.workflow.research, false);
assert.strictEqual(config.workflow.plan_check, false);
assert.strictEqual(config.workflow.verifier, true);
assert.strictEqual(config.workflow.nyquist_validation, false);
// Defaults still present for non-chosen keys
assert.strictEqual(config.git.branching_strategy, 'none');
assert.strictEqual(typeof config.search_gitignored, 'boolean');
});
test('works with empty choices — all defaults materialized', () => {
const result = runGsdTools(['config-new-project', '{}'], tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.model_profile, 'balanced');
assert.strictEqual(config.commit_docs, true);
assert.strictEqual(config.parallelization, true);
assert.strictEqual(config.search_gitignored, false);
assert.ok(config.git && typeof config.git === 'object');
assert.strictEqual(config.git.branching_strategy, 'none');
assert.ok(config.workflow && typeof config.workflow === 'object');
assert.strictEqual(config.workflow.nyquist_validation, true);
assert.strictEqual(config.workflow.auto_advance, false);
assert.strictEqual(config.workflow.node_repair, true);
assert.strictEqual(config.workflow.node_repair_budget, 2);
assert.strictEqual(config.workflow.ui_phase, true);
assert.strictEqual(config.workflow.ui_safety_gate, true);
assert.ok(config.hooks && typeof config.hooks === 'object');
assert.strictEqual(config.hooks.context_warnings, true);
});
test('is idempotent — returns already_exists if config exists', () => {
const choices = JSON.stringify({ mode: 'yolo', granularity: 'fine' });
const first = runGsdTools(['config-new-project', choices], tmpDir);
assert.ok(first.success, `First call failed: ${first.error}`);
const firstOut = JSON.parse(first.output);
assert.strictEqual(firstOut.created, true);
const second = runGsdTools(['config-new-project', choices], tmpDir);
assert.ok(second.success, `Second call failed: ${second.error}`);
const secondOut = JSON.parse(second.output);
assert.strictEqual(secondOut.created, false);
assert.strictEqual(secondOut.reason, 'already_exists');
// Config unchanged
const config = readConfig(tmpDir);
assert.strictEqual(config.mode, 'yolo');
assert.strictEqual(config.granularity, 'fine');
});
test('auto_advance in workflow choices is preserved', () => {
const choices = JSON.stringify({
mode: 'yolo',
granularity: 'standard',
workflow: { research: true, plan_check: true, verifier: true, nyquist_validation: true, auto_advance: true },
});
const result = runGsdTools(['config-new-project', choices], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.auto_advance, true);
});
test('rejects invalid JSON choices', () => {
const result = runGsdTools(['config-new-project', '{not-json}'], tmpDir);
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('Invalid JSON'), `Expected "Invalid JSON" in: ${result.error}`);
});
test('output has created:true and path on success', () => {
const choices = JSON.stringify({ mode: 'interactive', granularity: 'standard' });
const result = runGsdTools(['config-new-project', choices], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.created, true);
assert.strictEqual(out.path, '.planning/config.json');
});
});
// ─── config-set (research_before_questions and discuss_mode) ──────────────────
describe('config-set research_before_questions and discuss_mode', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
});
afterEach(() => {
cleanup(tmpDir);
});
test('workflow.research_before_questions is a valid config key', () => {
const result = runGsdTools('config-set workflow.research_before_questions true', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.research_before_questions, true);
});
test('workflow.discuss_mode is a valid config key', () => {
const result = runGsdTools('config-set workflow.discuss_mode assumptions', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.discuss_mode, 'assumptions');
});
test('research_before_questions defaults to false in new configs', () => {
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.research_before_questions, false);
});
test('discuss_mode defaults to discuss in new configs', () => {
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.discuss_mode, 'discuss');
});
test('hooks.research_questions is rejected with suggestion', () => {
const result = runGsdTools('config-set hooks.research_questions true', tmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('Unknown config key'),
`Expected "Unknown config key" in error: ${result.error}`
);
assert.ok(
result.error.includes('workflow.research_before_questions'),
`Expected suggestion for workflow.research_before_questions in error: ${result.error}`
);
});
});
// ─── config-set (additional coverage) ────────────────────────────────────────
describe('config-set unknown key (no suggestion)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
runGsdTools('config-ensure-section', tmpDir);
});
afterEach(() => {
cleanup(tmpDir);
});
test('rejects a key that has no suggestion', () => {
const result = runGsdTools('config-set totally.unknown.key value', tmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('Unknown config key'),
`Expected "Unknown config key" in error: ${result.error}`
);
});
});
// ─── config-get (additional coverage) ────────────────────────────────────────
describe('config-get edge cases', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('errors when traversing a dot-path through a non-object value', () => {
// model_profile is a string — requesting model_profile.something traverses into a non-object
writeConfig(tmpDir, { model_profile: 'balanced' });
const result = runGsdTools('config-get model_profile.something', tmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('Key not found'),
`Expected "Key not found" in error: ${result.error}`
);
});
test('errors when config.json contains malformed JSON', () => {
const configPath = path.join(tmpDir, '.planning', 'config.json');
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(configPath, '{not valid json', 'utf-8');
const result = runGsdTools('config-get model_profile', tmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('Failed to read config.json'),
`Expected "Failed to read config.json" in error: ${result.error}`
);
});
});
// ─── config-set-model-profile ─────────────────────────────────────────────────
describe('config-set-model-profile command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
});
afterEach(() => {
cleanup(tmpDir);
});
test('sets a valid profile and updates config', () => {
const result = runGsdTools('config-set-model-profile quality', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.updated, true);
assert.strictEqual(out.profile, 'quality');
assert.ok(out.agentToModelMap && typeof out.agentToModelMap === 'object');
const config = readConfig(tmpDir);
assert.strictEqual(config.model_profile, 'quality');
});
test('reports previous profile in output', () => {
const result = runGsdTools('config-set-model-profile budget', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.previousProfile, 'balanced'); // default was balanced
assert.strictEqual(out.profile, 'budget');
});
test('setting the same profile is a no-op on config but still succeeds', () => {
// Set to quality first, then set to quality again
runGsdTools('config-set-model-profile quality', tmpDir);
const result = runGsdTools('config-set-model-profile quality', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.profile, 'quality');
assert.strictEqual(out.previousProfile, 'quality');
});
test('is case-insensitive', () => {
const result = runGsdTools('config-set-model-profile BALANCED', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.model_profile, 'balanced');
});
test('rejects invalid profile', () => {
const result = runGsdTools('config-set-model-profile turbo', tmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('Invalid profile'),
`Expected "Invalid profile" in error: ${result.error}`
);
});
test('errors when no profile provided', () => {
const result = runGsdTools('config-set-model-profile', tmpDir);
assert.strictEqual(result.success, false);
});
describe('when config is missing', () => {
let emptyDir;
beforeEach(() => {
emptyDir = createTempProject();
});
afterEach(() => {
cleanup(emptyDir);
});
test('creates config if missing before setting profile', () => {
const result = runGsdTools('config-set-model-profile budget', emptyDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(emptyDir);
assert.strictEqual(config.model_profile, 'budget');
});
});
});
// ─── config-set (workflow.skip_discuss) ───────────────────────────────────────
describe('config-set workflow.skip_discuss', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
runGsdTools('config-ensure-section', tmpDir);
});
afterEach(() => {
cleanup(tmpDir);
});
test('workflow.skip_discuss is a valid config key', () => {
const result = runGsdTools('config-set workflow.skip_discuss true', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.skip_discuss, true);
});
test('skip_discuss defaults to false in new configs', () => {
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.skip_discuss, false);
});
test('skip_discuss can be toggled back to false', () => {
runGsdTools('config-set workflow.skip_discuss true', tmpDir);
const result = runGsdTools('config-set workflow.skip_discuss false', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.skip_discuss, false);
});
describe('skip_discuss in config-new-project', () => {
let emptyDir;
beforeEach(() => {
emptyDir = createTempProject();
});
afterEach(() => {
cleanup(emptyDir);
});
test('skip_discuss is present in config-new-project output', () => {
const result = runGsdTools(['config-new-project', '{}'], emptyDir, { HOME: emptyDir, USERPROFILE: emptyDir });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(emptyDir);
assert.strictEqual(config.workflow.skip_discuss, false, 'skip_discuss should default to false');
});
test('skip_discuss can be set via config-new-project choices', () => {
const choices = JSON.stringify({
workflow: { skip_discuss: true },
});
const result = runGsdTools(['config-new-project', choices], emptyDir, { HOME: emptyDir, USERPROFILE: emptyDir });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(emptyDir);
assert.strictEqual(config.workflow.skip_discuss, true);
});
});
test('config-get workflow.skip_discuss returns the set value', () => {
runGsdTools('config-set workflow.skip_discuss true', tmpDir);
const result = runGsdTools('config-get workflow.skip_discuss', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output, true);
});
});
// ─── config-set/config-get workflow.use_worktrees ────────────────────────────
describe('config-set/config-get workflow.use_worktrees', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
});
afterEach(() => {
cleanup(tmpDir);
});
test('config-get workflow.use_worktrees returns false after setting to false', () => {
runGsdTools('config-set workflow.use_worktrees false', tmpDir);
const result = runGsdTools('config-get workflow.use_worktrees', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output, false);
});
test('config-get workflow.use_worktrees errors when not set (default config)', () => {
// config-ensure-section does NOT include use_worktrees in hardcoded defaults,
// so config-get should error with "Key not found". This is the expected behavior
// that workflows rely on: the shell fallback `|| echo "true"` provides the default.
const result = runGsdTools('config-get workflow.use_worktrees', tmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('Key not found'),
`Expected "Key not found" in error: ${result.error}`
);
});
test('config-get workflow.use_worktrees returns true after setting to true', () => {
runGsdTools('config-set workflow.use_worktrees true', tmpDir);
const result = runGsdTools('config-get workflow.use_worktrees', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output, true);
});
test('use_worktrees can be toggled back and forth', () => {
runGsdTools('config-set workflow.use_worktrees false', tmpDir);
runGsdTools('config-set workflow.use_worktrees true', tmpDir);
const result = runGsdTools('config-get workflow.use_worktrees', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output, true);
});
});
// ─── config-set/config-get context ─────────────────────────────────────────
describe('config-set/config-get context', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
});
afterEach(() => {
cleanup(tmpDir);
});
test('config set context dev succeeds', () => {
const result = runGsdTools('config-set context dev', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.context, 'dev');
});
test('config set context research succeeds', () => {
const result = runGsdTools('config-set context research', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.context, 'research');
});
test('config set context review succeeds', () => {
const result = runGsdTools('config-set context review', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.context, 'review');
});
test('config get context returns the set value', () => {
runGsdTools('config-set context dev', tmpDir);
const result = runGsdTools('config-get context', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output, 'dev');
});
test('config set context rejects invalid values', () => {
const result = runGsdTools('config-set context foobar', tmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('Invalid context value'),
`Expected "Invalid context value" in error: ${result.error}`
);
});
test('all three context profile files exist', () => {
const contextsDir = path.join(__dirname, '..', 'gsd-core', 'contexts');
assert.ok(fs.existsSync(path.join(contextsDir, 'dev.md')), 'dev.md should exist');
assert.ok(fs.existsSync(path.join(contextsDir, 'research.md')), 'research.md should exist');
assert.ok(fs.existsSync(path.join(contextsDir, 'review.md')), 'review.md should exist');
});
});
// ─── config-path (#2282) ────────────────────────────────────────────────────
describe('config-path command (#2282)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
runGsdTools('config-ensure-section', tmpDir);
});
afterEach(() => {
cleanup(tmpDir);
});
test('returns root config path when no workstream is active', () => {
const result = runGsdTools('config-path', tmpDir);
assert.ok(result.success, `config-path failed: ${result.error}`);
// Normalize separators: Windows emits backslashes in the resolved path.
assert.ok(result.output.trim().replace(/\\/g, '/').endsWith('.planning/config.json'), `expected root config path, got: ${result.output}`);
assert.ok(!result.output.includes('workstreams'), 'should not include workstreams in path');
});
test('returns workstream config path when GSD_WORKSTREAM is set', () => {
const result = runGsdTools('config-path', tmpDir, { GSD_WORKSTREAM: 'my-stream' });
assert.ok(result.success, `config-path failed: ${result.error}`);
assert.ok(result.output.trim().replace(/\\/g, '/').includes('workstreams/my-stream/config.json'), `expected workstream config path, got: ${result.output}`);
});
test('config-path and config-get agree on the active path', () => {
// Write a value via config-set (uses planningDir internally)
runGsdTools('config-set model_profile quality', tmpDir);
// config-path should point to a file containing that value
const pathResult = runGsdTools('config-path', tmpDir);
const configPath = pathResult.output.trim();
const configContent = JSON.parse(require('fs').readFileSync(configPath, 'utf-8'));
assert.strictEqual(configContent.model_profile, 'quality', 'config-path should point to the file config-set wrote');
});
});
// ─── config-set prototype-pollution guard (#663) ─────────────────────────────
describe('config-set prototype-pollution guard (#663)', () => {
let tmpDir;
beforeEach(async () => {
tmpDir = createTempProject();
// Initialise config so there is a config.json to write to. Retry + assert
// so a transient config-ensure-section child failure under scoped-lane load
// cannot leave config.json absent (#770).
await ensureConfigReady(tmpDir);
});
afterEach(() => {
cleanup(tmpDir);
});
test('rejects __proto__ key segment and does not pollute Object.prototype', () => {
const result = runGsdTools('config-set __proto__.polluted true', tmpDir);
assert.strictEqual(result.success, false, `Expected failure but got: ${result.output}`);
// No prototype pollution.
assert.strictEqual(({}).polluted, undefined, '__proto__ pollution: {}.polluted should be undefined');
assert.strictEqual(Object.prototype.polluted, undefined, '__proto__ pollution: Object.prototype.polluted should be undefined');
// Confirm .planning/config.json does not have a 'polluted' property at any level.
const config = readConfig(tmpDir);
assert.strictEqual(Object.prototype.hasOwnProperty.call(config, 'polluted'), false,
'config.json root must not gain a "polluted" key');
});
test('rejects constructor.prototype key and does not pollute Object.prototype', () => {
const result = runGsdTools('config-set constructor.prototype.polluted2 true', tmpDir);
assert.strictEqual(result.success, false, `Expected failure but got: ${result.output}`);
assert.strictEqual(({}).polluted2, undefined, 'constructor chain pollution: {}.polluted2 should be undefined');
assert.strictEqual(Object.prototype.polluted2, undefined,
'constructor chain pollution: Object.prototype.polluted2 should be undefined');
});
test('rejects bare prototype key segment', () => {
const result = runGsdTools('config-set prototype.x true', tmpDir);
assert.strictEqual(result.success, false, `Expected failure but got: ${result.output}`);
assert.strictEqual(Object.prototype.x, undefined, 'prototype.x should not leak onto Object.prototype');
});
test('positive control: legitimate nested key workflow.research succeeds', () => {
const result = runGsdTools('config-set workflow.research true', tmpDir);
assert.ok(result.success, `Legitimate key rejected unexpectedly: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.research, true, 'workflow.research should be written to config.json');
});
});
// ─── config-set prototype-pollution guard via dynamic-key prefixes (alert #26) ─
describe('config-set prototype-pollution guard via dynamic-key prefixes (alert #26)', () => {
let tmpDir;
beforeEach(async () => {
tmpDir = createTempProject();
// Initialise config so there is a config.json to write to. Retry + assert
// so a transient config-ensure-section child failure under scoped-lane load
// cannot leave config.json absent (#770).
await ensureConfigReady(tmpDir);
});
afterEach(() => {
cleanup(tmpDir);
});
test('agent_skills.__proto__ is blocked by setConfigValue guard (not schema gate)', () => {
const result = runGsdTools('config-set agent_skills.__proto__ somevalue', tmpDir);
assert.strictEqual(result.success, false, `Expected failure but got: ${result.output}`);
// Must be the pollution guard, not the schema gate.
assert.ok(
result.error.includes('prototype pollution guard'),
`Expected "prototype pollution guard" in error, got: ${result.error}`,
);
// No schema-gate message.
assert.ok(
!result.error.includes('Unknown config key'),
`Should not hit schema gate, got: ${result.error}`,
);
// No prototype pollution occurred.
assert.strictEqual(({}).somevalue, undefined, 'agent_skills.__proto__: {}.somevalue should be undefined');
assert.strictEqual(Object.prototype.hasOwnProperty.call(Object.prototype, 'somevalue'), false,
'agent_skills.__proto__: Object.prototype should not gain "somevalue"');
});
test('agent_skills.constructor is blocked by setConfigValue guard (not schema gate)', () => {
const result = runGsdTools('config-set agent_skills.constructor somevalue', tmpDir);
assert.strictEqual(result.success, false, `Expected failure but got: ${result.output}`);
assert.ok(
result.error.includes('prototype pollution guard'),
`Expected "prototype pollution guard" in error, got: ${result.error}`,
);
assert.ok(
!result.error.includes('Unknown config key'),
`Should not hit schema gate, got: ${result.error}`,
);
assert.strictEqual(Object.prototype.hasOwnProperty.call(Object.prototype, 'somevalue'), false,
'agent_skills.constructor: Object.prototype should not gain "somevalue"');
});
test('agent_skills.prototype is blocked by setConfigValue guard (not schema gate)', () => {
const result = runGsdTools('config-set agent_skills.prototype somevalue', tmpDir);
assert.strictEqual(result.success, false, `Expected failure but got: ${result.output}`);
assert.ok(
result.error.includes('prototype pollution guard'),
`Expected "prototype pollution guard" in error, got: ${result.error}`,
);
assert.ok(
!result.error.includes('Unknown config key'),
`Should not hit schema gate, got: ${result.error}`,
);
assert.strictEqual(Object.prototype.hasOwnProperty.call(Object.prototype, 'somevalue'), false,
'agent_skills.prototype: Object.prototype should not gain "somevalue"');
});
test('features.__proto__ is blocked by setConfigValue guard (not schema gate)', () => {
const result = runGsdTools('config-set features.__proto__ somevalue', tmpDir);
assert.strictEqual(result.success, false, `Expected failure but got: ${result.output}`);
assert.ok(
result.error.includes('prototype pollution guard'),
`Expected "prototype pollution guard" in error, got: ${result.error}`,
);
assert.ok(
!result.error.includes('Unknown config key'),
`Should not hit schema gate, got: ${result.error}`,
);
assert.strictEqual(({}).somevalue, undefined, 'features.__proto__: {}.somevalue should be undefined');
assert.strictEqual(Object.prototype.hasOwnProperty.call(Object.prototype, 'somevalue'), false,
'features.__proto__: Object.prototype should not gain "somevalue"');
});
test('review.models.constructor is blocked by setConfigValue guard (not schema gate)', () => {
const result = runGsdTools('config-set review.models.constructor somevalue', tmpDir);
assert.strictEqual(result.success, false, `Expected failure but got: ${result.output}`);
assert.ok(
result.error.includes('prototype pollution guard'),
`Expected "prototype pollution guard" in error, got: ${result.error}`,
);
assert.ok(
!result.error.includes('Unknown config key'),
`Should not hit schema gate, got: ${result.error}`,
);
assert.strictEqual(Object.prototype.hasOwnProperty.call(Object.prototype, 'somevalue'), false,
'review.models.constructor: Object.prototype should not gain "somevalue"');
});
test('positive control: agent_skills.sonnet-coder with valid value succeeds', () => {
const result = runGsdTools('config-set agent_skills.sonnet-coder true', tmpDir);
assert.ok(result.success, `Legitimate agent_skills key rejected unexpectedly: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.agent_skills['sonnet-coder'], true,
'agent_skills.sonnet-coder should be written to config.json');
});
});
// ─── plan_review.source_grounding + _authority (#22) ─────────────────────────
describe('plan_review.source_grounding and plan_review.source_grounding_authority (#22)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
});
afterEach(() => {
cleanup(tmpDir);
});
// (a) Default of plan_review.source_grounding is true
test('plan_review.source_grounding defaults to true when not set in config.json', () => {
const config = readConfig(tmpDir);
assert.strictEqual(
config.plan_review?.source_grounding,
true,
'plan_review.source_grounding must default to true'
);
});
// (b) Default of plan_review.source_grounding_authority is "grep"
test('plan_review.source_grounding_authority defaults to "grep" when not set in config.json', () => {
const config = readConfig(tmpDir);
assert.strictEqual(
config.plan_review?.source_grounding_authority,
'grep',
'plan_review.source_grounding_authority must default to "grep"'
);
});
// (c) Both keys are recognized as valid config keys
test('plan_review.source_grounding is a valid config key accepted by config-set', () => {
const result = runGsdTools('config-set plan_review.source_grounding false', tmpDir);
assert.ok(result.success, `config-set plan_review.source_grounding failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.plan_review.source_grounding, false);
});
test('plan_review.source_grounding_authority is a valid config key accepted by config-set', () => {
const result = runGsdTools('config-set plan_review.source_grounding_authority intel', tmpDir);
assert.ok(result.success, `config-set plan_review.source_grounding_authority failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.plan_review.source_grounding_authority, 'intel');
});
// Enum positive: all valid authority values
test('plan_review.source_grounding_authority accepts all valid enum values', () => {
const validValues = ['grep', 'intel', 'treesitter', 'lsp', 'scip'];
for (const v of validValues) {
const result = runGsdTools(`config-set plan_review.source_grounding_authority ${v}`, tmpDir);
assert.ok(result.success, `config-set plan_review.source_grounding_authority ${v} failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.plan_review.source_grounding_authority, v);
}
});
// (d) NEGATIVE MATRIX — invalid enum values are rejected
test('plan_review.source_grounding_authority rejects invalid value "bogus"', () => {
const result = runGsdTools('config-set plan_review.source_grounding_authority bogus', tmpDir);
assert.strictEqual(result.success, false, 'bogus should be rejected');
assert.ok(
result.error.includes('Invalid plan_review.source_grounding_authority'),
`Expected "Invalid plan_review.source_grounding_authority" in error: ${result.error}`
);
});
test('plan_review.source_grounding_authority rejects flag-looking value "--grep"', () => {
const result = runGsdTools(['config-set', 'plan_review.source_grounding_authority', '--grep'], tmpDir);
assert.strictEqual(result.success, false, '--grep should be rejected');
assert.ok(
result.error.includes('Invalid plan_review.source_grounding_authority'),
`Expected "Invalid plan_review.source_grounding_authority" in error: ${result.error}`
);
});
test('plan_review.source_grounding_authority rejects empty string', () => {
const result = runGsdTools(['config-set', 'plan_review.source_grounding_authority', ''], tmpDir);
assert.strictEqual(result.success, false, 'empty string should be rejected');
});
test('plan_review.source_grounding rejects non-boolean value "yes"', () => {
const result = runGsdTools('config-set plan_review.source_grounding yes', tmpDir);
assert.strictEqual(result.success, false, '"yes" should be rejected as non-boolean');
assert.ok(
result.error.includes('Invalid plan_review.source_grounding'),
`Expected "Invalid plan_review.source_grounding" in error: ${result.error}`
);
});
test('plan_review.source_grounding rejects numeric value 1', () => {
const result = runGsdTools('config-set plan_review.source_grounding 1', tmpDir);
assert.strictEqual(result.success, false, 'numeric 1 should be rejected as non-boolean');
assert.ok(
result.error.includes('Invalid plan_review.source_grounding'),
`Expected "Invalid plan_review.source_grounding" in error: ${result.error}`
);
});
});
// ─── config-set workflow.test_command (#1216) ────────────────────────────────
describe('config-set workflow.test_command (#1216)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
runGsdTools('config-ensure-section', tmpDir);
});
afterEach(() => {
cleanup(tmpDir);
});
test('config-set accepts workflow.test_command', () => {
const result = runGsdTools(['config-set', 'workflow.test_command', 'npm test'], tmpDir);
assert.ok(result.success, `config-set should accept workflow.test_command: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow?.test_command, 'npm test', 'value must be persisted');
});
test('config-set workflow.test_command persists a custom make command', () => {
const result = runGsdTools(['config-set', 'workflow.test_command', 'make test'], tmpDir);
assert.ok(result.success, `config-set should accept workflow.test_command: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow?.test_command, 'make test', 'make test must be persisted');
});
test('config-get workflow.test_command returns the set value', () => {
runGsdTools(['config-set', 'workflow.test_command', 'cargo test'], tmpDir);
const result = runGsdTools('config-get workflow.test_command', tmpDir);
assert.ok(result.success, `config-get should return workflow.test_command: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output, 'cargo test', 'config-get must return the persisted value');
});
test('config-set accepts workflow.build_command', () => {
const result = runGsdTools(['config-set', 'workflow.build_command', 'npm run build'], tmpDir);
assert.ok(result.success, `config-set should accept workflow.build_command: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow?.build_command, 'npm run build', 'value must be persisted');
});
});
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-2601-inherit-model-profile.test.cjs — consolidation epic #1969 (B2 #1971)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-2601-inherit-model-profile (consolidation epic #1969 B2 #1971)", () => {
'use strict';
/**
* Regression tests for bug #2601
*
* `config-set-model-profile inherit` (and `config-set model_profile inherit`)
* was rejected by the validator even though the runtime accepts 'inherit' as a
* valid model_profile value meaning "inherit from parent configuration".
*
* Root cause: VALID_PROFILES in model-profiles.cjs is derived from
* Object.keys(MODEL_PROFILES['gsd-planner']), which does not include 'inherit'.
* cmdConfigSetModelProfile() rejects any value not in VALID_PROFILES.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
describe('bug #2601: config-set-model-profile accepts inherit', () => {
test('config-set-model-profile inherit succeeds', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(['config-set-model-profile', 'inherit'], tmpDir);
assert.ok(result.success, `should accept inherit: ${result.error}`);
});
test('config-set model_profile inherit succeeds', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(['config-set', 'model_profile', 'inherit'], tmpDir);
assert.ok(result.success, `config-set model_profile inherit should succeed: ${result.error}`);
});
test('config-set-model-profile inherit writes inherit to config', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
runGsdTools(['config-set-model-profile', 'inherit'], tmpDir);
const getResult = runGsdTools(['config-get', 'model_profile'], tmpDir);
assert.ok(getResult.success, `config-get should succeed: ${getResult.error}`);
assert.strictEqual(JSON.parse(getResult.output), 'inherit');
});
test('config-set-model-profile still rejects truly invalid profiles', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(['config-set-model-profile', 'not-a-real-profile'], tmpDir);
assert.ok(!result.success, 'should reject invalid profiles');
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3197-gsd-tools-config-whitelist.test.cjs — consolidation epic #1969 (B2 #1971)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3197-gsd-tools-config-whitelist (consolidation epic #1969 B2 #1971)", () => {
'use strict';
/**
* Regression test for #3197 — gsd-tools config-set rejects workflow._auto_chain_active.
*
* Root cause: RUNTIME_STATE_KEYS was added to sdk/src/query/config-schema.ts in #3162
* but not to gsd-core/bin/lib/config-schema.cjs, so gsd-tools.cjs users still hit
* "Unknown config key" when setting workflow._auto_chain_active.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
describe('#3197 — gsd-tools.cjs config-set workflow._auto_chain_active', () => {
test('config-set workflow._auto_chain_active true succeeds via gsd-tools.cjs (CJS path)', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(['config-set', 'workflow._auto_chain_active', 'true'], tmpDir);
assert.ok(
result.success,
`config-set workflow._auto_chain_active true should succeed, got:\nstdout: ${result.output}\nstderr: ${result.error}`
);
});
test('config-set workflow._auto_chain_active true writes value to config.json', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
runGsdTools(['config-set', 'workflow._auto_chain_active', 'true'], tmpDir);
const configPath = path.join(tmpDir, '.planning', 'config.json');
assert.ok(fs.existsSync(configPath), '.planning/config.json must exist after config-set');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
assert.ok(
config.workflow !== undefined && config.workflow._auto_chain_active === true,
`Expected workflow._auto_chain_active: true in config.json, got: ${JSON.stringify(config)}`
);
});
test('config-set workflow._auto_chain_active false writes false to config.json', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
runGsdTools(['config-set', 'workflow._auto_chain_active', 'false'], tmpDir);
const configPath = path.join(tmpDir, '.planning', 'config.json');
assert.ok(fs.existsSync(configPath), '.planning/config.json must exist after config-set');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
assert.ok(
config.workflow !== undefined && config.workflow._auto_chain_active === false,
`Expected workflow._auto_chain_active: false in config.json, got: ${JSON.stringify(config)}`
);
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3086-git-create-tag-config-gate.test.cjs — consolidation epic #1969 (B2 #1971)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3086-git-create-tag-config-gate (consolidation epic #1969 B2 #1971)", () => {
// allow-test-rule: workflow-markdown-is-the-runtime-contract (see #3086)
// Justification: complete-milestone.md IS the runtime — the agent reads and
// follows it directly. Asserting the <config-check> block is present in the
// markdown is the only way to verify the gate is wired. Per CONTEXT.md L611.
'use strict';
/**
* #3086 — git.create_tag config gate for milestone tagging.
*
* Tests:
* A. Default value: fresh project returns `true` for git.create_tag
* B. config-set false → config-get returns false
* C. Invalid value (e.g. "maybe") is rejected by schema validator
* D. complete-milestone.md workflow contains the <config-check> gate for git.create_tag
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
const WORKFLOW_PATH = path.join(
__dirname,
'..',
'gsd-core',
'workflows',
'complete-milestone.md',
);
describe('#3086: git.create_tag config key', () => {
test('A. fresh project: config-get git.create_tag returns true (default)', (t) => {
const tmpDir = createTempProject('gsd-3086-default-');
t.after(() => cleanup(tmpDir));
const result = runGsdTools(['config-get', 'git.create_tag'], tmpDir, { HOME: tmpDir });
assert.ok(result.success, `config-get git.create_tag failed:\n${result.error}`);
assert.strictEqual(
result.output.trim(),
'true',
`Expected default value 'true', got: '${result.output.trim()}'`,
);
});
test('B. config-set git.create_tag false → config-get returns false', (t) => {
const tmpDir = createTempProject('gsd-3086-set-false-');
t.after(() => cleanup(tmpDir));
const setResult = runGsdTools(['config-set', 'git.create_tag', 'false'], tmpDir, {
HOME: tmpDir,
});
assert.ok(setResult.success, `config-set git.create_tag false failed:\n${setResult.error}`);
const getResult = runGsdTools(['config-get', 'git.create_tag'], tmpDir, { HOME: tmpDir });
assert.ok(getResult.success, `config-get after set failed:\n${getResult.error}`);
assert.strictEqual(
getResult.output.trim(),
'false',
`Expected 'false' after set, got: '${getResult.output.trim()}'`,
);
});
test('C. config-set git.create_tag with invalid value "maybe" is rejected', (t) => {
const tmpDir = createTempProject('gsd-3086-invalid-');
t.after(() => cleanup(tmpDir));
const result = runGsdTools(['config-set', 'git.create_tag', 'maybe'], tmpDir, {
HOME: tmpDir,
});
assert.ok(
!result.success,
`Expected config-set to fail for invalid value "maybe", but it succeeded`,
);
});
test('D. complete-milestone.md contains <config-check> gate for git.create_tag', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf8');
assert.ok(
content.includes('git.create_tag'),
'complete-milestone.md must reference git.create_tag in a <config-check> block',
);
assert.ok(
content.includes('<config-check>'),
'complete-milestone.md must have a <config-check> block in the git_tag step',
);
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/feat-3167-ship-pr-body-sections.test.cjs — consolidation epic #1969 (B2 #1971)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:feat-3167-ship-pr-body-sections (consolidation epic #1969 B2 #1971)", () => {
/**
* Regression tests for issue #3167: configurable /gsd-ship PR body sections.
*/
// allow-test-rule: source-text-is-the-product (see #3167)
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { describe, test, afterEach } = require('node:test');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const repoRoot = path.resolve(__dirname, '..');
const tmpDirs = [];
function readRepoFile(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
}
function makeProject() {
const tmpDir = createTempProject('gsd-3167-');
tmpDirs.push(tmpDir);
return tmpDir;
}
afterEach(() => {
while (tmpDirs.length) {
cleanup(tmpDirs.pop());
}
});
describe('ship.pr_body_sections config (#3167)', () => {
test('CLI config-set accepts additional PR body section arrays', () => {
const cwd = makeProject();
const value = JSON.stringify([
{
heading: 'Risks & Rollback',
enabled: true,
source: 'PLAN.md ## Risks || PLAN.md ## Rollback',
fallback: '- Rollback: revert this PR.',
},
{
heading: 'Stakeholder Sign-off',
enabled: false,
template: '- Product owner: pending',
},
]);
const result = runGsdTools(['config-set', 'ship.pr_body_sections', value, '--raw'], cwd, { HOME: cwd });
assert.equal(result.success, true, result.error);
const config = JSON.parse(fs.readFileSync(path.join(cwd, '.planning', 'config.json'), 'utf8'));
assert.deepEqual(config.ship.pr_body_sections, [
{
heading: 'Risks & Rollback',
enabled: true,
source: 'PLAN.md ## Risks || PLAN.md ## Rollback',
fallback: '- Rollback: revert this PR.',
},
{
heading: 'Stakeholder Sign-off',
enabled: false,
template: '- Product owner: pending',
},
]);
});
test('CLI config-set rejects malformed PR body section values before writing config', () => {
const cwd = makeProject();
const notArray = runGsdTools(
['config-set', 'ship.pr_body_sections', JSON.stringify({ heading: 'Not an array' }), '--raw'],
cwd,
{ HOME: cwd }
);
assert.equal(notArray.success, false);
assert.match(notArray.error, /ship\.pr_body_sections.*JSON array/);
const missingHeading = runGsdTools(
['config-set', 'ship.pr_body_sections', JSON.stringify([{ fallback: '- Missing heading' }]), '--raw'],
cwd,
{ HOME: cwd }
);
assert.equal(missingHeading.success, false);
assert.match(missingHeading.error, /heading/);
const invalidEnabled = runGsdTools(
['config-set', 'ship.pr_body_sections', JSON.stringify([{ heading: 'Toggle', enabled: 'yes', fallback: '- item' }]), '--raw'],
cwd,
{ HOME: cwd }
);
assert.equal(invalidEnabled.success, false);
assert.match(invalidEnabled.error, /enabled/);
assert.equal(fs.existsSync(path.join(cwd, '.planning', 'config.json')), false);
});
test('CLI config-new-project validates onboarded PR body sections before writing config', () => {
const cwd = makeProject();
const choices = JSON.stringify({
ship: {
pr_body_sections: [
{
heading: 'Invalid source',
source: 'package.json ## Scripts',
},
],
},
});
const result = runGsdTools(['config-new-project', choices], cwd, { HOME: cwd });
assert.equal(result.success, false);
assert.match(result.error, /source must use selectors/);
assert.equal(fs.existsSync(path.join(cwd, '.planning', 'config.json')), false);
});
test('ship workflow composes configured sections as append-only extensions', () => {
const workflow = readRepoFile('gsd-core/workflows/ship.md');
assert.match(workflow, /config-get ship\.pr_body_sections --default '\[\]'/);
assert.match(workflow, /append-only/i);
assert.match(workflow, /enabled.*false/i);
assert.match(workflow, /cannot replace/i);
assert.match(workflow, /Summary[\s\S]*Changes[\s\S]*Requirements Addressed[\s\S]*Verification[\s\S]*Key Decisions/);
assert.match(workflow, /\{phase_number\}[\s\S]*\{phase_name\}[\s\S]*\{phase_dir\}[\s\S]*\{base_branch\}[\s\S]*\{padded_phase\}/);
assert.match(workflow, /User Stories & Acceptance Criteria/);
assert.match(workflow, /Definition of Done/);
assert.match(workflow, /--body-file/);
assert.match(workflow, /trap 'rm -f "\$\{PR_BODY_FILE:-\}"' EXIT/);
});
test('default config and documentation describe ship.pr_body_sections', () => {
const template = JSON.parse(readRepoFile('gsd-core/templates/config.json'));
assert.deepEqual(template.ship.pr_body_sections, []);
const docs = readRepoFile('docs/CONFIGURATION.md');
assert.match(docs, /`ship\.pr_body_sections`/);
assert.match(docs, /additional PR body sections/i);
assert.match(docs, /append-only/i);
assert.match(docs, /lean\/agile PRD/i);
assert.match(docs, /Definition of Done/);
const planningConfig = readRepoFile('gsd-core/references/planning-config.md');
assert.match(planningConfig, /ship\.pr_body_sections/);
});
test('new-project onboarding can seed enabled or disabled PR body sections', () => {
const workflow = readRepoFile('gsd-core/workflows/new-project.md');
assert.match(workflow, /ship\.pr_body_sections/);
assert.match(workflow, /enabled.*true/);
assert.match(workflow, /enabled.*false/);
assert.match(workflow, /User Stories & Acceptance Criteria/);
assert.match(workflow, /Risks & Dependencies/);
assert.match(workflow, /Success Metrics & Release Criteria/);
assert.match(workflow, /Stakeholder Review & Approval/);
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/feat-3210-fallow-schema-enum.test.cjs — consolidation epic #1969 (B2 #1971)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:feat-3210-fallow-schema-enum (consolidation epic #1969 B2 #1971)", () => {
'use strict';
/**
* Enum validation for code_quality.fallow.scope and code_quality.fallow.profile.
*
* Fixes H5 from #3424 review: config-set silently accepted invalid enum values
* (e.g. scope=fullrepo) and fell through to default behavior. This test asserts
* that invalid values are rejected with a helpful error, and valid values pass.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
describe('feat-3210 / H5: enum validation for code_quality.fallow.scope and .profile', () => {
// --- code_quality.fallow.scope ---
test('config-set code_quality.fallow.scope=fullrepo is REJECTED with helpful error', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.scope', 'fullrepo'],
tmpDir
);
assert.ok(
!result.success,
'config-set code_quality.fallow.scope=fullrepo must fail, but it succeeded'
);
const combined = (result.output || '') + (result.error || '');
assert.ok(
combined.includes('phase') && combined.includes('repo'),
`Error message must mention valid values "phase" and "repo", got: ${combined}`
);
});
test('config-set code_quality.fallow.scope=phase is ACCEPTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.scope', 'phase'],
tmpDir
);
assert.ok(
result.success,
[
'config-set code_quality.fallow.scope=phase must succeed,',
'stdout: ' + result.output,
'stderr: ' + result.error,
].join('\n')
);
});
test('config-set code_quality.fallow.scope=repo is ACCEPTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.scope', 'repo'],
tmpDir
);
assert.ok(
result.success,
[
'config-set code_quality.fallow.scope=repo must succeed,',
'stdout: ' + result.output,
'stderr: ' + result.error,
].join('\n')
);
});
test('config-set code_quality.fallow.scope=PHASE (wrong case) is REJECTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.scope', 'PHASE'],
tmpDir
);
assert.ok(
!result.success,
'config-set code_quality.fallow.scope=PHASE must fail (values are case-sensitive)'
);
});
// --- code_quality.fallow.profile ---
test('config-set code_quality.fallow.profile=aggressive is REJECTED with helpful error', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.profile', 'aggressive'],
tmpDir
);
assert.ok(
!result.success,
'config-set code_quality.fallow.profile=aggressive must fail, but it succeeded'
);
const combined = (result.output || '') + (result.error || '');
assert.ok(
combined.includes('minimal') && combined.includes('standard') && combined.includes('strict'),
`Error message must mention valid values "minimal", "standard", "strict", got: ${combined}`
);
});
test('config-set code_quality.fallow.profile=minimal is ACCEPTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.profile', 'minimal'],
tmpDir
);
assert.ok(
result.success,
[
'config-set code_quality.fallow.profile=minimal must succeed,',
'stdout: ' + result.output,
'stderr: ' + result.error,
].join('\n')
);
});
test('config-set code_quality.fallow.profile=standard is ACCEPTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.profile', 'standard'],
tmpDir
);
assert.ok(
result.success,
[
'config-set code_quality.fallow.profile=standard must succeed,',
'stdout: ' + result.output,
'stderr: ' + result.error,
].join('\n')
);
});
test('config-set code_quality.fallow.profile=strict is ACCEPTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.profile', 'strict'],
tmpDir
);
assert.ok(
result.success,
[
'config-set code_quality.fallow.profile=strict must succeed,',
'stdout: ' + result.output,
'stderr: ' + result.error,
].join('\n')
);
});
test('config-set code_quality.fallow.profile=unknown is REJECTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.profile', 'unknown'],
tmpDir
);
assert.ok(
!result.success,
'config-set code_quality.fallow.profile=unknown must fail'
);
});
});
});
}