mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 22:26:38 +03:00
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 <noreply@anthropic.com>
721 lines
23 KiB
JavaScript
721 lines
23 KiB
JavaScript
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import {
|
|
parseFrontmatter,
|
|
readFilesRecursive,
|
|
readSourceFiles,
|
|
ensureDir,
|
|
cleanDir,
|
|
writeFile,
|
|
generateYamlFrontmatter,
|
|
readPatterns,
|
|
replacePlaceholders,
|
|
prefixSkillReferences
|
|
} from '../../scripts/lib/utils.js';
|
|
|
|
// Temporary test directory
|
|
const TEST_DIR = path.join(process.cwd(), 'test-tmp');
|
|
|
|
describe('parseFrontmatter', () => {
|
|
test('should parse basic frontmatter with simple key-value pairs', () => {
|
|
const content = `---
|
|
name: test-skill
|
|
description: A test skill
|
|
---
|
|
|
|
This is the body content.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
expect(result.frontmatter.name).toBe('test-skill');
|
|
expect(result.frontmatter.description).toBe('A test skill');
|
|
expect(result.body).toBe('This is the body content.');
|
|
});
|
|
|
|
test('should parse frontmatter with args array', () => {
|
|
const content = `---
|
|
name: test-skill
|
|
description: A test skill
|
|
args:
|
|
- name: target
|
|
description: The target to normalize
|
|
required: false
|
|
- name: output
|
|
description: Output format
|
|
required: true
|
|
---
|
|
|
|
Body here.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
expect(result.frontmatter.name).toBe('test-skill');
|
|
expect(result.frontmatter.args).toBeArray();
|
|
expect(result.frontmatter.args).toHaveLength(2);
|
|
expect(result.frontmatter.args[0].name).toBe('target');
|
|
expect(result.frontmatter.args[0].description).toBe('The target to normalize');
|
|
expect(result.frontmatter.args[0].required).toBe(false);
|
|
expect(result.frontmatter.args[1].required).toBe(true);
|
|
});
|
|
|
|
test('should return empty frontmatter when no frontmatter present', () => {
|
|
const content = 'Just some content without frontmatter.';
|
|
const result = parseFrontmatter(content);
|
|
|
|
expect(result.frontmatter).toEqual({});
|
|
expect(result.body).toBe(content);
|
|
});
|
|
|
|
test('should handle empty body', () => {
|
|
const content = `---
|
|
name: test
|
|
---
|
|
`;
|
|
const result = parseFrontmatter(content);
|
|
|
|
expect(result.frontmatter.name).toBe('test');
|
|
expect(result.body).toBe('');
|
|
});
|
|
|
|
test('should handle frontmatter with license field', () => {
|
|
const content = `---
|
|
name: skill-name
|
|
description: A skill
|
|
license: MIT
|
|
---
|
|
|
|
Skill body.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
expect(result.frontmatter.license).toBe('MIT');
|
|
});
|
|
|
|
test('should parse user-invokable boolean', () => {
|
|
const content = `---
|
|
name: test-skill
|
|
user-invokable: true
|
|
---
|
|
|
|
Body.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
expect(result.frontmatter['user-invokable']).toBe(true);
|
|
});
|
|
|
|
test('should parse user-invokable as string true (code behavior)', () => {
|
|
const content = `---
|
|
name: test-skill
|
|
user-invokable: 'true'
|
|
---
|
|
|
|
Body.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
// The parseFrontmatter function doesn't strip quotes from YAML string values
|
|
expect(result.frontmatter['user-invokable']).toBe("'true'");
|
|
});
|
|
|
|
test('should parse allowed-tools field', () => {
|
|
const content = `---
|
|
name: test-skill
|
|
allowed-tools: Bash
|
|
---
|
|
|
|
Body.`;
|
|
|
|
const result = parseFrontmatter(content);
|
|
expect(result.frontmatter['allowed-tools']).toBe('Bash');
|
|
});
|
|
});
|
|
|
|
describe('generateYamlFrontmatter', () => {
|
|
test('should generate basic frontmatter', () => {
|
|
const data = {
|
|
name: 'test-skill',
|
|
description: 'A test'
|
|
};
|
|
|
|
const result = generateYamlFrontmatter(data);
|
|
expect(result).toContain('---');
|
|
expect(result).toContain('name: test-skill');
|
|
expect(result).toContain('description: A test');
|
|
});
|
|
|
|
test('should generate frontmatter with args array', () => {
|
|
const data = {
|
|
name: 'test',
|
|
description: 'Test skill',
|
|
args: [
|
|
{ name: 'target', description: 'The target', required: false },
|
|
{ name: 'output', description: 'Output format', required: true }
|
|
]
|
|
};
|
|
|
|
const result = generateYamlFrontmatter(data);
|
|
expect(result).toContain('args:');
|
|
expect(result).toContain('- name: target');
|
|
expect(result).toContain('description: The target');
|
|
expect(result).toContain('required: false');
|
|
expect(result).toContain('required: true');
|
|
});
|
|
|
|
test('should generate frontmatter with boolean', () => {
|
|
const data = {
|
|
name: 'test',
|
|
description: 'Test',
|
|
'user-invokable': true
|
|
};
|
|
|
|
const result = generateYamlFrontmatter(data);
|
|
expect(result).toContain('user-invokable: true');
|
|
});
|
|
|
|
test('should roundtrip: generate and parse back', () => {
|
|
const original = {
|
|
name: 'roundtrip-test',
|
|
description: 'Testing roundtrip',
|
|
args: [
|
|
{ name: 'arg1', description: 'First arg', required: true }
|
|
]
|
|
};
|
|
|
|
const yaml = generateYamlFrontmatter(original);
|
|
const content = `${yaml}\n\nBody content`;
|
|
const parsed = parseFrontmatter(content);
|
|
|
|
expect(parsed.frontmatter.name).toBe(original.name);
|
|
expect(parsed.frontmatter.description).toBe(original.description);
|
|
expect(parsed.frontmatter.args).toBeArray();
|
|
expect(parsed.frontmatter.args[0].name).toBe('arg1');
|
|
});
|
|
});
|
|
|
|
describe('ensureDir', () => {
|
|
afterEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should create directory if it does not exist', () => {
|
|
const testPath = path.join(TEST_DIR, 'new-dir');
|
|
ensureDir(testPath);
|
|
|
|
expect(fs.existsSync(testPath)).toBe(true);
|
|
expect(fs.statSync(testPath).isDirectory()).toBe(true);
|
|
});
|
|
|
|
test('should create nested directories', () => {
|
|
const testPath = path.join(TEST_DIR, 'level1', 'level2', 'level3');
|
|
ensureDir(testPath);
|
|
|
|
expect(fs.existsSync(testPath)).toBe(true);
|
|
});
|
|
|
|
test('should not throw if directory already exists', () => {
|
|
const testPath = path.join(TEST_DIR, 'existing');
|
|
fs.mkdirSync(testPath, { recursive: true });
|
|
|
|
expect(() => ensureDir(testPath)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('cleanDir', () => {
|
|
beforeEach(() => {
|
|
ensureDir(TEST_DIR);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should remove directory and all contents', () => {
|
|
const filePath = path.join(TEST_DIR, 'test.txt');
|
|
fs.writeFileSync(filePath, 'content');
|
|
|
|
expect(fs.existsSync(filePath)).toBe(true);
|
|
|
|
cleanDir(TEST_DIR);
|
|
expect(fs.existsSync(TEST_DIR)).toBe(false);
|
|
});
|
|
|
|
test('should not throw if directory does not exist', () => {
|
|
const nonExistent = path.join(TEST_DIR, 'does-not-exist');
|
|
expect(() => cleanDir(nonExistent)).not.toThrow();
|
|
});
|
|
|
|
test('should remove nested directories', () => {
|
|
const nestedPath = path.join(TEST_DIR, 'level1', 'level2');
|
|
ensureDir(nestedPath);
|
|
fs.writeFileSync(path.join(nestedPath, 'file.txt'), 'content');
|
|
|
|
cleanDir(TEST_DIR);
|
|
expect(fs.existsSync(TEST_DIR)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('writeFile', () => {
|
|
afterEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should write file with content', () => {
|
|
const filePath = path.join(TEST_DIR, 'test.txt');
|
|
const content = 'Hello, world!';
|
|
|
|
writeFile(filePath, content);
|
|
|
|
expect(fs.existsSync(filePath)).toBe(true);
|
|
expect(fs.readFileSync(filePath, 'utf-8')).toBe(content);
|
|
});
|
|
|
|
test('should create parent directories automatically', () => {
|
|
const filePath = path.join(TEST_DIR, 'nested', 'deep', 'file.txt');
|
|
writeFile(filePath, 'content');
|
|
|
|
expect(fs.existsSync(filePath)).toBe(true);
|
|
expect(fs.readFileSync(filePath, 'utf-8')).toBe('content');
|
|
});
|
|
|
|
test('should overwrite existing file', () => {
|
|
const filePath = path.join(TEST_DIR, 'file.txt');
|
|
writeFile(filePath, 'first');
|
|
writeFile(filePath, 'second');
|
|
|
|
expect(fs.readFileSync(filePath, 'utf-8')).toBe('second');
|
|
});
|
|
});
|
|
|
|
describe('readFilesRecursive', () => {
|
|
beforeEach(() => {
|
|
ensureDir(TEST_DIR);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should find all markdown files in directory', () => {
|
|
writeFile(path.join(TEST_DIR, 'file1.md'), 'content1');
|
|
writeFile(path.join(TEST_DIR, 'file2.md'), 'content2');
|
|
writeFile(path.join(TEST_DIR, 'file3.txt'), 'not markdown');
|
|
|
|
const files = readFilesRecursive(TEST_DIR);
|
|
expect(files).toHaveLength(2);
|
|
expect(files.some(f => f.endsWith('file1.md'))).toBe(true);
|
|
expect(files.some(f => f.endsWith('file2.md'))).toBe(true);
|
|
});
|
|
|
|
test('should find markdown files in nested directories', () => {
|
|
writeFile(path.join(TEST_DIR, 'root.md'), 'root');
|
|
writeFile(path.join(TEST_DIR, 'sub', 'nested.md'), 'nested');
|
|
writeFile(path.join(TEST_DIR, 'sub', 'deep', 'deeper.md'), 'deeper');
|
|
|
|
const files = readFilesRecursive(TEST_DIR);
|
|
expect(files).toHaveLength(3);
|
|
expect(files.some(f => f.endsWith('root.md'))).toBe(true);
|
|
expect(files.some(f => f.endsWith('nested.md'))).toBe(true);
|
|
expect(files.some(f => f.endsWith('deeper.md'))).toBe(true);
|
|
});
|
|
|
|
test('should return empty array for non-existent directory', () => {
|
|
const files = readFilesRecursive(path.join(TEST_DIR, 'does-not-exist'));
|
|
expect(files).toEqual([]);
|
|
});
|
|
|
|
test('should return empty array for directory with no markdown files', () => {
|
|
writeFile(path.join(TEST_DIR, 'file.txt'), 'text');
|
|
writeFile(path.join(TEST_DIR, 'file.js'), 'code');
|
|
|
|
const files = readFilesRecursive(TEST_DIR);
|
|
expect(files).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('readSourceFiles', () => {
|
|
const testRootDir = TEST_DIR;
|
|
|
|
beforeEach(() => {
|
|
ensureDir(testRootDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(testRootDir)) {
|
|
fs.rmSync(testRootDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should read and parse skill files from directory-based structure', () => {
|
|
const skillContent = `---
|
|
name: test-skill
|
|
description: A test skill
|
|
license: MIT
|
|
---
|
|
|
|
Skill instructions here.`;
|
|
|
|
const skillDir = path.join(testRootDir, 'source/skills/test-skill');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].name).toBe('test-skill');
|
|
expect(skills[0].description).toBe('A test skill');
|
|
expect(skills[0].license).toBe('MIT');
|
|
expect(skills[0].body).toBe('Skill instructions here.');
|
|
});
|
|
|
|
test('should read skill with user-invokable flag', () => {
|
|
const skillContent = `---
|
|
name: audit
|
|
description: Run technical quality checks
|
|
user-invokable: true
|
|
---
|
|
|
|
Audit the code.`;
|
|
|
|
const skillDir = path.join(testRootDir, 'source/skills/audit');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].userInvokable).toBe(true);
|
|
});
|
|
|
|
test('should read skill with reference files', () => {
|
|
const skillContent = `---
|
|
name: frontend-design
|
|
description: Frontend design skill
|
|
---
|
|
|
|
Frontend design instructions.`;
|
|
|
|
const skillDir = path.join(testRootDir, 'source/skills/frontend-design');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
|
|
|
const refDir = path.join(skillDir, 'reference');
|
|
ensureDir(refDir);
|
|
fs.writeFileSync(path.join(refDir, 'typography.md'), 'Typography reference content.');
|
|
fs.writeFileSync(path.join(refDir, 'color.md'), 'Color reference content.');
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].references).toHaveLength(2);
|
|
// References may not be in a specific order due to fs.readdirSync
|
|
const refNames = skills[0].references.map(r => r.name).sort();
|
|
expect(refNames).toEqual(['color', 'typography']);
|
|
});
|
|
|
|
test('should use filename as name if not in frontmatter', () => {
|
|
const skillDir = path.join(testRootDir, 'source/skills/my-skill');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'Just body, no frontmatter.');
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].name).toBe('my-skill');
|
|
});
|
|
|
|
test('should handle empty source directories', () => {
|
|
ensureDir(path.join(testRootDir, 'source/skills'));
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toEqual([]);
|
|
});
|
|
|
|
test('should read multiple skills', () => {
|
|
const skill1Dir = path.join(testRootDir, 'source/skills/skill1');
|
|
ensureDir(skill1Dir);
|
|
fs.writeFileSync(path.join(skill1Dir, 'SKILL.md'), '---\nname: skill1\n---\nSkill1');
|
|
|
|
const skill2Dir = path.join(testRootDir, 'source/skills/skill2');
|
|
ensureDir(skill2Dir);
|
|
fs.writeFileSync(path.join(skill2Dir, 'SKILL.md'), '---\nname: skill2\n---\nSkill2');
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toHaveLength(2);
|
|
});
|
|
|
|
test('should ignore non-md files in skill directories', () => {
|
|
const skillDir = path.join(testRootDir, 'source/skills/test-skill');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '---\nname: test-skill\n---\nBody');
|
|
|
|
const refDir = path.join(skillDir, 'reference');
|
|
ensureDir(refDir);
|
|
fs.writeFileSync(path.join(refDir, 'readme.txt'), 'Not a markdown file');
|
|
fs.writeFileSync(path.join(refDir, 'typography.md'), 'Valid reference');
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].references).toHaveLength(1);
|
|
expect(skills[0].references[0].name).toBe('typography');
|
|
});
|
|
|
|
test('should handle missing skills directory', () => {
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
expect(skills).toEqual([]);
|
|
});
|
|
|
|
test('should parse all frontmatter fields correctly', () => {
|
|
const skillContent = `---
|
|
name: test-skill
|
|
description: A comprehensive test skill
|
|
license: Apache-2.0
|
|
compatibility: claude-code
|
|
user-invokable: true
|
|
allowed-tools: Bash,Edit
|
|
---
|
|
|
|
Body content.`;
|
|
|
|
const skillDir = path.join(testRootDir, 'source/skills/test-skill');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
|
|
|
const { skills } = readSourceFiles(testRootDir);
|
|
|
|
expect(skills[0].name).toBe('test-skill');
|
|
expect(skills[0].description).toBe('A comprehensive test skill');
|
|
expect(skills[0].license).toBe('Apache-2.0');
|
|
expect(skills[0].compatibility).toBe('claude-code');
|
|
expect(skills[0].userInvokable).toBe(true);
|
|
expect(skills[0].allowedTools).toBe('Bash,Edit');
|
|
});
|
|
});
|
|
|
|
describe('readPatterns', () => {
|
|
const testRootDir = TEST_DIR;
|
|
|
|
beforeEach(() => {
|
|
ensureDir(testRootDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(testRootDir)) {
|
|
fs.rmSync(testRootDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should extract DO and DON\'T patterns from SKILL.md', () => {
|
|
const skillContent = `---
|
|
name: frontend-design
|
|
---
|
|
|
|
### Typography
|
|
**DO**: Use variable fonts for flexibility.
|
|
**DON'T**: Use system fonts like Arial.
|
|
|
|
### Color & Contrast
|
|
**DO**: Ensure WCAG AA compliance.
|
|
**DON'T**: Use gray text on colored backgrounds.
|
|
|
|
### Layout & Space
|
|
**DO**: Use consistent spacing scale.
|
|
**DON'T**: Nest cards inside cards.`;
|
|
|
|
const skillDir = path.join(testRootDir, 'source/skills/frontend-design');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
|
|
|
const { patterns, antipatterns } = readPatterns(testRootDir);
|
|
|
|
expect(patterns).toHaveLength(3);
|
|
expect(antipatterns).toHaveLength(3);
|
|
|
|
expect(patterns[0].name).toBe('Typography');
|
|
expect(patterns[0].items).toContain('Use variable fonts for flexibility.');
|
|
expect(antipatterns[0].items).toContain('Use system fonts like Arial.');
|
|
});
|
|
|
|
test('should normalize "Color & Theme" to "Color & Contrast"', () => {
|
|
const skillContent = `---
|
|
name: frontend-design
|
|
---
|
|
|
|
### Color & Theme
|
|
**DO**: Use OKLCH color space.
|
|
**DON'T**: Use pure black.`;
|
|
|
|
const skillDir = path.join(testRootDir, 'source/skills/frontend-design');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
|
|
|
const { patterns, antipatterns } = readPatterns(testRootDir);
|
|
|
|
expect(patterns[0].name).toBe('Color & Contrast');
|
|
});
|
|
|
|
test('should handle missing SKILL.md file', () => {
|
|
ensureDir(path.join(testRootDir, 'source/skills/frontend-design'));
|
|
|
|
const { patterns, antipatterns } = readPatterns(testRootDir);
|
|
|
|
expect(patterns).toEqual([]);
|
|
expect(antipatterns).toEqual([]);
|
|
});
|
|
|
|
test('should return patterns in consistent section order', () => {
|
|
const skillContent = `---
|
|
name: frontend-design
|
|
---
|
|
|
|
### Motion
|
|
**DO**: Use ease-out for natural movement.
|
|
|
|
### Typography
|
|
**DO**: Use modular scale.
|
|
|
|
### Color & Contrast
|
|
**DO**: Use tinted neutrals.`;
|
|
|
|
const skillDir = path.join(testRootDir, 'source/skills/frontend-design');
|
|
ensureDir(skillDir);
|
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
|
|
|
const { patterns } = readPatterns(testRootDir);
|
|
|
|
// Patterns are returned in predefined section order, not source order
|
|
// Only sections with content are included
|
|
expect(patterns[0].name).toBe('Typography');
|
|
expect(patterns[1].name).toBe('Color & Contrast');
|
|
expect(patterns[2].name).toBe('Motion');
|
|
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.');
|
|
});
|
|
});
|