mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Merge pull request #16 from hobostay/fix-tests
Fix broken tests to match actual implementation
This commit is contained in:
+125
-109
@@ -1,4 +1,4 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
|
||||
import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import * as utils from '../scripts/lib/utils.js';
|
||||
@@ -22,7 +22,6 @@ describe('build orchestration', () => {
|
||||
|
||||
test('should call readSourceFiles with root directory', () => {
|
||||
const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
|
||||
commands: [],
|
||||
skills: []
|
||||
});
|
||||
|
||||
@@ -35,14 +34,15 @@ describe('build orchestration', () => {
|
||||
const ROOT_DIR = TEST_DIR;
|
||||
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
|
||||
const { commands, skills } = utils.readSourceFiles(ROOT_DIR);
|
||||
transformers.transformCursor(commands, skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformers.transformGemini(commands, skills, DIST_DIR);
|
||||
transformers.transformCodex(commands, skills, DIST_DIR);
|
||||
const { skills } = utils.readSourceFiles(ROOT_DIR);
|
||||
const patterns = utils.readPatterns(ROOT_DIR);
|
||||
transformers.transformCursor(skills, DIST_DIR, patterns);
|
||||
transformers.transformClaudeCode(skills, DIST_DIR, patterns);
|
||||
transformers.transformGemini(skills, DIST_DIR, patterns);
|
||||
transformers.transformCodex(skills, DIST_DIR, patterns);
|
||||
|
||||
expect(readSourceFilesSpy).toHaveBeenCalledWith(ROOT_DIR);
|
||||
|
||||
|
||||
readSourceFilesSpy.mockRestore();
|
||||
transformCursorSpy.mockRestore();
|
||||
transformClaudeCodeSpy.mockRestore();
|
||||
@@ -50,18 +50,16 @@ describe('build orchestration', () => {
|
||||
transformCodexSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('should call all four transformers with correct arguments', () => {
|
||||
const commands = [
|
||||
{ name: 'cmd1', description: 'Command 1', args: [], body: 'Body 1' }
|
||||
];
|
||||
test('should call all transformers with correct arguments', () => {
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Skill body 1' }
|
||||
];
|
||||
const patterns = { patterns: [], antipatterns: [] };
|
||||
|
||||
const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
|
||||
commands,
|
||||
skills
|
||||
});
|
||||
const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue(patterns);
|
||||
|
||||
const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {});
|
||||
const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {});
|
||||
@@ -72,17 +70,19 @@ describe('build orchestration', () => {
|
||||
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
|
||||
const sourceFiles = utils.readSourceFiles(ROOT_DIR);
|
||||
transformers.transformCursor(sourceFiles.commands, sourceFiles.skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(sourceFiles.commands, sourceFiles.skills, DIST_DIR);
|
||||
transformers.transformGemini(sourceFiles.commands, sourceFiles.skills, DIST_DIR);
|
||||
transformers.transformCodex(sourceFiles.commands, sourceFiles.skills, DIST_DIR);
|
||||
const patternData = utils.readPatterns(ROOT_DIR);
|
||||
transformers.transformCursor(sourceFiles.skills, DIST_DIR, patternData);
|
||||
transformers.transformClaudeCode(sourceFiles.skills, DIST_DIR, patternData);
|
||||
transformers.transformGemini(sourceFiles.skills, DIST_DIR, patternData);
|
||||
transformers.transformCodex(sourceFiles.skills, DIST_DIR, patternData);
|
||||
|
||||
expect(transformCursorSpy).toHaveBeenCalledWith(commands, skills, DIST_DIR);
|
||||
expect(transformClaudeCodeSpy).toHaveBeenCalledWith(commands, skills, DIST_DIR);
|
||||
expect(transformGeminiSpy).toHaveBeenCalledWith(commands, skills, DIST_DIR);
|
||||
expect(transformCodexSpy).toHaveBeenCalledWith(commands, skills, DIST_DIR);
|
||||
expect(transformCursorSpy).toHaveBeenCalledWith(skills, DIST_DIR, patterns);
|
||||
expect(transformClaudeCodeSpy).toHaveBeenCalledWith(skills, DIST_DIR, patterns);
|
||||
expect(transformGeminiSpy).toHaveBeenCalledWith(skills, DIST_DIR, patterns);
|
||||
expect(transformCodexSpy).toHaveBeenCalledWith(skills, DIST_DIR, patterns);
|
||||
|
||||
readSourceFilesSpy.mockRestore();
|
||||
readPatternsSpy.mockRestore();
|
||||
transformCursorSpy.mockRestore();
|
||||
transformClaudeCodeSpy.mockRestore();
|
||||
transformGeminiSpy.mockRestore();
|
||||
@@ -90,10 +90,12 @@ describe('build orchestration', () => {
|
||||
});
|
||||
|
||||
test('should handle empty source files', () => {
|
||||
const patterns = { patterns: [], antipatterns: [] };
|
||||
|
||||
const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
|
||||
commands: [],
|
||||
skills: []
|
||||
});
|
||||
const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue(patterns);
|
||||
|
||||
const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {});
|
||||
const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {});
|
||||
@@ -103,18 +105,20 @@ describe('build orchestration', () => {
|
||||
const ROOT_DIR = TEST_DIR;
|
||||
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
|
||||
const { commands, skills } = utils.readSourceFiles(ROOT_DIR);
|
||||
transformers.transformCursor(commands, skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformers.transformGemini(commands, skills, DIST_DIR);
|
||||
transformers.transformCodex(commands, skills, DIST_DIR);
|
||||
const { skills } = utils.readSourceFiles(ROOT_DIR);
|
||||
const patternData = utils.readPatterns(ROOT_DIR);
|
||||
transformers.transformCursor(skills, DIST_DIR, patternData);
|
||||
transformers.transformClaudeCode(skills, DIST_DIR, patternData);
|
||||
transformers.transformGemini(skills, DIST_DIR, patternData);
|
||||
transformers.transformCodex(skills, DIST_DIR, patternData);
|
||||
|
||||
expect(transformCursorSpy).toHaveBeenCalledWith([], [], DIST_DIR);
|
||||
expect(transformClaudeCodeSpy).toHaveBeenCalledWith([], [], DIST_DIR);
|
||||
expect(transformGeminiSpy).toHaveBeenCalledWith([], [], DIST_DIR);
|
||||
expect(transformCodexSpy).toHaveBeenCalledWith([], [], DIST_DIR);
|
||||
expect(transformCursorSpy).toHaveBeenCalledWith([], DIST_DIR, patterns);
|
||||
expect(transformClaudeCodeSpy).toHaveBeenCalledWith([], DIST_DIR, patterns);
|
||||
expect(transformGeminiSpy).toHaveBeenCalledWith([], DIST_DIR, patterns);
|
||||
expect(transformCodexSpy).toHaveBeenCalledWith([], DIST_DIR, patterns);
|
||||
|
||||
readSourceFilesSpy.mockRestore();
|
||||
readPatternsSpy.mockRestore();
|
||||
transformCursorSpy.mockRestore();
|
||||
transformClaudeCodeSpy.mockRestore();
|
||||
transformGeminiSpy.mockRestore();
|
||||
@@ -123,17 +127,6 @@ describe('build orchestration', () => {
|
||||
|
||||
test('integration: full build creates all expected outputs', () => {
|
||||
// Create test source files
|
||||
const commandContent = `---
|
||||
name: test-command
|
||||
description: A test command
|
||||
args:
|
||||
- name: target
|
||||
description: Target parameter
|
||||
required: false
|
||||
---
|
||||
|
||||
This is a test command body with {{target}} placeholder.`;
|
||||
|
||||
const skillContent = `---
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
@@ -142,113 +135,121 @@ license: MIT
|
||||
|
||||
This is a test skill body.`;
|
||||
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/commands/test-command.md'), commandContent);
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/skills/test-skill.md'), skillContent);
|
||||
const skillDir = path.join(TEST_DIR, 'source/skills/test-skill');
|
||||
fs.mkdirSync(skillDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
||||
|
||||
// Run the build process
|
||||
const DIST_DIR = path.join(TEST_DIR, 'dist');
|
||||
const { commands, skills } = utils.readSourceFiles(TEST_DIR);
|
||||
|
||||
transformers.transformCursor(commands, skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformers.transformGemini(commands, skills, DIST_DIR);
|
||||
transformers.transformCodex(commands, skills, DIST_DIR);
|
||||
const { skills } = utils.readSourceFiles(TEST_DIR);
|
||||
const patterns = utils.readPatterns(TEST_DIR);
|
||||
|
||||
transformers.transformCursor(skills, DIST_DIR, patterns);
|
||||
transformers.transformClaudeCode(skills, DIST_DIR, patterns);
|
||||
transformers.transformGemini(skills, DIST_DIR, patterns);
|
||||
transformers.transformCodex(skills, DIST_DIR, patterns);
|
||||
|
||||
// Verify Cursor outputs
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/commands/test-command.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/rules/test-skill.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/.cursor/skills/test-skill/SKILL.md'))).toBe(true);
|
||||
|
||||
// Verify Claude Code outputs
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'claude-code/commands/test-command.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'claude-code/skills/test-skill/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'claude-code/.claude/skills/test-skill/SKILL.md'))).toBe(true);
|
||||
|
||||
// Verify Gemini outputs
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'gemini/commands/test-command.toml'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'gemini/GEMINI.test-skill.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'gemini/GEMINI.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'gemini/.gemini/skills/test-skill/SKILL.md'))).toBe(true);
|
||||
|
||||
// Verify Codex outputs
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'codex/prompts/test-command.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'codex/AGENTS.test-skill.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'codex/AGENTS.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'codex/.codex/skills/test-skill/SKILL.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('integration: verify transformations are correct', () => {
|
||||
const commandContent = `---
|
||||
name: normalize
|
||||
description: Normalize design
|
||||
const skillContent = `---
|
||||
name: audit
|
||||
description: Run technical quality checks
|
||||
user-invokable: true
|
||||
args:
|
||||
- name: target
|
||||
description: Target element
|
||||
required: false
|
||||
---
|
||||
|
||||
Please normalize {{target}} to match the design system.`;
|
||||
Please audit {{target}} for technical quality. Ask {{model}} for help.`;
|
||||
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/commands/normalize.md'), commandContent);
|
||||
const skillDir = path.join(TEST_DIR, 'source/skills/audit');
|
||||
fs.mkdirSync(skillDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
||||
|
||||
const DIST_DIR = path.join(TEST_DIR, 'dist');
|
||||
const { commands, skills } = utils.readSourceFiles(TEST_DIR);
|
||||
|
||||
transformers.transformCursor(commands, skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformers.transformGemini(commands, skills, DIST_DIR);
|
||||
transformers.transformCodex(commands, skills, DIST_DIR);
|
||||
const { skills } = utils.readSourceFiles(TEST_DIR);
|
||||
const patterns = utils.readPatterns(TEST_DIR);
|
||||
|
||||
// Verify Cursor: body only, no frontmatter
|
||||
const cursorContent = fs.readFileSync(path.join(DIST_DIR, 'cursor/commands/normalize.md'), 'utf-8');
|
||||
expect(cursorContent).not.toContain('---');
|
||||
transformers.transformCursor(skills, DIST_DIR, patterns);
|
||||
transformers.transformClaudeCode(skills, DIST_DIR, patterns);
|
||||
transformers.transformGemini(skills, DIST_DIR, patterns);
|
||||
transformers.transformCodex(skills, DIST_DIR, patterns);
|
||||
|
||||
// Verify Cursor: full frontmatter with user-invokable
|
||||
const cursorContent = fs.readFileSync(path.join(DIST_DIR, 'cursor/.cursor/skills/audit/SKILL.md'), 'utf-8');
|
||||
expect(cursorContent).toContain('---');
|
||||
expect(cursorContent).toContain('name: audit');
|
||||
expect(cursorContent).toContain('{{target}}');
|
||||
expect(cursorContent).toContain('the model');
|
||||
|
||||
// Verify Claude Code: full frontmatter
|
||||
const claudeContent = fs.readFileSync(path.join(DIST_DIR, 'claude-code/commands/normalize.md'), 'utf-8');
|
||||
// Verify Claude Code: full frontmatter with user-invokable and args
|
||||
const claudeContent = fs.readFileSync(path.join(DIST_DIR, 'claude-code/.claude/skills/audit/SKILL.md'), 'utf-8');
|
||||
expect(claudeContent).toContain('---');
|
||||
expect(claudeContent).toContain('name: normalize');
|
||||
expect(claudeContent).toContain('name: audit');
|
||||
expect(claudeContent).toContain('user-invokable: true');
|
||||
expect(claudeContent).toContain('{{target}}');
|
||||
expect(claudeContent).toContain('Claude');
|
||||
|
||||
// Verify Gemini: TOML with {{args}}
|
||||
const geminiContent = fs.readFileSync(path.join(DIST_DIR, 'gemini/commands/normalize.toml'), 'utf-8');
|
||||
expect(geminiContent).toContain('description = "Normalize design"');
|
||||
expect(geminiContent).toContain('{{args}}');
|
||||
expect(geminiContent).not.toContain('{{target}}');
|
||||
// Verify Gemini: skill in skills directory
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'gemini/.gemini/skills/audit/SKILL.md'))).toBe(true);
|
||||
const geminiContent = fs.readFileSync(path.join(DIST_DIR, 'gemini/.gemini/skills/audit/SKILL.md'), 'utf-8');
|
||||
expect(geminiContent).toContain('{{args}}'); // Replaced for user-invokable in Gemini
|
||||
expect(geminiContent).toContain('Gemini');
|
||||
|
||||
// Verify Codex: $VARIABLE
|
||||
const codexContent = fs.readFileSync(path.join(DIST_DIR, 'codex/prompts/normalize.md'), 'utf-8');
|
||||
expect(codexContent).toContain('$TARGET');
|
||||
expect(codexContent).not.toContain('{{target}}');
|
||||
// Verify Codex: skill in skills directory
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'codex/.codex/skills/audit/SKILL.md'))).toBe(true);
|
||||
const codexContent = fs.readFileSync(path.join(DIST_DIR, 'codex/.codex/skills/audit/SKILL.md'), 'utf-8');
|
||||
expect(codexContent).toContain('$TARGET'); // Replaced for user-invokable in Codex
|
||||
expect(codexContent).toContain('GPT');
|
||||
});
|
||||
|
||||
test('integration: multiple commands and skills', () => {
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/commands/cmd1.md'), '---\nname: cmd1\n---\nBody1');
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/commands/cmd2.md'), '---\nname: cmd2\n---\nBody2');
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/skills/skill1.md'), '---\nname: skill1\n---\nSkill1');
|
||||
utils.writeFile(path.join(TEST_DIR, 'source/skills/skill2.md'), '---\nname: skill2\n---\nSkill2');
|
||||
test('integration: multiple skills', () => {
|
||||
const skill1Dir = path.join(TEST_DIR, 'source/skills/skill1');
|
||||
fs.mkdirSync(skill1Dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(skill1Dir, 'SKILL.md'), '---\nname: skill1\n---\nSkill1');
|
||||
|
||||
const skill2Dir = path.join(TEST_DIR, 'source/skills/skill2');
|
||||
fs.mkdirSync(skill2Dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(skill2Dir, 'SKILL.md'), '---\nname: skill2\n---\nSkill2');
|
||||
|
||||
const DIST_DIR = path.join(TEST_DIR, 'dist');
|
||||
const { commands, skills } = utils.readSourceFiles(TEST_DIR);
|
||||
|
||||
expect(commands).toHaveLength(2);
|
||||
const { skills } = utils.readSourceFiles(TEST_DIR);
|
||||
const patterns = utils.readPatterns(TEST_DIR);
|
||||
|
||||
expect(skills).toHaveLength(2);
|
||||
|
||||
transformers.transformCursor(commands, skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformers.transformGemini(commands, skills, DIST_DIR);
|
||||
transformers.transformCodex(commands, skills, DIST_DIR);
|
||||
transformers.transformCursor(skills, DIST_DIR, patterns);
|
||||
transformers.transformClaudeCode(skills, DIST_DIR, patterns);
|
||||
transformers.transformGemini(skills, DIST_DIR, patterns);
|
||||
transformers.transformCodex(skills, DIST_DIR, patterns);
|
||||
|
||||
// Verify all files exist
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/commands/cmd1.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/commands/cmd2.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/rules/skill1.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/rules/skill2.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/.cursor/skills/skill1/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'cursor/.cursor/skills/skill2/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'claude-code/.claude/skills/skill1/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'claude-code/.claude/skills/skill2/SKILL.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should call transformers in correct order', () => {
|
||||
const callOrder = [];
|
||||
|
||||
const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
|
||||
commands: [],
|
||||
skills: []
|
||||
});
|
||||
const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue({ patterns: [], antipatterns: [] });
|
||||
|
||||
const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {
|
||||
callOrder.push('cursor');
|
||||
@@ -266,19 +267,34 @@ Please normalize {{target}} to match the design system.`;
|
||||
const ROOT_DIR = TEST_DIR;
|
||||
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
|
||||
const { commands, skills } = utils.readSourceFiles(ROOT_DIR);
|
||||
transformers.transformCursor(commands, skills, DIST_DIR);
|
||||
transformers.transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformers.transformGemini(commands, skills, DIST_DIR);
|
||||
transformers.transformCodex(commands, skills, DIST_DIR);
|
||||
const { skills } = utils.readSourceFiles(ROOT_DIR);
|
||||
const patterns = utils.readPatterns(ROOT_DIR);
|
||||
transformers.transformCursor(skills, DIST_DIR, patterns);
|
||||
transformers.transformClaudeCode(skills, DIST_DIR, patterns);
|
||||
transformers.transformGemini(skills, DIST_DIR, patterns);
|
||||
transformers.transformCodex(skills, DIST_DIR, patterns);
|
||||
|
||||
expect(callOrder).toEqual(['cursor', 'claude-code', 'gemini', 'codex']);
|
||||
|
||||
readSourceFilesSpy.mockRestore();
|
||||
readPatternsSpy.mockRestore();
|
||||
transformCursorSpy.mockRestore();
|
||||
transformClaudeCodeSpy.mockRestore();
|
||||
transformGeminiSpy.mockRestore();
|
||||
transformCodexSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
test('should include agents and kiro transformers', () => {
|
||||
const { skills } = utils.readSourceFiles(TEST_DIR);
|
||||
const patterns = utils.readPatterns(TEST_DIR);
|
||||
const DIST_DIR = path.join(TEST_DIR, 'dist');
|
||||
|
||||
// These should not throw
|
||||
transformers.transformAgents(skills, DIST_DIR, patterns);
|
||||
transformers.transformKiro(skills, DIST_DIR, patterns);
|
||||
|
||||
// Verify outputs
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'agents/.agents/skills'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(DIST_DIR, 'kiro/.kiro/skills'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,63 +20,14 @@ describe('transformClaudeCode', () => {
|
||||
});
|
||||
|
||||
test('should create correct directory structure', () => {
|
||||
const commands = [];
|
||||
const skills = [];
|
||||
|
||||
transformClaudeCode(commands, skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/commands'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/skills'))).toBe(true);
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should preserve full frontmatter for commands', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'test-command',
|
||||
description: 'A test command',
|
||||
args: [
|
||||
{ name: 'target', description: 'The target', required: false },
|
||||
{ name: 'output', description: 'Output format', required: true }
|
||||
],
|
||||
body: 'Command body here.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'claude-code/commands/test-command.md');
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.name).toBe('test-command');
|
||||
expect(parsed.frontmatter.description).toBe('A test command');
|
||||
expect(parsed.frontmatter.args).toBeArray();
|
||||
expect(parsed.frontmatter.args).toHaveLength(2);
|
||||
expect(parsed.frontmatter.args[0].name).toBe('target');
|
||||
expect(parsed.frontmatter.args[1].required).toBe(true);
|
||||
expect(parsed.body).toBe('Command body here.');
|
||||
});
|
||||
|
||||
test('should handle commands without args', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'simple-cmd',
|
||||
description: 'Simple command',
|
||||
args: [],
|
||||
body: 'Simple body.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/commands/simple-cmd.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.name).toBe('simple-cmd');
|
||||
expect(parsed.frontmatter.args).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should create skills in subdirectories with SKILL.md filename', () => {
|
||||
test('should create skill with full frontmatter', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test-skill',
|
||||
@@ -85,21 +36,84 @@ describe('transformClaudeCode', () => {
|
||||
body: 'Skill instructions.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode([], skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'claude-code/skills/test-skill/SKILL.md');
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'claude-code/.claude/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 add user-invokable flag for user-invokable skills', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'audit',
|
||||
description: 'Audit command',
|
||||
userInvokable: true,
|
||||
body: 'Audit the code.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/audit/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter['user-invokable']).toBe(true);
|
||||
});
|
||||
|
||||
test('should include args in frontmatter for user-invokable skills', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test-command',
|
||||
description: 'A test command',
|
||||
userInvokable: true,
|
||||
args: [
|
||||
{ name: 'target', description: 'The target', required: false },
|
||||
{ name: 'output', description: 'Output format', required: true }
|
||||
],
|
||||
body: 'Command body here.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'claude-code/.claude/skills/test-command/SKILL.md');
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.args).toBeArray();
|
||||
expect(parsed.frontmatter.args).toHaveLength(2);
|
||||
expect(parsed.frontmatter.args[0].name).toBe('target');
|
||||
expect(parsed.frontmatter.args[1].required).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle skills without args', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'simple-skill',
|
||||
description: 'Simple skill',
|
||||
userInvokable: true,
|
||||
args: [],
|
||||
body: 'Simple body.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/simple-skill/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.args).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should handle skills without license', () => {
|
||||
const skills = [
|
||||
{
|
||||
@@ -109,27 +123,13 @@ describe('transformClaudeCode', () => {
|
||||
body: 'Body content.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode([], skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/skills/no-license-skill/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.license).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should handle multiple commands', () => {
|
||||
const commands = [
|
||||
{ name: 'cmd1', description: 'Command 1', args: [], body: 'Body 1' },
|
||||
{ name: 'cmd2', description: 'Command 2', args: [], body: 'Body 2' },
|
||||
{ name: 'cmd3', description: 'Command 3', args: [], body: 'Body 3' }
|
||||
];
|
||||
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/commands/cmd1.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/commands/cmd2.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/commands/cmd3.md'))).toBe(true);
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/no-license-skill/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.license).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should handle multiple skills', () => {
|
||||
@@ -137,70 +137,98 @@ describe('transformClaudeCode', () => {
|
||||
{ name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Body 1' },
|
||||
{ name: 'skill2', description: 'Skill 2', license: 'Apache', body: 'Body 2' }
|
||||
];
|
||||
|
||||
transformClaudeCode([], skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/skills/skill1/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/skills/skill2/SKILL.md'))).toBe(true);
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills/skill1/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills/skill2/SKILL.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should clean existing directory before writing', () => {
|
||||
fs.mkdirSync(path.join(TEST_DIR, 'claude-code/commands'), { recursive: true });
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'claude-code/commands/old.md'), 'old');
|
||||
|
||||
const commands = [{ name: 'new', description: 'New', args: [], body: 'New' }];
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/commands/old.md'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/commands/new.md'))).toBe(true);
|
||||
// Create a pre-existing file structure
|
||||
const existingDir = path.join(TEST_DIR, 'claude-code/.claude/skills/old');
|
||||
fs.mkdirSync(existingDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(existingDir, 'SKILL.md'), 'old');
|
||||
|
||||
const skills = [{ name: 'new', description: 'New', license: '', body: 'New' }];
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills/old/SKILL.md'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills/new/SKILL.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should preserve {{placeholder}} syntax in body', () => {
|
||||
const commands = [
|
||||
const skills = [
|
||||
{
|
||||
name: 'with-placeholder',
|
||||
description: 'Has placeholder',
|
||||
userInvokable: true,
|
||||
args: [{ name: 'target', description: 'Target', required: false }],
|
||||
body: 'Process {{target}} and generate output.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/commands/with-placeholder.md'), 'utf-8');
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/with-placeholder/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('{{target}}');
|
||||
});
|
||||
|
||||
test('should copy reference files', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'frontend-design',
|
||||
description: 'Design skill',
|
||||
license: 'MIT',
|
||||
body: 'Design instructions.',
|
||||
references: [
|
||||
{ name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' },
|
||||
{ name: 'color', content: 'Color reference', filePath: '/fake/path/color.md' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills/frontend-design/reference/typography.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills/frontend-design/reference/color.md'))).toBe(true);
|
||||
|
||||
const typoContent = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/frontend-design/reference/typography.md'), 'utf-8');
|
||||
expect(typoContent).toBe('Typography reference');
|
||||
});
|
||||
|
||||
test('should log correct summary', () => {
|
||||
const consoleMock = mock(() => {});
|
||||
const originalLog = console.log;
|
||||
console.log = consoleMock;
|
||||
|
||||
const commands = [{ name: 'cmd1', description: 'Test', args: [], body: 'body' }];
|
||||
const skills = [{ name: 'skill1', description: 'Test', license: '', body: 'body' }];
|
||||
|
||||
transformClaudeCode(commands, skills, TEST_DIR);
|
||||
|
||||
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'Test', license: '', userInvokable: true, body: 'body' },
|
||||
{ name: 'skill2', description: 'Test', license: '', userInvokable: false, body: 'body' }
|
||||
];
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
console.log = originalLog;
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledWith('✓ Claude Code: 1 commands, 1 skills');
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Claude Code:'));
|
||||
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
|
||||
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable'));
|
||||
});
|
||||
|
||||
test('should handle empty arrays', () => {
|
||||
transformClaudeCode([], [], TEST_DIR);
|
||||
|
||||
const commandFiles = fs.readdirSync(path.join(TEST_DIR, 'claude-code/commands'));
|
||||
const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'claude-code/skills'));
|
||||
|
||||
expect(commandFiles).toHaveLength(0);
|
||||
transformClaudeCode([], TEST_DIR);
|
||||
|
||||
const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'claude-code/.claude/skills'));
|
||||
expect(skillDirs).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should format frontmatter correctly with args', () => {
|
||||
const commands = [
|
||||
const skills = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
userInvokable: true,
|
||||
args: [
|
||||
{ name: 'arg1', description: 'First arg', required: true },
|
||||
{ name: 'arg2', description: 'Second arg', required: false }
|
||||
@@ -208,11 +236,11 @@ describe('transformClaudeCode', () => {
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/commands/test.md'), 'utf-8');
|
||||
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/test/SKILL.md'), 'utf-8');
|
||||
|
||||
expect(content).toContain('---');
|
||||
expect(content).toContain('name: test');
|
||||
expect(content).toContain('description: Test command');
|
||||
@@ -225,11 +253,11 @@ describe('transformClaudeCode', () => {
|
||||
});
|
||||
|
||||
test('should preserve multiline body content', () => {
|
||||
const commands = [
|
||||
const skills = [
|
||||
{
|
||||
name: 'multiline',
|
||||
description: 'Test',
|
||||
args: [],
|
||||
license: '',
|
||||
body: `First paragraph.
|
||||
|
||||
Second paragraph with details.
|
||||
@@ -238,15 +266,91 @@ Second paragraph with details.
|
||||
- List item 2`
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/commands/multiline.md'), 'utf-8');
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/multiline/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
|
||||
expect(parsed.body).toContain('First paragraph.');
|
||||
expect(parsed.body).toContain('Second paragraph');
|
||||
expect(parsed.body).toContain('- List item 1');
|
||||
});
|
||||
});
|
||||
|
||||
test('should support prefix option', () => {
|
||||
const skills = [
|
||||
{ name: 'audit', description: 'Audit', license: '', userInvokable: true, body: 'Audit body' }
|
||||
];
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'claude-code-prefixed/.claude/skills/i-audit/SKILL.md'))).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code-prefixed/.claude/skills/i-audit/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('name: i-audit');
|
||||
});
|
||||
|
||||
test('should include compatibility in frontmatter', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test',
|
||||
compatibility: 'claude-code',
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/test/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('compatibility: claude-code');
|
||||
});
|
||||
|
||||
test('should include allowed-tools in frontmatter', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test',
|
||||
allowedTools: 'Bash,Edit',
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/test/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('allowed-tools: Bash,Edit');
|
||||
});
|
||||
|
||||
test('should replace {{model}} placeholder', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test',
|
||||
license: '',
|
||||
body: 'Ask {{model}} for help.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/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',
|
||||
license: '',
|
||||
body: 'See {{config_file}} for more.'
|
||||
}
|
||||
];
|
||||
|
||||
transformClaudeCode(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/test/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('See CLAUDE.md for more.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,140 +20,14 @@ describe('transformCodex', () => {
|
||||
});
|
||||
|
||||
test('should create correct directory structure', () => {
|
||||
const commands = [];
|
||||
const skills = [];
|
||||
|
||||
transformCodex(commands, skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/prompts'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex'))).toBe(true);
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/.codex/skills'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should create command with custom frontmatter format', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'test-command',
|
||||
description: 'A test command',
|
||||
args: [],
|
||||
body: 'Command body content.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'codex/prompts/test-command.md');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter.description).toBe('A test command');
|
||||
expect(parsed.body).toBe('Command body content.');
|
||||
});
|
||||
|
||||
test('should create argument-hint for required args', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'with-args',
|
||||
description: 'Command with args',
|
||||
args: [
|
||||
{ name: 'target', description: 'Target', required: true },
|
||||
{ name: 'output', description: 'Output', required: true }
|
||||
],
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/with-args.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter['argument-hint']).toBe('<target> <output>');
|
||||
});
|
||||
|
||||
test('should create argument-hint for optional args', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'optional-args',
|
||||
description: 'Command with optional args',
|
||||
args: [
|
||||
{ name: 'format', description: 'Format', required: false }
|
||||
],
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/optional-args.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter['argument-hint']).toBe('[FORMAT=<value>]');
|
||||
});
|
||||
|
||||
test('should create argument-hint with mixed required and optional args', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'mixed-args',
|
||||
description: 'Mixed args',
|
||||
args: [
|
||||
{ name: 'input', description: 'Input', required: true },
|
||||
{ name: 'format', description: 'Format', required: false },
|
||||
{ name: 'output', description: 'Output', required: true }
|
||||
],
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/mixed-args.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter['argument-hint']).toBe('<input> [FORMAT=<value>] <output>');
|
||||
});
|
||||
|
||||
test('should transform {{argname}} to $ARGNAME', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'normalize',
|
||||
description: 'Normalize',
|
||||
args: [{ name: 'target', description: 'Target', required: false }],
|
||||
body: 'Please normalize {{target}} to match the design system.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/normalize.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.body).toContain('$TARGET');
|
||||
expect(parsed.body).not.toContain('{{target}}');
|
||||
});
|
||||
|
||||
test('should transform multiple different placeholders', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'multi-arg',
|
||||
description: 'Multiple args',
|
||||
args: [],
|
||||
body: 'Process {{input}} and output to {{output}} with {{format}}.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/multi-arg.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.body).toContain('$INPUT');
|
||||
expect(parsed.body).toContain('$OUTPUT');
|
||||
expect(parsed.body).toContain('$FORMAT');
|
||||
});
|
||||
|
||||
test('should create modular skill files', () => {
|
||||
test('should create skill files with frontmatter and body in .codex/skills/ directory', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test-skill',
|
||||
@@ -162,96 +36,213 @@ describe('transformCodex', () => {
|
||||
body: 'Skill instructions here.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex([], skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'codex/AGENTS.test-skill.md');
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'codex/.codex/skills/test-skill/SKILL.md');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
expect(content).toBe('Skill instructions here.');
|
||||
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 here.');
|
||||
});
|
||||
|
||||
test('should create main AGENTS.md with routing instructions', () => {
|
||||
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: 'output', description: 'Output', required: true }
|
||||
],
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/with-args/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter['argument-hint']).toBe('<target> <output>');
|
||||
});
|
||||
|
||||
test('should create argument-hint for optional args', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'optional-args',
|
||||
description: 'Command with optional args',
|
||||
userInvokable: true,
|
||||
args: [
|
||||
{ name: 'format', description: 'Format', required: false }
|
||||
],
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/optional-args/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter['argument-hint']).toBe('[FORMAT=<value>]');
|
||||
});
|
||||
|
||||
test('should create argument-hint with mixed required and optional args', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'mixed-args',
|
||||
description: 'Mixed args',
|
||||
userInvokable: true,
|
||||
args: [
|
||||
{ name: 'input', description: 'Input', required: true },
|
||||
{ name: 'format', description: 'Format', required: false },
|
||||
{ name: 'output', description: 'Output', required: true }
|
||||
],
|
||||
body: 'Body'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/mixed-args/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.frontmatter['argument-hint']).toBe('<input> [FORMAT=<value>] <output>');
|
||||
});
|
||||
|
||||
test('should transform {{argname}} to $ARGNAME for user-invokable skills', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'normalize',
|
||||
description: 'Normalize',
|
||||
userInvokable: true,
|
||||
args: [{ name: 'target', description: 'Target', required: false }],
|
||||
body: 'Please normalize {{target}} to match the design system.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/normalize/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.body).toContain('$TARGET');
|
||||
expect(parsed.body).not.toContain('{{target}}');
|
||||
});
|
||||
|
||||
test('should transform multiple different placeholders', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'multi-arg',
|
||||
description: 'Multiple args',
|
||||
userInvokable: true,
|
||||
args: [],
|
||||
body: 'Process {{input}} and output to {{output}} with {{format}}.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/multi-arg/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.body).toContain('$INPUT');
|
||||
expect(parsed.body).toContain('$OUTPUT');
|
||||
expect(parsed.body).toContain('$FORMAT');
|
||||
});
|
||||
|
||||
test('should handle multiple skills', () => {
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Body 1' },
|
||||
{ name: 'skill2', description: 'Skill 2', license: 'Apache', body: 'Body 2' },
|
||||
{ name: 'skill3', description: 'Skill 3', license: 'MIT', body: 'Body 3' }
|
||||
];
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/.codex/skills/skill1/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/.codex/skills/skill2/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/.codex/skills/skill3/SKILL.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should copy reference files', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'frontend-design',
|
||||
description: 'Create distinctive, production-grade frontend interfaces',
|
||||
description: 'Design skill',
|
||||
license: 'MIT',
|
||||
body: 'Frontend design instructions.'
|
||||
},
|
||||
{
|
||||
name: 'backend-api',
|
||||
description: 'Design robust API endpoints',
|
||||
license: 'Apache',
|
||||
body: 'Backend API instructions.'
|
||||
body: 'Design instructions.',
|
||||
references: [
|
||||
{ name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex([], skills, TEST_DIR);
|
||||
|
||||
const agentsMdPath = path.join(TEST_DIR, 'codex/AGENTS.md');
|
||||
expect(fs.existsSync(agentsMdPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(agentsMdPath, 'utf-8');
|
||||
expect(content).toContain('# Codex Agent Instructions');
|
||||
expect(content).toContain('## Available Skills');
|
||||
expect(content).toContain('### frontend-design');
|
||||
expect(content).toContain('**When to use**: Create distinctive, production-grade frontend interfaces');
|
||||
expect(content).toContain('**Read**: `AGENTS.frontend-design.md`');
|
||||
expect(content).toContain('### backend-api');
|
||||
expect(content).toContain('**Read**: `AGENTS.backend-api.md`');
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/.codex/skills/frontend-design/reference/typography.md'))).toBe(true);
|
||||
|
||||
const typoContent = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/frontend-design/reference/typography.md'), 'utf-8');
|
||||
expect(typoContent).toBe('Typography reference');
|
||||
});
|
||||
|
||||
test('should handle multiple commands', () => {
|
||||
const commands = [
|
||||
{ name: 'cmd1', description: 'Command 1', args: [], body: 'Body 1' },
|
||||
{ name: 'cmd2', description: 'Command 2', args: [], body: 'Body 2' },
|
||||
{ name: 'cmd3', description: 'Command 3', args: [], body: 'Body 3' }
|
||||
test('should log correct summary', () => {
|
||||
const consoleMock = mock(() => {});
|
||||
const originalLog = console.log;
|
||||
console.log = consoleMock;
|
||||
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'Test', license: '', userInvokable: true, body: 'body' },
|
||||
{ name: 'skill2', description: 'Test', license: '', userInvokable: false, body: 'body' }
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/prompts/cmd1.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/prompts/cmd2.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/prompts/cmd3.md'))).toBe(true);
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
console.log = originalLog;
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Codex:'));
|
||||
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
|
||||
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable'));
|
||||
});
|
||||
|
||||
test('should clean existing directory before writing', () => {
|
||||
fs.mkdirSync(path.join(TEST_DIR, 'codex/prompts'), { recursive: true });
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'codex/prompts/old.md'), 'old');
|
||||
|
||||
const commands = [{ name: 'new', description: 'New', args: [], body: 'New' }];
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/prompts/old.md'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/prompts/new.md'))).toBe(true);
|
||||
test('should handle empty arrays', () => {
|
||||
transformCodex([], TEST_DIR);
|
||||
|
||||
const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'codex/.codex/skills'));
|
||||
expect(skillDirs).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should handle commands without args', () => {
|
||||
const commands = [
|
||||
test('should handle user-invokable skills without args', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'no-args',
|
||||
description: 'No args command',
|
||||
userInvokable: true,
|
||||
args: [],
|
||||
body: 'Body content'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/no-args.md'), 'utf-8');
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/no-args/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
|
||||
expect(parsed.frontmatter['argument-hint']).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should preserve multiline body', () => {
|
||||
const commands = [
|
||||
const skills = [
|
||||
{
|
||||
name: 'multiline',
|
||||
description: 'Test',
|
||||
args: [],
|
||||
license: '',
|
||||
body: `First line.
|
||||
|
||||
Second line after blank.
|
||||
@@ -260,112 +251,58 @@ Second line after blank.
|
||||
- Bullet 2`
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/multiline.md'), 'utf-8');
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/multiline/SKILL.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
|
||||
expect(parsed.body).toContain('First line.\n\nSecond line');
|
||||
expect(parsed.body).toContain('- Bullet 1\n- Bullet 2');
|
||||
});
|
||||
|
||||
test('should log correct summary', () => {
|
||||
const consoleMock = mock(() => {});
|
||||
const originalLog = console.log;
|
||||
console.log = consoleMock;
|
||||
|
||||
const commands = [{ name: 'cmd1', description: 'Test', args: [], body: 'body' }];
|
||||
const skills = [{ name: 'skill1', description: 'Test', license: '', body: 'body' }];
|
||||
|
||||
transformCodex(commands, skills, TEST_DIR);
|
||||
|
||||
console.log = originalLog;
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledWith('✓ Codex: 1 prompts, 1 skills (modular)');
|
||||
});
|
||||
|
||||
test('should handle empty arrays', () => {
|
||||
transformCodex([], [], TEST_DIR);
|
||||
|
||||
const promptFiles = fs.readdirSync(path.join(TEST_DIR, 'codex/prompts'));
|
||||
expect(promptFiles).toHaveLength(0);
|
||||
|
||||
// Should still create AGENTS.md even with no skills
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex/AGENTS.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle body without placeholders', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'no-placeholders',
|
||||
description: 'No placeholders',
|
||||
args: [],
|
||||
body: 'Just plain text without any placeholders.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/no-placeholders.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.body).toBe('Just plain text without any placeholders.');
|
||||
});
|
||||
|
||||
test('AGENTS.md should have proper structure', () => {
|
||||
test('should support prefix option', () => {
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'First skill', license: '', body: 'body1' }
|
||||
{ name: 'audit', description: 'Audit', license: '', userInvokable: true, body: 'Audit body' }
|
||||
];
|
||||
|
||||
transformCodex([], skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/AGENTS.md'), 'utf-8');
|
||||
|
||||
expect(content).toContain('# Codex Agent Instructions');
|
||||
expect(content).toContain('## Available Skills');
|
||||
expect(content).toContain('## How to Use Skills');
|
||||
expect(content).toContain('### skill1');
|
||||
expect(content).toContain('**When to use**: First skill');
|
||||
expect(content).toContain('**Read**: `AGENTS.skill1.md`');
|
||||
|
||||
transformCodex(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'codex-prefixed/.codex/skills/i-audit/SKILL.md'))).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex-prefixed/.codex/skills/i-audit/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('name: i-audit');
|
||||
});
|
||||
|
||||
test('should handle arg names with hyphens', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'hyphen-arg',
|
||||
description: 'Test',
|
||||
args: [],
|
||||
body: 'Process {{my-input}} and {{output-file}}.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/hyphen-arg.md'), 'utf-8');
|
||||
const parsed = parseFrontmatter(content);
|
||||
|
||||
expect(parsed.body).toContain('$MY-INPUT');
|
||||
expect(parsed.body).toContain('$OUTPUT-FILE');
|
||||
});
|
||||
|
||||
test('should create proper frontmatter structure', () => {
|
||||
const commands = [
|
||||
test('should replace {{model}} placeholder', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
args: [{ name: 'arg1', description: 'Arg 1', required: true }],
|
||||
body: 'Body'
|
||||
description: 'Test',
|
||||
license: '',
|
||||
body: 'Ask {{model}} for help.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/prompts/test.md'), 'utf-8');
|
||||
|
||||
expect(content).toContain('---');
|
||||
expect(content).toContain('description: Test command');
|
||||
expect(content).toContain('argument-hint: <arg1>');
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/test/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('Ask GPT for help.');
|
||||
});
|
||||
|
||||
test('should replace {{config_file}} placeholder', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test',
|
||||
license: '',
|
||||
body: 'See {{config_file}} for more.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCodex(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/test/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('See AGENTS.md for more.');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -19,154 +19,135 @@ describe('transformCursor', () => {
|
||||
});
|
||||
|
||||
test('should create correct directory structure', () => {
|
||||
const commands = [];
|
||||
const skills = [];
|
||||
|
||||
transformCursor(commands, skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/rules'))).toBe(true);
|
||||
|
||||
transformCursor(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should strip frontmatter from commands and output body only', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'test-command',
|
||||
description: 'A test command',
|
||||
args: [{ name: 'target', description: 'Target', required: false }],
|
||||
body: 'This is the command body content.'
|
||||
}
|
||||
];
|
||||
const skills = [];
|
||||
|
||||
transformCursor(commands, skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'cursor/commands/test-command.md');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
expect(content).toBe('This is the command body content.');
|
||||
expect(content).not.toContain('---');
|
||||
expect(content).not.toContain('name:');
|
||||
expect(content).not.toContain('description:');
|
||||
});
|
||||
|
||||
test('should strip frontmatter from skills and output body only', () => {
|
||||
const commands = [];
|
||||
test('should create skill with frontmatter and body', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test-skill',
|
||||
description: 'A test skill',
|
||||
license: 'MIT',
|
||||
body: 'These are the skill instructions.'
|
||||
body: 'Skill instructions here.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCursor(commands, skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'cursor/rules/test-skill.md');
|
||||
|
||||
transformCursor(skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'cursor/.cursor/skills/test-skill/SKILL.md');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
expect(content).toBe('These are the skill instructions.');
|
||||
expect(content).not.toContain('---');
|
||||
expect(content).toContain('---');
|
||||
expect(content).toContain('name: test-skill');
|
||||
expect(content).toContain('description: A test skill');
|
||||
expect(content).toContain('license: MIT');
|
||||
expect(content).toContain('Skill instructions here.');
|
||||
});
|
||||
|
||||
test('should handle skills without license', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'no-license-skill',
|
||||
description: 'Skill without license',
|
||||
license: '',
|
||||
body: 'Body content.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCursor(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.cursor/skills/no-license-skill/SKILL.md'), 'utf-8');
|
||||
expect(content).not.toContain('license:');
|
||||
});
|
||||
|
||||
test('should handle multiple commands', () => {
|
||||
const commands = [
|
||||
{ name: 'cmd1', description: 'Command 1', args: [], body: 'Body 1' },
|
||||
{ name: 'cmd2', description: 'Command 2', args: [], body: 'Body 2' },
|
||||
{ name: 'cmd3', description: 'Command 3', args: [], body: 'Body 3' }
|
||||
];
|
||||
const skills = [];
|
||||
|
||||
transformCursor(commands, skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands/cmd1.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands/cmd2.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands/cmd3.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle multiple skills', () => {
|
||||
const commands = [];
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Skill body 1' },
|
||||
{ name: 'skill2', description: 'Skill 2', license: 'Apache', body: 'Skill body 2' }
|
||||
];
|
||||
|
||||
transformCursor(commands, skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/rules/skill1.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/rules/skill2.md'))).toBe(true);
|
||||
|
||||
transformCursor(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/skill1/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/skill2/SKILL.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should clean existing directory before writing', () => {
|
||||
// Create a pre-existing file
|
||||
fs.mkdirSync(path.join(TEST_DIR, 'cursor/commands'), { recursive: true });
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'cursor/commands/old-file.md'), 'old content');
|
||||
|
||||
const commands = [
|
||||
{ name: 'new-cmd', description: 'New', args: [], body: 'New body' }
|
||||
];
|
||||
|
||||
transformCursor(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands/old-file.md'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands/new-cmd.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle commands with placeholder args in body', () => {
|
||||
const commands = [
|
||||
test('should copy reference files', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'normalize',
|
||||
description: 'Normalize design',
|
||||
args: [{ name: 'target', description: 'Target', required: false }],
|
||||
body: 'Please normalize {{target}} to match the design system.'
|
||||
name: 'frontend-design',
|
||||
description: 'Design skill',
|
||||
license: 'MIT',
|
||||
body: 'Design instructions.',
|
||||
references: [
|
||||
{ name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' },
|
||||
{ name: 'color', content: 'Color reference', filePath: '/fake/path/color.md' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
transformCursor(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/commands/normalize.md'), 'utf-8');
|
||||
// Cursor transformer should preserve the body as-is
|
||||
expect(content).toBe('Please normalize {{target}} to match the design system.');
|
||||
|
||||
transformCursor(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/frontend-design/reference/typography.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/frontend-design/reference/color.md'))).toBe(true);
|
||||
|
||||
const typoContent = fs.readFileSync(path.join(TEST_DIR, 'cursor/.cursor/skills/frontend-design/reference/typography.md'), 'utf-8');
|
||||
expect(typoContent).toBe('Typography reference');
|
||||
});
|
||||
|
||||
test('should handle skills without references', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'simple-skill',
|
||||
description: 'Simple',
|
||||
license: '',
|
||||
body: 'Body',
|
||||
references: []
|
||||
}
|
||||
];
|
||||
|
||||
transformCursor(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/simple-skill/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/simple-skill/reference'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should log correct summary', () => {
|
||||
const consoleMock = mock(() => {});
|
||||
const originalLog = console.log;
|
||||
console.log = consoleMock;
|
||||
|
||||
const commands = [{ name: 'cmd1', description: '', args: [], body: 'body1' }];
|
||||
const skills = [{ name: 'skill1', description: '', license: '', body: 'body1' }];
|
||||
|
||||
transformCursor(commands, skills, TEST_DIR);
|
||||
|
||||
|
||||
const skills = [
|
||||
{ name: 'skill1', description: '', license: '', userInvokable: true, body: 'body1' },
|
||||
{ name: 'skill2', description: '', license: '', userInvokable: false, body: 'body2' }
|
||||
];
|
||||
|
||||
transformCursor(skills, TEST_DIR);
|
||||
|
||||
console.log = originalLog;
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledWith('✓ Cursor: 1 commands, 1 skills (downgraded)');
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Cursor:'));
|
||||
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
|
||||
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable'));
|
||||
});
|
||||
|
||||
test('should handle empty commands and skills arrays', () => {
|
||||
transformCursor([], [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/commands'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/rules'))).toBe(true);
|
||||
|
||||
const commandFiles = fs.readdirSync(path.join(TEST_DIR, 'cursor/commands'));
|
||||
const ruleFiles = fs.readdirSync(path.join(TEST_DIR, 'cursor/rules'));
|
||||
|
||||
expect(commandFiles).toHaveLength(0);
|
||||
expect(ruleFiles).toHaveLength(0);
|
||||
test('should handle empty skills array', () => {
|
||||
transformCursor([], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should preserve line breaks and formatting in body', () => {
|
||||
const commands = [
|
||||
const skills = [
|
||||
{
|
||||
name: 'formatted',
|
||||
description: 'Test',
|
||||
args: [],
|
||||
license: '',
|
||||
body: `Line 1
|
||||
|
||||
Line 3 after blank line
|
||||
@@ -177,12 +158,88 @@ Line 3 after blank line
|
||||
End.`
|
||||
}
|
||||
];
|
||||
|
||||
transformCursor(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/commands/formatted.md'), 'utf-8');
|
||||
|
||||
transformCursor(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.cursor/skills/formatted/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('Line 1\n\nLine 3');
|
||||
expect(content).toContain('- Bullet 1\n- Bullet 2');
|
||||
});
|
||||
});
|
||||
|
||||
test('should support prefix option', () => {
|
||||
const skills = [
|
||||
{ name: 'audit', description: 'Audit', license: '', body: 'Audit body' }
|
||||
];
|
||||
|
||||
transformCursor(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor-prefixed/.cursor/skills/i-audit/SKILL.md'))).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor-prefixed/.cursor/skills/i-audit/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('name: i-audit');
|
||||
});
|
||||
|
||||
test('should replace {{model}} placeholder', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test',
|
||||
license: '',
|
||||
body: 'Ask {{model}} for help.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCursor(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.cursor/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',
|
||||
license: '',
|
||||
body: 'See {{config_file}} for more.'
|
||||
}
|
||||
];
|
||||
|
||||
transformCursor(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.cursor/skills/test/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('See .cursorrules for more.');
|
||||
});
|
||||
|
||||
test('should replace {{ask_instruction}} placeholder', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test',
|
||||
license: '',
|
||||
body: 'When unsure, {{ask_instruction}}'
|
||||
}
|
||||
];
|
||||
|
||||
transformCursor(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.cursor/skills/test/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('When unsure, ask the user directly to clarify what you cannot infer.');
|
||||
});
|
||||
|
||||
test('should clean existing directory before writing', () => {
|
||||
// Create a pre-existing file structure
|
||||
const existingDir = path.join(TEST_DIR, 'cursor/.cursor/skills/old-skill');
|
||||
fs.mkdirSync(existingDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(existingDir, 'SKILL.md'), 'old content');
|
||||
|
||||
const skills = [
|
||||
{ name: 'new-skill', description: 'New', license: '', body: 'New body' }
|
||||
];
|
||||
|
||||
transformCursor(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/old-skill/SKILL.md'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/new-skill/SKILL.md'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,88 +19,14 @@ describe('transformGemini', () => {
|
||||
});
|
||||
|
||||
test('should create correct directory structure', () => {
|
||||
const commands = [];
|
||||
const skills = [];
|
||||
|
||||
transformGemini(commands, skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/commands'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini'))).toBe(true);
|
||||
|
||||
transformGemini(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/.gemini/skills'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should convert command to TOML format', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'test-command',
|
||||
description: 'A test command',
|
||||
args: [],
|
||||
body: 'Command body content.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'gemini/commands/test-command.toml');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
expect(content).toContain('description = "A test command"');
|
||||
expect(content).toContain('prompt = """');
|
||||
expect(content).toContain('Command body content.');
|
||||
expect(content).toContain('"""');
|
||||
});
|
||||
|
||||
test('should replace {{argname}} placeholders with {{args}}', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'normalize',
|
||||
description: 'Normalize design',
|
||||
args: [{ name: 'target', description: 'Target', required: false }],
|
||||
body: 'Please normalize {{target}} to match the design system.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/commands/normalize.toml'), 'utf-8');
|
||||
expect(content).toContain('{{args}}');
|
||||
expect(content).not.toContain('{{target}}');
|
||||
});
|
||||
|
||||
test('should replace multiple different placeholders with {{args}}', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'multi-arg',
|
||||
description: 'Multiple args',
|
||||
args: [],
|
||||
body: 'Process {{input}} and output to {{output}} with {{format}}.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/commands/multi-arg.toml'), 'utf-8');
|
||||
const argsMatches = content.match(/\{\{args\}\}/g);
|
||||
expect(argsMatches).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('should escape quotes in description', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'with-quotes',
|
||||
description: 'A command with "quotes" in description',
|
||||
args: [],
|
||||
body: 'Body content.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/commands/with-quotes.toml'), 'utf-8');
|
||||
expect(content).toContain('description = "A command with \\"quotes\\" in description"');
|
||||
});
|
||||
|
||||
test('should create modular skill files', () => {
|
||||
test('should create skill files with frontmatter and body in .gemini/skills/ directory', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test-skill',
|
||||
@@ -109,170 +35,176 @@ describe('transformGemini', () => {
|
||||
body: 'Skill instructions here.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini([], skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'gemini/GEMINI.test-skill.md');
|
||||
|
||||
transformGemini(skills, TEST_DIR);
|
||||
|
||||
const outputPath = path.join(TEST_DIR, 'gemini/.gemini/skills/test-skill/SKILL.md');
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
expect(content).toBe('Skill instructions here.');
|
||||
expect(content).toContain('---');
|
||||
expect(content).toContain('name: test-skill');
|
||||
expect(content).toContain('description: A test skill');
|
||||
expect(content).toContain('Skill instructions here.');
|
||||
});
|
||||
|
||||
test('should create main GEMINI.md with imports', () => {
|
||||
test('should handle multiple skills', () => {
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Body 1' },
|
||||
{ name: 'skill2', description: 'Skill 2', license: 'Apache', body: 'Body 2' },
|
||||
{ name: 'skill3', description: 'Skill 3', license: 'MIT', body: 'Body 3' }
|
||||
];
|
||||
|
||||
transformGemini(skills, TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/.gemini/skills/skill1/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/.gemini/skills/skill2/SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/.gemini/skills/skill3/SKILL.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle user-invokable skills with args', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'normalize',
|
||||
description: 'Normalize design',
|
||||
userInvokable: true,
|
||||
args: [{ name: 'target', description: 'Target', required: false }],
|
||||
body: 'Please normalize {{target}} to match the design system.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/normalize/SKILL.md'), 'utf-8');
|
||||
// For user-invokable skills, {{arg}} placeholders become {{args}}
|
||||
expect(content).toContain('{{args}}');
|
||||
expect(content).not.toContain('{{target}}');
|
||||
});
|
||||
|
||||
test('should replace multiple different placeholders with {{args}} for user-invokable skills', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'multi-arg',
|
||||
description: 'Multiple args',
|
||||
userInvokable: true,
|
||||
args: [],
|
||||
body: 'Process {{input}} and output to {{output}} with {{format}}.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/multi-arg/SKILL.md'), 'utf-8');
|
||||
const argsMatches = content.match(/\{\{args\}\}/g);
|
||||
expect(argsMatches).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('should not replace placeholders for non-user-invokable skills', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'passive-skill',
|
||||
description: 'Passive skill',
|
||||
userInvokable: false,
|
||||
body: 'Process {{target}} normally.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/passive-skill/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('{{target}}');
|
||||
expect(content).not.toContain('{{args}}');
|
||||
});
|
||||
|
||||
test('should copy reference files', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'frontend-design',
|
||||
description: 'Create distinctive, production-grade frontend interfaces',
|
||||
description: 'Design skill',
|
||||
license: 'MIT',
|
||||
body: 'Frontend design instructions.'
|
||||
},
|
||||
{
|
||||
name: 'backend-api',
|
||||
description: 'Design robust API endpoints',
|
||||
license: 'Apache',
|
||||
body: 'Backend API instructions.'
|
||||
body: 'Design instructions.',
|
||||
references: [
|
||||
{ name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini([], skills, TEST_DIR);
|
||||
|
||||
const geminiMdPath = path.join(TEST_DIR, 'gemini/GEMINI.md');
|
||||
expect(fs.existsSync(geminiMdPath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(geminiMdPath, 'utf-8');
|
||||
expect(content).toContain('# Gemini Context');
|
||||
expect(content).toContain('frontend-design');
|
||||
expect(content).toContain('Create distinctive, production-grade frontend interfaces');
|
||||
expect(content).toContain('@./GEMINI.frontend-design.md');
|
||||
expect(content).toContain('backend-api');
|
||||
expect(content).toContain('@./GEMINI.backend-api.md');
|
||||
});
|
||||
|
||||
test('should handle multiple commands', () => {
|
||||
const commands = [
|
||||
{ name: 'cmd1', description: 'Command 1', args: [], body: 'Body 1' },
|
||||
{ name: 'cmd2', description: 'Command 2', args: [], body: 'Body 2' },
|
||||
{ name: 'cmd3', description: 'Command 3', args: [], body: 'Body 3' }
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/commands/cmd1.toml'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/commands/cmd2.toml'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/commands/cmd3.toml'))).toBe(true);
|
||||
});
|
||||
transformGemini(skills, TEST_DIR);
|
||||
|
||||
test('should clean existing directory before writing', () => {
|
||||
fs.mkdirSync(path.join(TEST_DIR, 'gemini/commands'), { recursive: true });
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'gemini/commands/old.toml'), 'old');
|
||||
|
||||
const commands = [{ name: 'new', description: 'New', args: [], body: 'New' }];
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/commands/old.toml'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/commands/new.toml'))).toBe(true);
|
||||
});
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/.gemini/skills/frontend-design/reference/typography.md'))).toBe(true);
|
||||
|
||||
test('should preserve multiline body in TOML triple-quoted strings', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'multiline',
|
||||
description: 'Test',
|
||||
args: [],
|
||||
body: `First line.
|
||||
|
||||
Second line after blank.
|
||||
|
||||
- Bullet 1
|
||||
- Bullet 2`
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/commands/multiline.toml'), 'utf-8');
|
||||
expect(content).toContain('First line.\n\nSecond line');
|
||||
expect(content).toContain('- Bullet 1\n- Bullet 2');
|
||||
const typoContent = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/frontend-design/reference/typography.md'), 'utf-8');
|
||||
expect(typoContent).toBe('Typography reference');
|
||||
});
|
||||
|
||||
test('should log correct summary', () => {
|
||||
const consoleMock = mock(() => {});
|
||||
const originalLog = console.log;
|
||||
console.log = consoleMock;
|
||||
|
||||
const commands = [{ name: 'cmd1', description: 'Test', args: [], body: 'body' }];
|
||||
const skills = [{ name: 'skill1', description: 'Test', license: '', body: 'body' }];
|
||||
|
||||
transformGemini(commands, skills, TEST_DIR);
|
||||
|
||||
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'Test', license: '', userInvokable: true, body: 'body' },
|
||||
{ name: 'skill2', description: 'Test', license: '', userInvokable: false, body: 'body' }
|
||||
];
|
||||
|
||||
transformGemini(skills, TEST_DIR);
|
||||
|
||||
console.log = originalLog;
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledWith('✓ Gemini: 1 commands (TOML), 1 skills (modular)');
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Gemini:'));
|
||||
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
|
||||
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable'));
|
||||
});
|
||||
|
||||
test('should handle empty arrays', () => {
|
||||
transformGemini([], [], TEST_DIR);
|
||||
|
||||
const commandFiles = fs.readdirSync(path.join(TEST_DIR, 'gemini/commands'));
|
||||
expect(commandFiles).toHaveLength(0);
|
||||
|
||||
// Should still create GEMINI.md even with no skills
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/GEMINI.md'))).toBe(true);
|
||||
transformGemini([], TEST_DIR);
|
||||
|
||||
const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'gemini/.gemini/skills'));
|
||||
expect(skillDirs).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should create proper TOML structure', () => {
|
||||
const commands = [
|
||||
test('should support prefix option', () => {
|
||||
const skills = [
|
||||
{ name: 'audit', description: 'Audit', license: '', body: 'Audit body' }
|
||||
];
|
||||
|
||||
transformGemini(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
|
||||
|
||||
expect(fs.existsSync(path.join(TEST_DIR, 'gemini-prefixed/.gemini/skills/i-audit/SKILL.md'))).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini-prefixed/.gemini/skills/i-audit/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('name: i-audit');
|
||||
});
|
||||
|
||||
test('should replace {{model}} placeholder', () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
args: [],
|
||||
body: 'Test body'
|
||||
description: 'Test',
|
||||
license: '',
|
||||
body: 'Ask {{model}} for help.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/commands/test.toml'), 'utf-8');
|
||||
|
||||
// Check for proper TOML structure
|
||||
const lines = content.split('\n');
|
||||
expect(lines[0]).toMatch(/^description = /);
|
||||
expect(lines[1]).toBe('prompt = """');
|
||||
expect(lines[lines.length - 1]).toBe('"""');
|
||||
|
||||
transformGemini(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/test/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('Ask Gemini for help.');
|
||||
});
|
||||
|
||||
test('should handle body without placeholders', () => {
|
||||
const commands = [
|
||||
{
|
||||
name: 'no-placeholders',
|
||||
description: 'No args',
|
||||
args: [],
|
||||
body: 'Just plain text without any placeholders.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini(commands, [], TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/commands/no-placeholders.toml'), 'utf-8');
|
||||
expect(content).toContain('Just plain text without any placeholders.');
|
||||
expect(content).not.toContain('{{args}}');
|
||||
});
|
||||
|
||||
test('GEMINI.md should have proper structure', () => {
|
||||
test('should replace {{config_file}} placeholder', () => {
|
||||
const skills = [
|
||||
{ name: 'skill1', description: 'First skill', license: '', body: 'body1' }
|
||||
{
|
||||
name: 'test',
|
||||
description: 'Test',
|
||||
license: '',
|
||||
body: 'See {{config_file}} for more.'
|
||||
}
|
||||
];
|
||||
|
||||
transformGemini([], skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/GEMINI.md'), 'utf-8');
|
||||
|
||||
expect(content).toContain('# Gemini Context');
|
||||
expect(content).toContain('## Available Skills');
|
||||
expect(content).toContain('## How Skills Work');
|
||||
expect(content).toContain('### skill1');
|
||||
expect(content).toContain('**When to use**: First skill');
|
||||
|
||||
transformGemini(skills, TEST_DIR);
|
||||
|
||||
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/test/SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('See GEMINI.md for more.');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+299
-76
@@ -1,14 +1,15 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {
|
||||
parseFrontmatter,
|
||||
readFilesRecursive,
|
||||
import {
|
||||
parseFrontmatter,
|
||||
readFilesRecursive,
|
||||
readSourceFiles,
|
||||
ensureDir,
|
||||
cleanDir,
|
||||
writeFile,
|
||||
generateYamlFrontmatter
|
||||
generateYamlFrontmatter,
|
||||
readPatterns
|
||||
} from '../../scripts/lib/utils.js';
|
||||
|
||||
// Temporary test directory
|
||||
@@ -17,22 +18,22 @@ 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-command
|
||||
description: A test command
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
---
|
||||
|
||||
This is the body content.`;
|
||||
|
||||
const result = parseFrontmatter(content);
|
||||
expect(result.frontmatter.name).toBe('test-command');
|
||||
expect(result.frontmatter.description).toBe('A test command');
|
||||
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-command
|
||||
description: A test command
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
args:
|
||||
- name: target
|
||||
description: The target to normalize
|
||||
@@ -45,7 +46,7 @@ args:
|
||||
Body here.`;
|
||||
|
||||
const result = parseFrontmatter(content);
|
||||
expect(result.frontmatter.name).toBe('test-command');
|
||||
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');
|
||||
@@ -57,7 +58,7 @@ Body here.`;
|
||||
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);
|
||||
});
|
||||
@@ -68,7 +69,7 @@ name: test
|
||||
---
|
||||
`;
|
||||
const result = parseFrontmatter(content);
|
||||
|
||||
|
||||
expect(result.frontmatter.name).toBe('test');
|
||||
expect(result.body).toBe('');
|
||||
});
|
||||
@@ -85,25 +86,62 @@ 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-command',
|
||||
name: 'test-skill',
|
||||
description: 'A test'
|
||||
};
|
||||
|
||||
const result = generateYamlFrontmatter(data);
|
||||
expect(result).toContain('---');
|
||||
expect(result).toContain('name: test-command');
|
||||
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 command',
|
||||
description: 'Test skill',
|
||||
args: [
|
||||
{ name: 'target', description: 'The target', required: false },
|
||||
{ name: 'output', description: 'Output format', required: true }
|
||||
@@ -118,6 +156,17 @@ describe('generateYamlFrontmatter', () => {
|
||||
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',
|
||||
@@ -148,7 +197,7 @@ describe('ensureDir', () => {
|
||||
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);
|
||||
});
|
||||
@@ -156,14 +205,14 @@ describe('ensureDir', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -182,9 +231,9 @@ describe('cleanDir', () => {
|
||||
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);
|
||||
});
|
||||
@@ -198,7 +247,7 @@ describe('cleanDir', () => {
|
||||
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);
|
||||
});
|
||||
@@ -214,9 +263,9 @@ describe('writeFile', () => {
|
||||
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);
|
||||
});
|
||||
@@ -224,7 +273,7 @@ describe('writeFile', () => {
|
||||
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');
|
||||
});
|
||||
@@ -233,7 +282,7 @@ describe('writeFile', () => {
|
||||
const filePath = path.join(TEST_DIR, 'file.txt');
|
||||
writeFile(filePath, 'first');
|
||||
writeFile(filePath, 'second');
|
||||
|
||||
|
||||
expect(fs.readFileSync(filePath, 'utf-8')).toBe('second');
|
||||
});
|
||||
});
|
||||
@@ -253,7 +302,7 @@ describe('readFilesRecursive', () => {
|
||||
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);
|
||||
@@ -264,7 +313,7 @@ describe('readFilesRecursive', () => {
|
||||
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);
|
||||
@@ -280,7 +329,7 @@ describe('readFilesRecursive', () => {
|
||||
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([]);
|
||||
});
|
||||
@@ -299,30 +348,7 @@ describe('readSourceFiles', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('should read and parse command files', () => {
|
||||
const commandContent = `---
|
||||
name: test-command
|
||||
description: A test command
|
||||
args:
|
||||
- name: target
|
||||
description: Target arg
|
||||
required: false
|
||||
---
|
||||
|
||||
Command body content.`;
|
||||
|
||||
writeFile(path.join(testRootDir, 'source/commands/test-command.md'), commandContent);
|
||||
|
||||
const { commands, skills } = readSourceFiles(testRootDir);
|
||||
|
||||
expect(commands).toHaveLength(1);
|
||||
expect(commands[0].name).toBe('test-command');
|
||||
expect(commands[0].description).toBe('A test command');
|
||||
expect(commands[0].args).toHaveLength(1);
|
||||
expect(commands[0].body).toBe('Command body content.');
|
||||
});
|
||||
|
||||
test('should read and parse skill files', () => {
|
||||
test('should read and parse skill files from directory-based structure', () => {
|
||||
const skillContent = `---
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
@@ -331,10 +357,12 @@ license: MIT
|
||||
|
||||
Skill instructions here.`;
|
||||
|
||||
writeFile(path.join(testRootDir, 'source/skills/test-skill.md'), skillContent);
|
||||
|
||||
const { commands, skills } = readSourceFiles(testRootDir);
|
||||
|
||||
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');
|
||||
@@ -342,34 +370,229 @@ Skill instructions here.`;
|
||||
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', () => {
|
||||
writeFile(path.join(testRootDir, 'source/commands/my-command.md'), 'Just body, no frontmatter.');
|
||||
|
||||
const { commands } = readSourceFiles(testRootDir);
|
||||
|
||||
expect(commands[0].name).toBe('my-command');
|
||||
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/commands'));
|
||||
ensureDir(path.join(testRootDir, 'source/skills'));
|
||||
|
||||
const { commands, skills } = readSourceFiles(testRootDir);
|
||||
|
||||
expect(commands).toEqual([]);
|
||||
|
||||
const { skills } = readSourceFiles(testRootDir);
|
||||
|
||||
expect(skills).toEqual([]);
|
||||
});
|
||||
|
||||
test('should read multiple commands and skills', () => {
|
||||
writeFile(path.join(testRootDir, 'source/commands/cmd1.md'), '---\nname: cmd1\n---\nBody1');
|
||||
writeFile(path.join(testRootDir, 'source/commands/cmd2.md'), '---\nname: cmd2\n---\nBody2');
|
||||
writeFile(path.join(testRootDir, 'source/skills/skill1.md'), '---\nname: skill1\n---\nSkill1');
|
||||
writeFile(path.join(testRootDir, 'source/skills/skill2.md'), '---\nname: skill2\n---\nSkill2');
|
||||
|
||||
const { commands, skills } = readSourceFiles(testRootDir);
|
||||
|
||||
expect(commands).toHaveLength(2);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user