mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
375 lines
14 KiB
JavaScript
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);
|
|
});
|
|
});
|