From 1efc74af51df32cd3dcd1c15de6b9569562a1987 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:56:25 -0400 Subject: [PATCH] refactor(tests): consolidate runtime converters, remove duplicate tests, standardize helpers (#1169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test consolidation: - Merged gemini-config.test.cjs + opencode-agent-conversion.test.cjs into runtime-converters.test.cjs (single file for small runtime converters) - Removed 11 duplicate tests from core.test.cjs: - 5 comparePhaseNum tests (authoritative copies in phase.test.cjs) - 6 normalizePhaseName tests (authoritative copies in phase.test.cjs) - Added note in core.test.cjs pointing to phase.test.cjs as canonical location Standardization: - core.test.cjs now uses createTempProject()/cleanup() from helpers.cjs instead of inline fs.mkdtempSync/fs.rmSync patterns File count: 21 → 19 test files Test count: 755 → 744 (11 duplicates removed, 0 coverage lost) --- tests/core.test.cjs | 101 ++++-------------- tests/gemini-config.test.cjs | 47 -------- ...n.test.cjs => runtime-converters.test.cjs} | 54 ++++++++-- 3 files changed, 69 insertions(+), 133 deletions(-) delete mode 100644 tests/gemini-config.test.cjs rename tests/{opencode-agent-conversion.test.cjs => runtime-converters.test.cjs} (66%) diff --git a/tests/core.test.cjs b/tests/core.test.cjs index b20328f32..2f9d796f0 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -10,6 +10,7 @@ const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); const os = require('os'); +const { createTempProject, cleanup } = require('./helpers.cjs'); const { loadConfig, @@ -35,14 +36,13 @@ describe('loadConfig', () => { let originalCwd; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); originalCwd = process.cwd(); }); afterEach(() => { process.chdir(originalCwd); - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); function writeConfig(obj) { @@ -129,12 +129,11 @@ describe('resolveModelInternal', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); function writeConfig(obj) { @@ -276,62 +275,11 @@ describe('generateSlugInternal', () => { }); }); -// ─── normalizePhaseName ──────────────────────────────────────────────────────── - -describe('normalizePhaseName', () => { - test('pads single digit', () => { - assert.strictEqual(normalizePhaseName('1'), '01'); - }); - - test('preserves double digit', () => { - assert.strictEqual(normalizePhaseName('12'), '12'); - }); - - test('handles letter suffix', () => { - assert.strictEqual(normalizePhaseName('1A'), '01A'); - }); - - test('handles decimal phases', () => { - assert.strictEqual(normalizePhaseName('2.1'), '02.1'); - }); - - test('handles multi-level decimals', () => { - assert.strictEqual(normalizePhaseName('1.2.3'), '01.2.3'); - }); - - test('returns non-matching input unchanged', () => { - assert.strictEqual(normalizePhaseName('abc'), 'abc'); - }); -}); - -// ─── comparePhaseNum ─────────────────────────────────────────────────────────── - -describe('comparePhaseNum', () => { - test('sorts integer phases numerically', () => { - assert.ok(comparePhaseNum('1', '2') < 0); - assert.ok(comparePhaseNum('10', '2') > 0); - }); - - test('sorts letter suffixes', () => { - assert.ok(comparePhaseNum('12', '12A') < 0); - assert.ok(comparePhaseNum('12A', '12B') < 0); - }); - - test('sorts decimal phases', () => { - assert.ok(comparePhaseNum('2', '2.1') < 0); - assert.ok(comparePhaseNum('2.1', '2.2') < 0); - }); - - test('handles multi-level decimals', () => { - assert.ok(comparePhaseNum('1.1', '1.1.2') < 0); - assert.ok(comparePhaseNum('1.1.2', '1.2') < 0); - }); - - test('returns 0 for equal phases', () => { - assert.strictEqual(comparePhaseNum('1', '1'), 0); - assert.strictEqual(comparePhaseNum('2.1', '2.1'), 0); - }); -}); +// ─── normalizePhaseName / comparePhaseNum ────────────────────────────────────── +// NOTE: Comprehensive tests for normalizePhaseName and comparePhaseNum are in +// phase.test.cjs (which covers all edge cases: hybrid, letter-suffix, +// multi-level decimal, case-insensitive, directory-slug, and full sort order). +// Removed duplicates here to keep a single authoritative test location. // ─── safeReadFile ────────────────────────────────────────────────────────────── @@ -343,7 +291,7 @@ describe('safeReadFile', () => { }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('reads existing file', () => { @@ -363,12 +311,11 @@ describe('pathExistsInternal', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('returns true for existing path', () => { @@ -390,12 +337,11 @@ describe('getMilestoneInfo', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('extracts version and name from roadmap', () => { @@ -502,7 +448,7 @@ describe('searchPhaseInDir', () => { }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('finds phase directory by normalized prefix', () => { @@ -564,12 +510,11 @@ describe('findPhaseInternal', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('finds phase in current phases directory', () => { @@ -605,12 +550,11 @@ describe('getRoadmapPhaseInternal', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); // Bug: getRoadmapPhaseInternal was missing from module.exports @@ -687,12 +631,11 @@ describe('getMilestonePhaseFilter', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('filters directories to only current milestone phases', () => { diff --git a/tests/gemini-config.test.cjs b/tests/gemini-config.test.cjs deleted file mode 100644 index 794208427..000000000 --- a/tests/gemini-config.test.cjs +++ /dev/null @@ -1,47 +0,0 @@ -/** - * GSD Tools Tests - Gemini agent conversion - * - * Verifies Gemini-specific agent frontmatter conversion removes - * unsupported fields while preserving converted tools and body text. - */ - -process.env.GSD_TEST_MODE = '1'; - -const { test, describe } = require('node:test'); -const assert = require('node:assert'); - -const { convertClaudeToGeminiAgent } = require('../bin/install.js'); - -describe('convertClaudeToGeminiAgent', () => { - test('drops unsupported skills frontmatter while keeping converted tools', () => { - const input = `--- -name: gsd-codebase-mapper -description: Explores codebase and writes structured analysis documents. -tools: Read, Bash, Grep, Glob, Write -color: cyan -skills: - - gsd-mapper-workflow ---- - - -Use \${PHASE} in shell examples. -`; - - const result = convertClaudeToGeminiAgent(input); - const frontmatter = result.split('---')[1] || ''; - - assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name'); - assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description'); - assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array'); - assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file'); - assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command'); - assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content'); - assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob'); - assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file'); - assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field'); - assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field'); - assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items'); - assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini'); - assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern'); - }); -}); diff --git a/tests/opencode-agent-conversion.test.cjs b/tests/runtime-converters.test.cjs similarity index 66% rename from tests/opencode-agent-conversion.test.cjs rename to tests/runtime-converters.test.cjs index 6ba62b54b..6491d9376 100644 --- a/tests/opencode-agent-conversion.test.cjs +++ b/tests/runtime-converters.test.cjs @@ -1,19 +1,21 @@ /** - * OpenCode Agent Frontmatter Conversion Tests + * Runtime Converter Tests — OpenCode + Gemini * - * Validates that convertClaudeToOpencodeFrontmatter correctly converts - * agent frontmatter for OpenCode compatibility when isAgent: true. + * Tests for small runtime-specific conversion functions from install.js. + * Larger runtime test suites (Copilot, Codex, Antigravity) have their own files. * - * Bug: Without isAgent flag, the function strips name: (agents need it), - * keeps color:/skills:/tools: record (should strip), and doesn't add - * model: inherit / mode: subagent (required by OpenCode agents). + * OpenCode: convertClaudeToOpencodeFrontmatter (agent + command modes) + * Gemini: convertClaudeToGeminiAgent (frontmatter + tool mapping + body escaping) */ const { test, describe } = require('node:test'); const assert = require('node:assert'); process.env.GSD_TEST_MODE = '1'; -const { convertClaudeToOpencodeFrontmatter } = require('../bin/install.js'); +const { + convertClaudeToOpencodeFrontmatter, + convertClaudeToGeminiAgent, +} = require('../bin/install.js'); // Sample Claude agent frontmatter (matches actual GSD agent format) const SAMPLE_AGENT = `--- @@ -141,3 +143,41 @@ describe('OpenCode command conversion (isAgent: false, default)', () => { assert.ok(frontmatter.includes('description:'), 'description should be kept'); }); }); + +// ───────────────────────────────────────────────────────────────────────────── +// Gemini CLI agent conversion (merged from gemini-config.test.cjs) +// ───────────────────────────────────────────────────────────────────────────── + +describe('convertClaudeToGeminiAgent', () => { + test('drops unsupported skills frontmatter while keeping converted tools', () => { + const input = `--- +name: gsd-codebase-mapper +description: Explores codebase and writes structured analysis documents. +tools: Read, Bash, Grep, Glob, Write +color: cyan +skills: + - gsd-mapper-workflow +--- + + +Use \${PHASE} in shell examples. +`; + + const result = convertClaudeToGeminiAgent(input); + const frontmatter = result.split('---')[1] || ''; + + assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name'); + assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description'); + assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array'); + assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file'); + assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command'); + assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content'); + assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob'); + assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file'); + assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field'); + assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field'); + assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items'); + assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini'); + assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern'); + }); +});