build.test.js

  1import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test';
  2import fs from 'fs';
  3import path from 'path';
  4import * as utils from '../scripts/lib/utils.js';
  5import * as transformers from '../scripts/lib/transformers/index.js';
  6
  7const TEST_DIR = path.join(process.cwd(), 'test-tmp-build');
  8
  9describe('build orchestration', () => {
 10  beforeEach(() => {
 11    if (fs.existsSync(TEST_DIR)) {
 12      fs.rmSync(TEST_DIR, { recursive: true, force: true });
 13    }
 14    fs.mkdirSync(TEST_DIR, { recursive: true });
 15  });
 16
 17  afterEach(() => {
 18    if (fs.existsSync(TEST_DIR)) {
 19      fs.rmSync(TEST_DIR, { recursive: true, force: true });
 20    }
 21  });
 22
 23  test('should call readSourceFiles with root directory', () => {
 24    const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
 25      skills: []
 26    });
 27
 28    const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {});
 29    const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {});
 30    const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {});
 31    const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {});
 32
 33    // Simulate the build process
 34    const ROOT_DIR = TEST_DIR;
 35    const DIST_DIR = path.join(ROOT_DIR, 'dist');
 36
 37    const { skills } = utils.readSourceFiles(ROOT_DIR);
 38    const patterns = utils.readPatterns(ROOT_DIR);
 39    transformers.transformCursor(skills, DIST_DIR, patterns);
 40    transformers.transformClaudeCode(skills, DIST_DIR, patterns);
 41    transformers.transformGemini(skills, DIST_DIR, patterns);
 42    transformers.transformCodex(skills, DIST_DIR, patterns);
 43
 44    expect(readSourceFilesSpy).toHaveBeenCalledWith(ROOT_DIR);
 45
 46    readSourceFilesSpy.mockRestore();
 47    transformCursorSpy.mockRestore();
 48    transformClaudeCodeSpy.mockRestore();
 49    transformGeminiSpy.mockRestore();
 50    transformCodexSpy.mockRestore();
 51  });
 52
 53  test('should call all transformers with correct arguments', () => {
 54    const skills = [
 55      { name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Skill body 1' }
 56    ];
 57    const patterns = { patterns: [], antipatterns: [] };
 58
 59    const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
 60      skills
 61    });
 62    const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue(patterns);
 63
 64    const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {});
 65    const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {});
 66    const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {});
 67    const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {});
 68
 69    const ROOT_DIR = TEST_DIR;
 70    const DIST_DIR = path.join(ROOT_DIR, 'dist');
 71
 72    const sourceFiles = utils.readSourceFiles(ROOT_DIR);
 73    const patternData = utils.readPatterns(ROOT_DIR);
 74    transformers.transformCursor(sourceFiles.skills, DIST_DIR, patternData);
 75    transformers.transformClaudeCode(sourceFiles.skills, DIST_DIR, patternData);
 76    transformers.transformGemini(sourceFiles.skills, DIST_DIR, patternData);
 77    transformers.transformCodex(sourceFiles.skills, DIST_DIR, patternData);
 78
 79    expect(transformCursorSpy).toHaveBeenCalledWith(skills, DIST_DIR, patterns);
 80    expect(transformClaudeCodeSpy).toHaveBeenCalledWith(skills, DIST_DIR, patterns);
 81    expect(transformGeminiSpy).toHaveBeenCalledWith(skills, DIST_DIR, patterns);
 82    expect(transformCodexSpy).toHaveBeenCalledWith(skills, DIST_DIR, patterns);
 83
 84    readSourceFilesSpy.mockRestore();
 85    readPatternsSpy.mockRestore();
 86    transformCursorSpy.mockRestore();
 87    transformClaudeCodeSpy.mockRestore();
 88    transformGeminiSpy.mockRestore();
 89    transformCodexSpy.mockRestore();
 90  });
 91
 92  test('should handle empty source files', () => {
 93    const patterns = { patterns: [], antipatterns: [] };
 94
 95    const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
 96      skills: []
 97    });
 98    const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue(patterns);
 99
100    const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {});
101    const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {});
102    const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {});
103    const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {});
104
105    const ROOT_DIR = TEST_DIR;
106    const DIST_DIR = path.join(ROOT_DIR, 'dist');
107
108    const { skills } = utils.readSourceFiles(ROOT_DIR);
109    const patternData = utils.readPatterns(ROOT_DIR);
110    transformers.transformCursor(skills, DIST_DIR, patternData);
111    transformers.transformClaudeCode(skills, DIST_DIR, patternData);
112    transformers.transformGemini(skills, DIST_DIR, patternData);
113    transformers.transformCodex(skills, DIST_DIR, patternData);
114
115    expect(transformCursorSpy).toHaveBeenCalledWith([], DIST_DIR, patterns);
116    expect(transformClaudeCodeSpy).toHaveBeenCalledWith([], DIST_DIR, patterns);
117    expect(transformGeminiSpy).toHaveBeenCalledWith([], DIST_DIR, patterns);
118    expect(transformCodexSpy).toHaveBeenCalledWith([], DIST_DIR, patterns);
119
120    readSourceFilesSpy.mockRestore();
121    readPatternsSpy.mockRestore();
122    transformCursorSpy.mockRestore();
123    transformClaudeCodeSpy.mockRestore();
124    transformGeminiSpy.mockRestore();
125    transformCodexSpy.mockRestore();
126  });
127
128  test('integration: full build creates all expected outputs', () => {
129    // Create test source files
130    const skillContent = `---
131name: test-skill
132description: A test skill
133license: MIT
134---
135
136This is a test skill body.`;
137
138    const skillDir = path.join(TEST_DIR, 'source/skills/test-skill');
139    fs.mkdirSync(skillDir, { recursive: true });
140    fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
141
142    // Run the build process
143    const DIST_DIR = path.join(TEST_DIR, 'dist');
144    const { skills } = utils.readSourceFiles(TEST_DIR);
145    const patterns = utils.readPatterns(TEST_DIR);
146
147    transformers.transformCursor(skills, DIST_DIR, patterns);
148    transformers.transformClaudeCode(skills, DIST_DIR, patterns);
149    transformers.transformGemini(skills, DIST_DIR, patterns);
150    transformers.transformCodex(skills, DIST_DIR, patterns);
151
152    // Verify Cursor outputs
153    expect(fs.existsSync(path.join(DIST_DIR, 'cursor/.cursor/skills/test-skill/SKILL.md'))).toBe(true);
154
155    // Verify Claude Code outputs
156    expect(fs.existsSync(path.join(DIST_DIR, 'claude-code/.claude/skills/test-skill/SKILL.md'))).toBe(true);
157
158    // Verify Gemini outputs
159    expect(fs.existsSync(path.join(DIST_DIR, 'gemini/.gemini/skills/test-skill/SKILL.md'))).toBe(true);
160
161    // Verify Codex outputs
162    expect(fs.existsSync(path.join(DIST_DIR, 'codex/.codex/skills/test-skill/SKILL.md'))).toBe(true);
163  });
164
165  test('integration: verify transformations are correct', () => {
166    const skillContent = `---
167name: audit
168description: Run technical quality checks
169user-invocable: true
170argument-hint: "[TARGET=<value>]"
171---
172
173Please audit {{target}} for technical quality. Ask {{model}} for help.`;
174
175    const skillDir = path.join(TEST_DIR, 'source/skills/audit');
176    fs.mkdirSync(skillDir, { recursive: true });
177    fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
178
179    const DIST_DIR = path.join(TEST_DIR, 'dist');
180    const { skills } = utils.readSourceFiles(TEST_DIR);
181    const patterns = utils.readPatterns(TEST_DIR);
182
183    transformers.transformCursor(skills, DIST_DIR, patterns);
184    transformers.transformClaudeCode(skills, DIST_DIR, patterns);
185    transformers.transformGemini(skills, DIST_DIR, patterns);
186    transformers.transformCodex(skills, DIST_DIR, patterns);
187
188    // Verify Cursor: full frontmatter with user-invocable
189    const cursorContent = fs.readFileSync(path.join(DIST_DIR, 'cursor/.cursor/skills/audit/SKILL.md'), 'utf-8');
190    expect(cursorContent).toContain('---');
191    expect(cursorContent).toContain('name: audit');
192    expect(cursorContent).toContain('{{target}}');
193    expect(cursorContent).toContain('the model');
194
195    // Verify Claude Code: full frontmatter with user-invocable and argument-hint
196    const claudeContent = fs.readFileSync(path.join(DIST_DIR, 'claude-code/.claude/skills/audit/SKILL.md'), 'utf-8');
197    expect(claudeContent).toContain('---');
198    expect(claudeContent).toContain('name: audit');
199    expect(claudeContent).toContain('user-invocable: true');
200    expect(claudeContent).toContain('{{target}}');
201    expect(claudeContent).toContain('Claude');
202
203    // Verify Gemini: skill in skills directory
204    expect(fs.existsSync(path.join(DIST_DIR, 'gemini/.gemini/skills/audit/SKILL.md'))).toBe(true);
205    const geminiContent = fs.readFileSync(path.join(DIST_DIR, 'gemini/.gemini/skills/audit/SKILL.md'), 'utf-8');
206    expect(geminiContent).toContain('{{target}}'); // No body transform, placeholder preserved
207    expect(geminiContent).toContain('Gemini');
208
209    // Verify Codex: skill in skills directory
210    expect(fs.existsSync(path.join(DIST_DIR, 'codex/.codex/skills/audit/SKILL.md'))).toBe(true);
211    const codexContent = fs.readFileSync(path.join(DIST_DIR, 'codex/.codex/skills/audit/SKILL.md'), 'utf-8');
212    expect(codexContent).toContain('{{target}}'); // No body transform, placeholder preserved
213    expect(codexContent).toContain('GPT');
214  });
215
216  test('integration: multiple skills', () => {
217    const skill1Dir = path.join(TEST_DIR, 'source/skills/skill1');
218    fs.mkdirSync(skill1Dir, { recursive: true });
219    fs.writeFileSync(path.join(skill1Dir, 'SKILL.md'), '---\nname: skill1\n---\nSkill1');
220
221    const skill2Dir = path.join(TEST_DIR, 'source/skills/skill2');
222    fs.mkdirSync(skill2Dir, { recursive: true });
223    fs.writeFileSync(path.join(skill2Dir, 'SKILL.md'), '---\nname: skill2\n---\nSkill2');
224
225    const DIST_DIR = path.join(TEST_DIR, 'dist');
226    const { skills } = utils.readSourceFiles(TEST_DIR);
227    const patterns = utils.readPatterns(TEST_DIR);
228
229    expect(skills).toHaveLength(2);
230
231    transformers.transformCursor(skills, DIST_DIR, patterns);
232    transformers.transformClaudeCode(skills, DIST_DIR, patterns);
233    transformers.transformGemini(skills, DIST_DIR, patterns);
234    transformers.transformCodex(skills, DIST_DIR, patterns);
235
236    // Verify all files exist
237    expect(fs.existsSync(path.join(DIST_DIR, 'cursor/.cursor/skills/skill1/SKILL.md'))).toBe(true);
238    expect(fs.existsSync(path.join(DIST_DIR, 'cursor/.cursor/skills/skill2/SKILL.md'))).toBe(true);
239    expect(fs.existsSync(path.join(DIST_DIR, 'claude-code/.claude/skills/skill1/SKILL.md'))).toBe(true);
240    expect(fs.existsSync(path.join(DIST_DIR, 'claude-code/.claude/skills/skill2/SKILL.md'))).toBe(true);
241  });
242
243  test('should call transformers in correct order', () => {
244    const callOrder = [];
245
246    const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
247      skills: []
248    });
249    const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue({ patterns: [], antipatterns: [] });
250
251    const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {
252      callOrder.push('cursor');
253    });
254    const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {
255      callOrder.push('claude-code');
256    });
257    const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {
258      callOrder.push('gemini');
259    });
260    const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {
261      callOrder.push('codex');
262    });
263
264    const ROOT_DIR = TEST_DIR;
265    const DIST_DIR = path.join(ROOT_DIR, 'dist');
266
267    const { skills } = utils.readSourceFiles(ROOT_DIR);
268    const patterns = utils.readPatterns(ROOT_DIR);
269    transformers.transformCursor(skills, DIST_DIR, patterns);
270    transformers.transformClaudeCode(skills, DIST_DIR, patterns);
271    transformers.transformGemini(skills, DIST_DIR, patterns);
272    transformers.transformCodex(skills, DIST_DIR, patterns);
273
274    expect(callOrder).toEqual(['cursor', 'claude-code', 'gemini', 'codex']);
275
276    readSourceFilesSpy.mockRestore();
277    readPatternsSpy.mockRestore();
278    transformCursorSpy.mockRestore();
279    transformClaudeCodeSpy.mockRestore();
280    transformGeminiSpy.mockRestore();
281    transformCodexSpy.mockRestore();
282  });
283
284  test('should include agents and kiro transformers', () => {
285    const { skills } = utils.readSourceFiles(TEST_DIR);
286    const patterns = utils.readPatterns(TEST_DIR);
287    const DIST_DIR = path.join(TEST_DIR, 'dist');
288
289    // These should not throw
290    transformers.transformAgents(skills, DIST_DIR, patterns);
291    transformers.transformKiro(skills, DIST_DIR, patterns);
292
293    // Verify outputs
294    expect(fs.existsSync(path.join(DIST_DIR, 'agents/.agents/skills'))).toBe(true);
295    expect(fs.existsSync(path.join(DIST_DIR, 'kiro/.kiro/skills'))).toBe(true);
296  });
297});