From 04f26b3e4d919c9ec8d6829b394ec47a9bb4b27d Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 10 Mar 2026 13:31:42 -0700 Subject: [PATCH] Fix Kiro ref file bug, add missing test coverage for Agents/Kiro/utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix bug in Kiro transformer where commandNames was incorrectly passed to replacePlaceholders for reference files. Add dedicated test suites for Agents and Kiro transformers, and add unit tests for replacePlaceholders and prefixSkillReferences utilities. (107 → 164 tests) Co-Authored-By: Claude Opus 4.6 --- scripts/lib/transformers/kiro.js | 2 +- tests/lib/transformers/agents.test.js | 330 ++++++++++++++++++++++++++ tests/lib/transformers/kiro.test.js | 302 +++++++++++++++++++++++ tests/lib/utils.test.js | 124 +++++++++- 4 files changed, 756 insertions(+), 2 deletions(-) create mode 100644 tests/lib/transformers/agents.test.js create mode 100644 tests/lib/transformers/kiro.test.js diff --git a/scripts/lib/transformers/kiro.js b/scripts/lib/transformers/kiro.js index 620db85b5..4d20ca728 100644 --- a/scripts/lib/transformers/kiro.js +++ b/scripts/lib/transformers/kiro.js @@ -51,7 +51,7 @@ export function transformKiro(skills, distDir, patterns = null, options = {}) { ensureDir(refDir); for (const ref of skill.references) { const refOutputPath = path.join(refDir, `${ref.name}.md`); - const refContent = replacePlaceholders(ref.content, 'kiro', commandNames); + const refContent = replacePlaceholders(ref.content, 'kiro'); writeFile(refOutputPath, refContent); refCount++; } diff --git a/tests/lib/transformers/agents.test.js b/tests/lib/transformers/agents.test.js new file mode 100644 index 000000000..d43273eed --- /dev/null +++ b/tests/lib/transformers/agents.test.js @@ -0,0 +1,330 @@ +import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; +import fs from 'fs'; +import path from 'path'; +import { transformAgents } from '../../../scripts/lib/transformers/agents.js'; +import { parseFrontmatter } from '../../../scripts/lib/utils.js'; + +const TEST_DIR = path.join(process.cwd(), 'test-tmp-agents'); + +describe('transformAgents', () => { + beforeEach(() => { + if (fs.existsSync(TEST_DIR)) { + fs.rmSync(TEST_DIR, { recursive: true, force: true }); + } + }); + + afterEach(() => { + if (fs.existsSync(TEST_DIR)) { + fs.rmSync(TEST_DIR, { recursive: true, force: true }); + } + }); + + test('should create correct directory structure', () => { + transformAgents([], TEST_DIR); + expect(fs.existsSync(path.join(TEST_DIR, 'agents/.agents/skills'))).toBe(true); + }); + + test('should create skill with full frontmatter', () => { + const skills = [ + { + name: 'test-skill', + description: 'A test skill', + license: 'MIT', + body: 'Skill instructions.' + } + ]; + + transformAgents(skills, TEST_DIR); + + const outputPath = path.join(TEST_DIR, 'agents/.agents/skills/test-skill/SKILL.md'); + expect(fs.existsSync(outputPath)).toBe(true); + + const content = fs.readFileSync(outputPath, 'utf-8'); + const parsed = parseFrontmatter(content); + + expect(parsed.frontmatter.name).toBe('test-skill'); + expect(parsed.frontmatter.description).toBe('A test skill'); + expect(parsed.body).toBe('Skill instructions.'); + }); + + test('should add user-invokable flag for user-invokable skills', () => { + const skills = [ + { + name: 'audit', + description: 'Audit command', + userInvokable: true, + body: 'Audit the code.' + } + ]; + + transformAgents(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/audit/SKILL.md'), 'utf-8'); + const parsed = parseFrontmatter(content); + + expect(parsed.frontmatter['user-invokable']).toBe(true); + }); + + test('should not add user-invokable flag for non-user-invokable skills', () => { + const skills = [ + { + name: 'helper', + description: 'Helper skill', + body: 'Helper body.' + } + ]; + + transformAgents(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/helper/SKILL.md'), 'utf-8'); + const parsed = parseFrontmatter(content); + + expect(parsed.frontmatter['user-invokable']).toBeUndefined(); + }); + + test('should create argument-hint for required args', () => { + const skills = [ + { + name: 'with-args', + description: 'Command with args', + userInvokable: true, + args: [ + { name: 'target', description: 'Target', required: true }, + { name: 'format', description: 'Format', required: false } + ], + body: 'Process {{target}} in {{format}}.' + } + ]; + + transformAgents(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/with-args/SKILL.md'), 'utf-8'); + const parsed = parseFrontmatter(content); + + expect(parsed.frontmatter['argument-hint']).toBe(' [FORMAT=]'); + }); + + test('should not add argument-hint for skills without args', () => { + const skills = [ + { + name: 'no-args', + description: 'No args', + userInvokable: true, + args: [], + body: 'Simple body.' + } + ]; + + transformAgents(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/no-args/SKILL.md'), 'utf-8'); + const parsed = parseFrontmatter(content); + + expect(parsed.frontmatter['argument-hint']).toBeUndefined(); + }); + + test('should not add argument-hint for non-user-invokable skills with args', () => { + const skills = [ + { + name: 'internal', + description: 'Internal skill', + userInvokable: false, + args: [{ name: 'target', description: 'Target', required: true }], + body: 'Body.' + } + ]; + + transformAgents(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/internal/SKILL.md'), 'utf-8'); + const parsed = parseFrontmatter(content); + + expect(parsed.frontmatter['argument-hint']).toBeUndefined(); + }); + + test('should handle multiple skills', () => { + const skills = [ + { name: 'skill1', description: 'Skill 1', body: 'Body 1' }, + { name: 'skill2', description: 'Skill 2', body: 'Body 2' }, + { name: 'skill3', description: 'Skill 3', body: 'Body 3' } + ]; + + transformAgents(skills, TEST_DIR); + + expect(fs.existsSync(path.join(TEST_DIR, 'agents/.agents/skills/skill1/SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(TEST_DIR, 'agents/.agents/skills/skill2/SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(TEST_DIR, 'agents/.agents/skills/skill3/SKILL.md'))).toBe(true); + }); + + test('should replace {{model}} placeholder', () => { + const skills = [ + { + name: 'test', + description: 'Test', + body: 'Ask {{model}} for help.' + } + ]; + + transformAgents(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/test/SKILL.md'), 'utf-8'); + expect(content).toContain('Ask the model for help.'); + }); + + test('should replace {{config_file}} placeholder', () => { + const skills = [ + { + name: 'test', + description: 'Test', + body: 'See {{config_file}} for details.' + } + ]; + + transformAgents(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/test/SKILL.md'), 'utf-8'); + expect(content).toContain('See .github/copilot-instructions.md for details.'); + }); + + test('should replace {{available_commands}} placeholder', () => { + const skills = [ + { name: 'audit', description: 'Audit', userInvokable: true, body: 'Available: {{available_commands}}' }, + { name: 'polish', description: 'Polish', userInvokable: true, body: 'Polish body.' } + ]; + + transformAgents(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/audit/SKILL.md'), 'utf-8'); + expect(content).toContain('Available: /audit, /polish'); + }); + + test('should copy reference files', () => { + const skills = [ + { + name: 'frontend-design', + description: 'Design skill', + body: 'Design instructions.', + references: [ + { name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' }, + { name: 'color', content: 'Color reference', filePath: '/fake/path/color.md' } + ] + } + ]; + + transformAgents(skills, TEST_DIR); + + const typoPath = path.join(TEST_DIR, 'agents/.agents/skills/frontend-design/reference/typography.md'); + const colorPath = path.join(TEST_DIR, 'agents/.agents/skills/frontend-design/reference/color.md'); + + expect(fs.existsSync(typoPath)).toBe(true); + expect(fs.existsSync(colorPath)).toBe(true); + expect(fs.readFileSync(typoPath, 'utf-8')).toBe('Typography reference'); + }); + + test('should replace placeholders in reference files', () => { + const skills = [ + { + name: 'test', + description: 'Test', + body: 'Body.', + references: [ + { name: 'ref', content: 'Use {{model}} with {{config_file}}.', filePath: '/fake/ref.md' } + ] + } + ]; + + transformAgents(skills, TEST_DIR); + + const refContent = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/test/reference/ref.md'), 'utf-8'); + expect(refContent).toContain('Use the model with .github/copilot-instructions.md.'); + }); + + test('should support prefix option', () => { + const skills = [ + { name: 'audit', description: 'Audit', userInvokable: true, body: 'Audit body' } + ]; + + transformAgents(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); + + const outputPath = path.join(TEST_DIR, 'agents-prefixed/.agents/skills/i-audit/SKILL.md'); + expect(fs.existsSync(outputPath)).toBe(true); + + const content = fs.readFileSync(outputPath, 'utf-8'); + expect(content).toContain('name: i-audit'); + }); + + test('should prefix skill references in body when prefix is set', () => { + const skills = [ + { name: 'audit', description: 'Audit', userInvokable: true, body: 'Run /polish after the audit skill.' }, + { name: 'polish', description: 'Polish', userInvokable: true, body: 'Polish body.' } + ]; + + transformAgents(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); + + const content = fs.readFileSync(path.join(TEST_DIR, 'agents-prefixed/.agents/skills/i-audit/SKILL.md'), 'utf-8'); + expect(content).toContain('/i-polish'); + expect(content).toContain('the i-audit skill'); + }); + + test('should clean existing directory before writing', () => { + const existingDir = path.join(TEST_DIR, 'agents/.agents/skills/old'); + fs.mkdirSync(existingDir, { recursive: true }); + fs.writeFileSync(path.join(existingDir, 'SKILL.md'), 'old'); + + const skills = [{ name: 'new', description: 'New', body: 'New' }]; + transformAgents(skills, TEST_DIR); + + expect(fs.existsSync(path.join(TEST_DIR, 'agents/.agents/skills/old/SKILL.md'))).toBe(false); + expect(fs.existsSync(path.join(TEST_DIR, 'agents/.agents/skills/new/SKILL.md'))).toBe(true); + }); + + test('should log correct summary', () => { + const consoleMock = mock(() => {}); + const originalLog = console.log; + console.log = consoleMock; + + const skills = [ + { name: 'skill1', description: 'Test', userInvokable: true, body: 'body' }, + { name: 'skill2', description: 'Test', userInvokable: false, body: 'body' } + ]; + + transformAgents(skills, TEST_DIR); + + console.log = originalLog; + + expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Agents:')); + expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills')); + expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable')); + }); + + test('should log reference file count', () => { + const consoleMock = mock(() => {}); + const originalLog = console.log; + console.log = consoleMock; + + const skills = [ + { + name: 'test', + description: 'Test', + body: 'Body.', + references: [ + { name: 'ref1', content: 'Ref 1', filePath: '/fake/ref1.md' }, + { name: 'ref2', content: 'Ref 2', filePath: '/fake/ref2.md' } + ] + } + ]; + + transformAgents(skills, TEST_DIR); + + console.log = originalLog; + + expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 reference files')); + }); + + test('should handle empty skills array', () => { + transformAgents([], TEST_DIR); + + const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'agents/.agents/skills')); + expect(skillDirs).toHaveLength(0); + }); +}); diff --git a/tests/lib/transformers/kiro.test.js b/tests/lib/transformers/kiro.test.js new file mode 100644 index 000000000..060089aef --- /dev/null +++ b/tests/lib/transformers/kiro.test.js @@ -0,0 +1,302 @@ +import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; +import fs from 'fs'; +import path from 'path'; +import { transformKiro } from '../../../scripts/lib/transformers/kiro.js'; +import { parseFrontmatter } from '../../../scripts/lib/utils.js'; + +const TEST_DIR = path.join(process.cwd(), 'test-tmp-kiro'); + +describe('transformKiro', () => { + beforeEach(() => { + if (fs.existsSync(TEST_DIR)) { + fs.rmSync(TEST_DIR, { recursive: true, force: true }); + } + }); + + afterEach(() => { + if (fs.existsSync(TEST_DIR)) { + fs.rmSync(TEST_DIR, { recursive: true, force: true }); + } + }); + + test('should create correct directory structure', () => { + transformKiro([], TEST_DIR); + expect(fs.existsSync(path.join(TEST_DIR, 'kiro/.kiro/skills'))).toBe(true); + }); + + test('should create skill with full frontmatter', () => { + const skills = [ + { + name: 'test-skill', + description: 'A test skill', + license: 'MIT', + body: 'Skill instructions.' + } + ]; + + transformKiro(skills, TEST_DIR); + + const outputPath = path.join(TEST_DIR, 'kiro/.kiro/skills/test-skill/SKILL.md'); + expect(fs.existsSync(outputPath)).toBe(true); + + const content = fs.readFileSync(outputPath, 'utf-8'); + const parsed = parseFrontmatter(content); + + expect(parsed.frontmatter.name).toBe('test-skill'); + expect(parsed.frontmatter.description).toBe('A test skill'); + expect(parsed.frontmatter.license).toBe('MIT'); + expect(parsed.body).toBe('Skill instructions.'); + }); + + test('should include compatibility in frontmatter', () => { + const skills = [ + { + name: 'test', + description: 'Test', + compatibility: 'kiro', + body: 'Body' + } + ]; + + transformKiro(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8'); + expect(content).toContain('compatibility: kiro'); + }); + + test('should include metadata in frontmatter', () => { + const skills = [ + { + name: 'test', + description: 'Test', + metadata: 'some-metadata', + body: 'Body' + } + ]; + + transformKiro(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8'); + expect(content).toContain('metadata: some-metadata'); + }); + + test('should not include user-invokable in frontmatter (Kiro does not use it)', () => { + const skills = [ + { + name: 'test', + description: 'Test', + userInvokable: true, + body: 'Body' + } + ]; + + transformKiro(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8'); + expect(content).not.toContain('user-invokable'); + }); + + test('should handle multiple skills', () => { + const skills = [ + { name: 'skill1', description: 'Skill 1', body: 'Body 1' }, + { name: 'skill2', description: 'Skill 2', body: 'Body 2' }, + { name: 'skill3', description: 'Skill 3', body: 'Body 3' } + ]; + + transformKiro(skills, TEST_DIR); + + expect(fs.existsSync(path.join(TEST_DIR, 'kiro/.kiro/skills/skill1/SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(TEST_DIR, 'kiro/.kiro/skills/skill2/SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(TEST_DIR, 'kiro/.kiro/skills/skill3/SKILL.md'))).toBe(true); + }); + + test('should replace {{model}} placeholder', () => { + const skills = [ + { + name: 'test', + description: 'Test', + body: 'Ask {{model}} for help.' + } + ]; + + transformKiro(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8'); + expect(content).toContain('Ask Claude for help.'); + }); + + test('should replace {{config_file}} placeholder', () => { + const skills = [ + { + name: 'test', + description: 'Test', + body: 'See {{config_file}} for details.' + } + ]; + + transformKiro(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8'); + expect(content).toContain('See .kiro/settings.json for details.'); + }); + + test('should replace {{available_commands}} placeholder', () => { + const skills = [ + { name: 'audit', description: 'Audit', userInvokable: true, body: 'Available: {{available_commands}}' }, + { name: 'polish', description: 'Polish', userInvokable: true, body: 'Polish body.' } + ]; + + transformKiro(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/audit/SKILL.md'), 'utf-8'); + expect(content).toContain('Available: /audit, /polish'); + }); + + test('should copy reference files', () => { + const skills = [ + { + name: 'frontend-design', + description: 'Design skill', + body: 'Design instructions.', + references: [ + { name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' }, + { name: 'color', content: 'Color reference', filePath: '/fake/path/color.md' } + ] + } + ]; + + transformKiro(skills, TEST_DIR); + + const typoPath = path.join(TEST_DIR, 'kiro/.kiro/skills/frontend-design/reference/typography.md'); + const colorPath = path.join(TEST_DIR, 'kiro/.kiro/skills/frontend-design/reference/color.md'); + + expect(fs.existsSync(typoPath)).toBe(true); + expect(fs.existsSync(colorPath)).toBe(true); + expect(fs.readFileSync(typoPath, 'utf-8')).toBe('Typography reference'); + }); + + test('should replace placeholders in reference files without commandNames', () => { + const skills = [ + { + name: 'test', + description: 'Test', + userInvokable: true, + body: 'Body with {{available_commands}}.', + references: [ + { name: 'ref', content: 'Use {{model}} with {{config_file}}. Commands: {{available_commands}}.', filePath: '/fake/ref.md' } + ] + } + ]; + + transformKiro(skills, TEST_DIR); + + const refContent = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/reference/ref.md'), 'utf-8'); + expect(refContent).toContain('Use Claude with .kiro/settings.json.'); + // Reference files should NOT get commandNames, so {{available_commands}} becomes empty string + expect(refContent).toContain('Commands: .'); + }); + + test('should support prefix option', () => { + const skills = [ + { name: 'audit', description: 'Audit', userInvokable: true, body: 'Audit body' } + ]; + + transformKiro(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); + + const outputPath = path.join(TEST_DIR, 'kiro-prefixed/.kiro/skills/i-audit/SKILL.md'); + expect(fs.existsSync(outputPath)).toBe(true); + + const content = fs.readFileSync(outputPath, 'utf-8'); + expect(content).toContain('name: i-audit'); + }); + + test('should prefix skill references in body when prefix is set', () => { + const skills = [ + { name: 'audit', description: 'Audit', userInvokable: true, body: 'Run /polish after the audit skill.' }, + { name: 'polish', description: 'Polish', userInvokable: true, body: 'Polish body.' } + ]; + + transformKiro(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); + + const content = fs.readFileSync(path.join(TEST_DIR, 'kiro-prefixed/.kiro/skills/i-audit/SKILL.md'), 'utf-8'); + expect(content).toContain('/i-polish'); + expect(content).toContain('the i-audit skill'); + }); + + test('should clean existing directory before writing', () => { + const existingDir = path.join(TEST_DIR, 'kiro/.kiro/skills/old'); + fs.mkdirSync(existingDir, { recursive: true }); + fs.writeFileSync(path.join(existingDir, 'SKILL.md'), 'old'); + + const skills = [{ name: 'new', description: 'New', body: 'New' }]; + transformKiro(skills, TEST_DIR); + + expect(fs.existsSync(path.join(TEST_DIR, 'kiro/.kiro/skills/old/SKILL.md'))).toBe(false); + expect(fs.existsSync(path.join(TEST_DIR, 'kiro/.kiro/skills/new/SKILL.md'))).toBe(true); + }); + + test('should log correct summary', () => { + const consoleMock = mock(() => {}); + const originalLog = console.log; + console.log = consoleMock; + + const skills = [ + { name: 'skill1', description: 'Test', userInvokable: true, body: 'body' }, + { name: 'skill2', description: 'Test', userInvokable: false, body: 'body' } + ]; + + transformKiro(skills, TEST_DIR); + + console.log = originalLog; + + expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Kiro:')); + expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills')); + expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable')); + }); + + test('should log reference file count', () => { + const consoleMock = mock(() => {}); + const originalLog = console.log; + console.log = consoleMock; + + const skills = [ + { + name: 'test', + description: 'Test', + body: 'Body.', + references: [ + { name: 'ref1', content: 'Ref 1', filePath: '/fake/ref1.md' } + ] + } + ]; + + transformKiro(skills, TEST_DIR); + + console.log = originalLog; + + expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 reference files')); + }); + + test('should handle empty skills array', () => { + transformKiro([], TEST_DIR); + + const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'kiro/.kiro/skills')); + expect(skillDirs).toHaveLength(0); + }); + + test('should not include license if empty', () => { + const skills = [ + { + name: 'test', + description: 'Test', + license: '', + body: 'Body' + } + ]; + + transformKiro(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8'); + expect(content).not.toContain('license:'); + }); +}); diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js index d4cd064ce..ba8739d3e 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -9,7 +9,9 @@ import { cleanDir, writeFile, generateYamlFrontmatter, - readPatterns + readPatterns, + replacePlaceholders, + prefixSkillReferences } from '../../scripts/lib/utils.js'; // Temporary test directory @@ -596,3 +598,123 @@ name: frontend-design expect(patterns.length).toBe(3); }); }); + +describe('replacePlaceholders', () => { + test('should replace {{model}} with provider-specific value', () => { + expect(replacePlaceholders('Ask {{model}} for help.', 'claude-code')).toBe('Ask Claude for help.'); + expect(replacePlaceholders('Ask {{model}} for help.', 'gemini')).toBe('Ask Gemini for help.'); + expect(replacePlaceholders('Ask {{model}} for help.', 'codex')).toBe('Ask GPT for help.'); + expect(replacePlaceholders('Ask {{model}} for help.', 'cursor')).toBe('Ask the model for help.'); + expect(replacePlaceholders('Ask {{model}} for help.', 'agents')).toBe('Ask the model for help.'); + expect(replacePlaceholders('Ask {{model}} for help.', 'kiro')).toBe('Ask Claude for help.'); + }); + + test('should replace {{config_file}} with provider-specific value', () => { + expect(replacePlaceholders('See {{config_file}}.', 'claude-code')).toBe('See CLAUDE.md.'); + expect(replacePlaceholders('See {{config_file}}.', 'cursor')).toBe('See .cursorrules.'); + expect(replacePlaceholders('See {{config_file}}.', 'gemini')).toBe('See GEMINI.md.'); + expect(replacePlaceholders('See {{config_file}}.', 'codex')).toBe('See AGENTS.md.'); + expect(replacePlaceholders('See {{config_file}}.', 'agents')).toBe('See .github/copilot-instructions.md.'); + expect(replacePlaceholders('See {{config_file}}.', 'kiro')).toBe('See .kiro/settings.json.'); + }); + + test('should replace {{ask_instruction}} with provider-specific value', () => { + const result = replacePlaceholders('{{ask_instruction}}', 'claude-code'); + expect(result).toBe('STOP and call the AskUserQuestionTool to clarify.'); + + const cursorResult = replacePlaceholders('{{ask_instruction}}', 'cursor'); + expect(cursorResult).toBe('ask the user directly to clarify what you cannot infer.'); + }); + + test('should replace {{available_commands}} with command list', () => { + const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['audit', 'polish', 'optimize']); + expect(result).toBe('Commands: /audit, /polish, /optimize'); + }); + + test('should exclude teach-impeccable from {{available_commands}}', () => { + const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['audit', 'teach-impeccable', 'polish']); + expect(result).toBe('Commands: /audit, /polish'); + }); + + test('should exclude i-teach-impeccable from {{available_commands}}', () => { + const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['i-audit', 'i-teach-impeccable', 'i-polish']); + expect(result).toBe('Commands: /i-audit, /i-polish'); + }); + + test('should produce empty string for {{available_commands}} with no commands', () => { + const result = replacePlaceholders('Commands: {{available_commands}}.', 'claude-code', []); + expect(result).toBe('Commands: .'); + }); + + test('should replace multiple placeholders in the same string', () => { + const result = replacePlaceholders('{{model}} uses {{config_file}} and {{ask_instruction}}', 'claude-code'); + expect(result).toBe('Claude uses CLAUDE.md and STOP and call the AskUserQuestionTool to clarify.'); + }); + + test('should replace multiple occurrences of the same placeholder', () => { + const result = replacePlaceholders('{{model}} and {{model}} again.', 'gemini'); + expect(result).toBe('Gemini and Gemini again.'); + }); + + test('should fall back to cursor placeholders for unknown provider', () => { + const result = replacePlaceholders('{{model}} {{config_file}}', 'unknown-provider'); + expect(result).toBe('the model .cursorrules'); + }); +}); + +describe('prefixSkillReferences', () => { + test('should prefix /skillname command references', () => { + const result = prefixSkillReferences('Run /audit to check.', 'i-', ['audit', 'polish']); + expect(result).toBe('Run /i-audit to check.'); + }); + + test('should prefix "the skillname skill" references', () => { + const result = prefixSkillReferences('Use the audit skill for checks.', 'i-', ['audit', 'polish']); + expect(result).toBe('Use the i-audit skill for checks.'); + }); + + test('should prefix multiple different references', () => { + const result = prefixSkillReferences('Run /audit then /polish. The audit skill is great.', 'i-', ['audit', 'polish']); + expect(result).toContain('/i-audit'); + expect(result).toContain('/i-polish'); + expect(result).toContain('the i-audit skill'); + }); + + test('should not partially match longer skill names', () => { + const result = prefixSkillReferences('Run /teach-impeccable command.', 'i-', ['teach', 'teach-impeccable']); + expect(result).toBe('Run /i-teach-impeccable command.'); + }); + + test('should handle case-insensitive "the X skill" matching', () => { + const result = prefixSkillReferences('The audit skill is useful.', 'i-', ['audit']); + // The regex replaces case-insensitively, so "The" becomes "the" in the replacement + expect(result).toBe('the i-audit skill is useful.'); + }); + + test('should return content unchanged with empty prefix', () => { + const result = prefixSkillReferences('Run /audit.', '', ['audit']); + expect(result).toBe('Run /audit.'); + }); + + test('should return content unchanged with empty skill names', () => { + const result = prefixSkillReferences('Run /audit.', 'i-', []); + expect(result).toBe('Run /audit.'); + }); + + test('should not match /skillname inside longer words', () => { + const result = prefixSkillReferences('The /auditing process.', 'i-', ['audit']); + // 'auditing' starts with 'audit' but has trailing letters — should NOT match + expect(result).toBe('The /auditing process.'); + }); + + test('should match /skillname at end of string', () => { + const result = prefixSkillReferences('Run /audit', 'i-', ['audit']); + expect(result).toBe('Run /i-audit'); + }); + + test('should match /skillname before punctuation', () => { + const result = prefixSkillReferences('Try /audit, /polish.', 'i-', ['audit', 'polish']); + expect(result).toContain('/i-audit,'); + expect(result).toContain('/i-polish.'); + }); +});