diff --git a/tests/kilo-install.test.cjs b/tests/kilo-install.test.cjs index b0188d1bd..77553c93d 100644 --- a/tests/kilo-install.test.cjs +++ b/tests/kilo-install.test.cjs @@ -225,12 +225,29 @@ describe('Source code integration (Kilo)', () => { }); test('promptRuntime runtimeMap has Kilo as option 11', () => { - assert.ok(src.includes("'11': 'kilo'"), 'runtimeMap has 11 -> kilo'); + // Structural assertion against exported runtimeMap rather than source-grep. + process.env.GSD_TEST_MODE = '1'; + delete require.cache[require.resolve(path.join(__dirname, '..', 'bin', 'install.js'))]; + const { runtimeMap } = require(path.join(__dirname, '..', 'bin', 'install.js')); + assert.strictEqual(runtimeMap['11'], 'kilo', 'runtimeMap has 11 -> kilo'); }); test('prompt text shows Kilo above OpenCode without marketing copy', () => { - assert.ok(src.includes('11${reset}) Kilo'), 'prompt lists Kilo as option 11'); - assert.ok(!src.includes('the #1 AI coding platform on OpenRouter'), 'prompt does not include marketing tagline'); + // Call the exported prompt builder; assert against rendered text, not raw source. + process.env.GSD_TEST_MODE = '1'; + delete require.cache[require.resolve(path.join(__dirname, '..', 'bin', 'install.js'))]; + const { buildRuntimePromptText } = require(path.join(__dirname, '..', 'bin', 'install.js')); + const promptText = buildRuntimePromptText(); + // Strip ANSI color codes so assertions don't depend on terminal escapes. + // eslint-disable-next-line no-control-regex + const plain = promptText.replace(/\x1b\[[0-9;]*m/g, ''); + assert.ok(/\b11\)\s*Kilo\b/.test(plain), 'prompt lists Kilo as option 11'); + const kiloIdx = plain.indexOf('11) Kilo'); + const opencodeIdx = plain.indexOf('OpenCode'); + assert.ok(kiloIdx > -1 && opencodeIdx > -1 && kiloIdx < opencodeIdx, + 'Kilo appears above OpenCode in prompt'); + assert.ok(!plain.includes('the #1 AI coding platform on OpenRouter'), + 'prompt does not include marketing tagline'); }); test('hooks are skipped for Kilo', () => { diff --git a/tests/multi-runtime-select.test.cjs b/tests/multi-runtime-select.test.cjs index 25536110a..e7f760dc1 100644 --- a/tests/multi-runtime-select.test.cjs +++ b/tests/multi-runtime-select.test.cjs @@ -3,60 +3,29 @@ * Verifies that promptRuntime accepts comma-separated, space-separated, * and single-choice inputs, deduplicates, and falls back to claude. * See issue #1281. + * + * Per CONTRIBUTING.md "no-source-grep" testing standard, prompt + parser + * behavior is asserted via the install module's exported pure functions + * (`runtimeMap`, `allRuntimes`, `parseRuntimeInput`, `buildRuntimePromptText`) + * instead of regexing bin/install.js source text. */ +process.env.GSD_TEST_MODE = '1'; + const { test, describe } = require('node:test'); const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const path = require('node:path'); -// Read install.js source to extract the runtimeMap and parsing logic -const installSrc = fs.readFileSync( - path.join(__dirname, '..', 'bin', 'install.js'), - 'utf8' -); +const { + runtimeMap, + allRuntimes, + parseRuntimeInput, + buildRuntimePromptText, +} = require('../bin/install.js'); -// Extract runtimeMap from source for validation -const runtimeMap = { - '1': 'claude', - '2': 'antigravity', - '3': 'augment', - '4': 'cline', - '5': 'codebuddy', - '6': 'codex', - '7': 'copilot', - '8': 'cursor', - '9': 'gemini', - '10': 'hermes', - '11': 'kilo', - '12': 'opencode', - '13': 'qwen', - '14': 'trae', - '15': 'windsurf' -}; -const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'gemini', 'hermes', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf']; - -/** - * Simulate the parsing logic from promptRuntime without requiring readline. - * This mirrors the exact logic in the rl.question callback. - */ -function parseRuntimeInput(input) { - input = input.trim() || '1'; - - if (input === '16') { - return allRuntimes; - } - - const choices = input.split(/[\s,]+/).filter(Boolean); - const selected = []; - for (const c of choices) { - const runtime = runtimeMap[c]; - if (runtime && !selected.includes(runtime)) { - selected.push(runtime); - } - } - - return selected.length > 0 ? selected : ['claude']; +// Strip ANSI color codes for human-readable assertions on prompt text. +function stripAnsi(s) { + // eslint-disable-next-line no-control-regex + return s.replace(/\x1b\[[0-9;]*m/g, ''); } describe('multi-runtime selection parsing', () => { @@ -142,68 +111,75 @@ describe('multi-runtime selection parsing', () => { }); }); -describe('install.js source contains multi-select support', () => { - test('runtimeMap is defined with all 15 runtimes', () => { - for (const [key, name] of Object.entries(runtimeMap)) { - assert.ok( - installSrc.includes(`'${key}': '${name}'`), - `runtimeMap has ${key} -> ${name}` - ); +describe('install.js exports multi-select runtime metadata', () => { + const expectedRuntimeMap = { + '1': 'claude', + '2': 'antigravity', + '3': 'augment', + '4': 'cline', + '5': 'codebuddy', + '6': 'codex', + '7': 'copilot', + '8': 'cursor', + '9': 'gemini', + '10': 'hermes', + '11': 'kilo', + '12': 'opencode', + '13': 'qwen', + '14': 'trae', + '15': 'windsurf', + }; + const expectedRuntimes = [ + 'claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', + 'copilot', 'cursor', 'gemini', 'hermes', 'kilo', 'opencode', 'qwen', + 'trae', 'windsurf', + ]; + + test('runtimeMap exports every option key bound to the right runtime', () => { + assert.deepStrictEqual(runtimeMap, expectedRuntimeMap, + 'exported runtimeMap matches the canonical option list'); + }); + + test('allRuntimes contains every runtime exactly once', () => { + assert.strictEqual(allRuntimes.length, expectedRuntimes.length); + for (const rt of expectedRuntimes) { + assert.ok(allRuntimes.includes(rt), `allRuntimes contains ${rt}`); } + assert.strictEqual(new Set(allRuntimes).size, allRuntimes.length, + 'allRuntimes has no duplicates'); }); - test('allRuntimes array contains all runtimes', () => { - const match = installSrc.match(/const allRuntimes = \[([^\]]+)\]/); - assert.ok(match, 'allRuntimes array found'); - for (const rt of allRuntimes) { - assert.ok(match[1].includes(`'${rt}'`), `allRuntimes includes ${rt}`); - } + test('"All" shortcut (option 16) selects every runtime', () => { + assert.deepStrictEqual(parseRuntimeInput('16'), allRuntimes); }); - test('all shortcut uses option 16', () => { - assert.ok( - installSrc.includes("if (input === '16')"), - 'all shortcut uses option 16' - ); - }); - - test('prompt lists Hermes Agent as option 10, Qwen Code as option 13, and All as option 16', () => { - assert.ok( - installSrc.includes('10${reset}) Hermes Agent'), - 'prompt lists Hermes Agent as option 10' - ); - assert.ok( - installSrc.includes('13${reset}) Qwen Code'), - 'prompt lists Qwen Code as option 13' - ); - assert.ok( - installSrc.includes('14${reset}) Trae'), - 'prompt lists Trae as option 14' - ); - assert.ok( - installSrc.includes('16${reset}) All'), - 'prompt lists All as option 16' - ); + test('prompt lists Hermes Agent (10), Qwen Code (13), Trae (14), and All (16)', () => { + const prompt = stripAnsi(buildRuntimePromptText()); + assert.ok(/\b10\)\s*Hermes Agent\b/.test(prompt), + 'prompt lists Hermes Agent as option 10'); + assert.ok(/\b13\)\s*Qwen Code\b/.test(prompt), + 'prompt lists Qwen Code as option 13'); + assert.ok(/\b14\)\s*Trae\b/.test(prompt), + 'prompt lists Trae as option 14'); + assert.ok(/\b16\)\s*All\b/.test(prompt), + 'prompt lists All as option 16'); }); test('prompt text shows multi-select hint', () => { - assert.ok( - installSrc.includes('Select multiple'), - 'prompt includes multi-select instructions' - ); + const prompt = stripAnsi(buildRuntimePromptText()); + assert.ok(/Select multiple/i.test(prompt), + 'prompt includes multi-select instructions'); }); - test('parsing uses split with comma and space regex', () => { - assert.ok( - installSrc.includes("split(/[\\s,]+/)"), - 'input is split on commas and whitespace' - ); - }); - - test('deduplication check exists', () => { - assert.ok( - installSrc.includes('!selected.includes(runtime)'), - 'deduplication guard exists' + test('parser splits on commas and whitespace and deduplicates', () => { + // Behavioral assertion: same set of choices in different separators + // produces the same selection, and duplicates collapse. + assert.deepStrictEqual( + parseRuntimeInput('1,7,9'), + parseRuntimeInput('1 7 9'), + 'comma- and space-separated input yield identical selections' ); + assert.deepStrictEqual(parseRuntimeInput('1,1,7,7'), ['claude', 'copilot'], + 'duplicates collapsed in order'); }); });