test(01-01): add loadConfig and resolveModelInternal tests

- 10 loadConfig tests: defaults, model_profile, nested keys, branching,
  model_overrides presence/absence (REG-01), invalid JSON, parallelization
- 17 resolveModelInternal tests: all 3 profiles (quality/balanced/budget)
  with 4 agent types each, override precedence, opus->inherit mapping,
  unknown agent fallback
This commit is contained in:
Ethan Hurst
2026-02-25 11:24:52 +10:00
parent 752e038914
commit abeacf0290

249
tests/core.test.cjs Normal file
View File

@@ -0,0 +1,249 @@
/**
* GSD Tools Tests - core.cjs
*
* Tests for the foundational module's exports including regressions
* for known bugs (REG-01: loadConfig model_overrides, REG-02: getRoadmapPhaseInternal export).
*/
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 {
loadConfig,
resolveModelInternal,
MODEL_PROFILES,
escapeRegex,
generateSlugInternal,
normalizePhaseName,
comparePhaseNum,
safeReadFile,
pathExistsInternal,
getMilestoneInfo,
getRoadmapPhaseInternal,
searchPhaseInDir,
findPhaseInternal,
} = require('../get-shit-done/bin/lib/core.cjs');
// ─── loadConfig ────────────────────────────────────────────────────────────────
describe('loadConfig', () => {
let tmpDir;
let originalCwd;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
originalCwd = process.cwd();
});
afterEach(() => {
process.chdir(originalCwd);
fs.rmSync(tmpDir, { recursive: true, force: true });
});
function writeConfig(obj) {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify(obj, null, 2)
);
}
test('returns defaults when config.json is missing', () => {
const config = loadConfig(tmpDir);
assert.strictEqual(config.model_profile, 'balanced');
assert.strictEqual(config.commit_docs, true);
assert.strictEqual(config.research, true);
assert.strictEqual(config.plan_checker, true);
assert.strictEqual(config.brave_search, false);
assert.strictEqual(config.parallelization, true);
});
test('reads model_profile from config.json', () => {
writeConfig({ model_profile: 'quality' });
const config = loadConfig(tmpDir);
assert.strictEqual(config.model_profile, 'quality');
});
test('reads nested config keys', () => {
writeConfig({ planning: { commit_docs: false } });
const config = loadConfig(tmpDir);
assert.strictEqual(config.commit_docs, false);
});
test('reads branching_strategy from git section', () => {
writeConfig({ git: { branching_strategy: 'per-phase' } });
const config = loadConfig(tmpDir);
assert.strictEqual(config.branching_strategy, 'per-phase');
});
// Bug: loadConfig previously omitted model_overrides from return value
test('returns model_overrides when present (REG-01)', () => {
writeConfig({ model_overrides: { 'gsd-executor': 'opus' } });
const config = loadConfig(tmpDir);
assert.deepStrictEqual(config.model_overrides, { 'gsd-executor': 'opus' });
});
test('returns model_overrides as null when not in config', () => {
writeConfig({ model_profile: 'balanced' });
const config = loadConfig(tmpDir);
assert.strictEqual(config.model_overrides, null);
});
test('returns defaults when config.json contains invalid JSON', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
'not valid json {{{{'
);
const config = loadConfig(tmpDir);
assert.strictEqual(config.model_profile, 'balanced');
assert.strictEqual(config.commit_docs, true);
});
test('handles parallelization as boolean', () => {
writeConfig({ parallelization: false });
const config = loadConfig(tmpDir);
assert.strictEqual(config.parallelization, false);
});
test('handles parallelization as object with enabled field', () => {
writeConfig({ parallelization: { enabled: false } });
const config = loadConfig(tmpDir);
assert.strictEqual(config.parallelization, false);
});
test('prefers top-level keys over nested keys', () => {
writeConfig({ commit_docs: false, planning: { commit_docs: true } });
const config = loadConfig(tmpDir);
assert.strictEqual(config.commit_docs, false);
});
});
// ─── resolveModelInternal ──────────────────────────────────────────────────────
describe('resolveModelInternal', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
function writeConfig(obj) {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify(obj, null, 2)
);
}
describe('quality profile', () => {
beforeEach(() => {
writeConfig({ model_profile: 'quality' });
});
test('resolves gsd-planner correctly', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'inherit');
});
test('resolves gsd-executor correctly', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'inherit');
});
test('resolves gsd-phase-researcher correctly', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-phase-researcher'), 'inherit');
});
test('resolves gsd-codebase-mapper correctly', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'sonnet');
});
});
describe('balanced profile', () => {
beforeEach(() => {
writeConfig({ model_profile: 'balanced' });
});
test('resolves gsd-planner correctly', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'inherit');
});
test('resolves gsd-executor correctly', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'sonnet');
});
test('resolves gsd-phase-researcher correctly', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-phase-researcher'), 'sonnet');
});
test('resolves gsd-codebase-mapper correctly', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'haiku');
});
});
describe('budget profile', () => {
beforeEach(() => {
writeConfig({ model_profile: 'budget' });
});
test('resolves gsd-planner correctly', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'sonnet');
});
test('resolves gsd-executor correctly', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'sonnet');
});
test('resolves gsd-phase-researcher correctly', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-phase-researcher'), 'haiku');
});
test('resolves gsd-codebase-mapper correctly', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'haiku');
});
});
describe('override precedence', () => {
test('per-agent override takes precedence over profile', () => {
writeConfig({
model_profile: 'balanced',
model_overrides: { 'gsd-executor': 'haiku' },
});
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'haiku');
});
test('opus override resolves to inherit', () => {
writeConfig({
model_overrides: { 'gsd-executor': 'opus' },
});
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'inherit');
});
test('agents not in override fall back to profile', () => {
writeConfig({
model_profile: 'quality',
model_overrides: { 'gsd-executor': 'haiku' },
});
// gsd-planner not overridden, should use quality profile -> opus -> inherit
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'inherit');
});
});
describe('edge cases', () => {
test('returns sonnet for unknown agent type', () => {
writeConfig({ model_profile: 'balanced' });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-nonexistent'), 'sonnet');
});
test('defaults to balanced profile when model_profile missing', () => {
writeConfig({});
// balanced profile, gsd-planner -> opus -> inherit
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'inherit');
});
});
});