Add `config-new-project` CLI command that writes a complete,
fully-materialized `.planning/config.json` with sane defaults
instead of the previous partial template (6-7 user-chosen keys
only). Unset keys are no longer silently resolved at read time —
every key GSD reads is written explicitly at project creation.
Previously, missing keys were resolved silently by loadConfig()
defaults, making the effective config non-discoverable. Now every
key that GSD reads is written explicitly at project creation.
- buildNewProjectConfig() — single source of truth for all
defaults; merges hardcoded ← ~/.gsd/defaults.json ← user choices
- ensureConfigFile() refactored to reuse buildNewProjectConfig({})
instead of duplicating default logic (~40 lines removed)
- new-project.md Steps 2a and 5 updated to call config-new-project
instead of writing a hardcoded partial JSON template
- Test coverage for config.cjs: 78.96% → 93.81% statements,
100% functions; adds config-set-model-profile test suite
FIXES:
- VALID_CONFIG_KEYS extended with workflow.auto_advance,
workflow.node_repair, workflow.node_repair_budget,
hooks.context_warnings — these keys had hardcoded defaults
but were not settable via config-set
680 lines
26 KiB
JavaScript
680 lines
26 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');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const { runGsdTools, createTempProject, cleanup } = 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');
|
|
}
|
|
|
|
// ─── 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', () => {
|
|
const first = runGsdTools('config-ensure-section', tmpDir);
|
|
assert.ok(first.success, `First call failed: ${first.error}`);
|
|
const firstOutput = JSON.parse(first.output);
|
|
assert.strictEqual(firstOutput.created, true);
|
|
|
|
const second = runGsdTools('config-ensure-section', 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');
|
|
});
|
|
|
|
// NOTE: This test touches ~/.gsd/ on the real filesystem. It uses save/restore
|
|
// try/finally and skips if the file already exists to avoid corrupting user config.
|
|
test('detects Brave Search from file-based key', () => {
|
|
const homedir = os.homedir();
|
|
const gsdDir = path.join(homedir, '.gsd');
|
|
const braveKeyFile = path.join(gsdDir, 'brave_api_key');
|
|
|
|
// Skip if file already exists (don't mess with user's real config)
|
|
if (fs.existsSync(braveKeyFile)) {
|
|
return;
|
|
}
|
|
|
|
// Create .gsd dir and brave_api_key file
|
|
const gsdDirExisted = fs.existsSync(gsdDir);
|
|
try {
|
|
if (!gsdDirExisted) {
|
|
fs.mkdirSync(gsdDir, { recursive: true });
|
|
}
|
|
fs.writeFileSync(braveKeyFile, 'test-key', 'utf-8');
|
|
|
|
const result = runGsdTools('config-ensure-section', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const config = readConfig(tmpDir);
|
|
assert.strictEqual(config.brave_search, true);
|
|
} finally {
|
|
// Clean up
|
|
try { fs.unlinkSync(braveKeyFile); } catch { /* ignore */ }
|
|
if (!gsdDirExisted) {
|
|
try { fs.rmdirSync(gsdDir); } catch { /* ignore if not empty */ }
|
|
}
|
|
}
|
|
});
|
|
|
|
// NOTE: This test touches ~/.gsd/ on the real filesystem. It uses save/restore
|
|
// try/finally and skips if the file already exists to avoid corrupting user config.
|
|
test('merges user defaults from defaults.json', () => {
|
|
const homedir = os.homedir();
|
|
const gsdDir = path.join(homedir, '.gsd');
|
|
const defaultsFile = path.join(gsdDir, 'defaults.json');
|
|
|
|
// Save existing defaults if present
|
|
let existingDefaults = null;
|
|
const gsdDirExisted = fs.existsSync(gsdDir);
|
|
if (fs.existsSync(defaultsFile)) {
|
|
existingDefaults = fs.readFileSync(defaultsFile, 'utf-8');
|
|
}
|
|
|
|
try {
|
|
if (!gsdDirExisted) {
|
|
fs.mkdirSync(gsdDir, { recursive: true });
|
|
}
|
|
fs.writeFileSync(defaultsFile, JSON.stringify({
|
|
model_profile: 'quality',
|
|
commit_docs: false,
|
|
}), 'utf-8');
|
|
|
|
const result = runGsdTools('config-ensure-section', 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');
|
|
} finally {
|
|
// Restore
|
|
if (existingDefaults !== null) {
|
|
fs.writeFileSync(defaultsFile, existingDefaults, 'utf-8');
|
|
} else {
|
|
try { fs.unlinkSync(defaultsFile); } catch { /* ignore */ }
|
|
}
|
|
if (!gsdDirExisted) {
|
|
try { fs.rmdirSync(gsdDir); } catch { /* ignore */ }
|
|
}
|
|
}
|
|
});
|
|
|
|
// NOTE: This test touches ~/.gsd/ on the real filesystem. It uses save/restore
|
|
// try/finally and skips if the file already exists to avoid corrupting user config.
|
|
test('merges nested workflow keys from defaults.json preserving unset keys', () => {
|
|
const homedir = os.homedir();
|
|
const gsdDir = path.join(homedir, '.gsd');
|
|
const defaultsFile = path.join(gsdDir, 'defaults.json');
|
|
|
|
let existingDefaults = null;
|
|
const gsdDirExisted = fs.existsSync(gsdDir);
|
|
if (fs.existsSync(defaultsFile)) {
|
|
existingDefaults = fs.readFileSync(defaultsFile, 'utf-8');
|
|
}
|
|
|
|
try {
|
|
if (!gsdDirExisted) {
|
|
fs.mkdirSync(gsdDir, { recursive: true });
|
|
}
|
|
fs.writeFileSync(defaultsFile, JSON.stringify({
|
|
workflow: { research: false },
|
|
}), 'utf-8');
|
|
|
|
const result = runGsdTools('config-ensure-section', 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');
|
|
} finally {
|
|
if (existingDefaults !== null) {
|
|
fs.writeFileSync(defaultsFile, existingDefaults, 'utf-8');
|
|
} else {
|
|
try { fs.unlinkSync(defaultsFile); } catch { /* ignore */ }
|
|
}
|
|
if (!gsdDirExisted) {
|
|
try { fs.rmdirSync(gsdDir); } catch { /* ignore */ }
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── 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('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
|
|
runGsdTools('config-ensure-section', tmpDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('gets a top-level value', () => {
|
|
const result = runGsdTools('config-get model_profile', 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}`
|
|
);
|
|
});
|
|
|
|
test('errors when config.json does not exist', () => {
|
|
const emptyTmpDir = createTempProject();
|
|
try {
|
|
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}`
|
|
);
|
|
} finally {
|
|
cleanup(emptyTmpDir);
|
|
}
|
|
});
|
|
|
|
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);
|
|
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);
|
|
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);
|
|
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 (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);
|
|
});
|
|
|
|
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);
|
|
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);
|
|
});
|
|
|
|
test('creates config if missing before setting profile', () => {
|
|
const emptyDir = createTempProject();
|
|
try {
|
|
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');
|
|
} finally {
|
|
cleanup(emptyDir);
|
|
}
|
|
});
|
|
});
|