Merge pull request #16 from hobostay/fix-tests

Fix broken tests to match actual implementation
This commit is contained in:
Paul Bakaus
2026-03-10 10:51:25 -07:00
committed by GitHub
6 changed files with 1187 additions and 918 deletions
+125 -109
View File
@@ -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);
});
});
+230 -126
View File
@@ -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.');
});
});
+220 -283
View File
@@ -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.');
});
});
+170 -113
View File
@@ -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);
});
});
+143 -211
View File
@@ -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
View File
@@ -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);
});
});