Files
get-shit-done/tests/config.test.cjs

375 lines
14 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.strictEqual(typeof config.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.strictEqual(typeof config.branching_strategy, 'string', '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);
});
});