From abeacf02903ed4dfb85c019007322be225f2af5f Mon Sep 17 00:00:00 2001 From: Ethan Hurst Date: Wed, 25 Feb 2026 11:24:52 +1000 Subject: [PATCH] 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 --- tests/core.test.cjs | 249 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 tests/core.test.cjs diff --git a/tests/core.test.cjs b/tests/core.test.cjs new file mode 100644 index 000000000..368c1d801 --- /dev/null +++ b/tests/core.test.cjs @@ -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'); + }); + }); +});