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, '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: emits native subagent files for Codex and Claude Code', () => {
166    const skillContent = `---
167name: test-skill
168description: A test skill
169---
170
171This is a test skill body.`;
172
173    const agentContent = `---
174name: asset-producer
175codex-name: asset_producer
176description: Produces assets from approved crops
177tools: Read, Write
178model: inherit
179effort: medium
180max-turns: 8
181nickname-candidates:
182  - Asset Plate
183---
184
185Do not redesign the approved crop.`;
186
187    const skillDir = path.join(TEST_DIR, 'skill');
188    fs.mkdirSync(path.join(skillDir, 'agents'), { recursive: true });
189    fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
190    fs.writeFileSync(path.join(skillDir, 'agents/asset-producer.md'), agentContent);
191
192    const DIST_DIR = path.join(TEST_DIR, 'dist');
193    const { skills } = utils.readSourceFiles(TEST_DIR);
194    const patterns = utils.readPatterns(TEST_DIR);
195
196    transformers.transformClaudeCode(skills, DIST_DIR, patterns);
197    transformers.transformCodex(skills, DIST_DIR, patterns);
198
199    const claudeAgentPath = path.join(DIST_DIR, 'claude-code/.claude/agents/asset-producer.md');
200    const codexAgentPath = path.join(DIST_DIR, 'codex/.codex/agents/asset_producer.toml');
201
202    expect(fs.existsSync(claudeAgentPath)).toBe(true);
203    expect(fs.existsSync(codexAgentPath)).toBe(true);
204
205    const claudeAgent = fs.readFileSync(claudeAgentPath, 'utf-8');
206    expect(claudeAgent).toContain('name: asset-producer');
207    expect(claudeAgent).toContain('tools: Read, Write');
208    expect(claudeAgent).toContain('maxTurns: 8');
209
210    const codexAgent = fs.readFileSync(codexAgentPath, 'utf-8');
211    expect(codexAgent).toContain('name = "asset_producer"');
212    expect(codexAgent).toContain('model_reasoning_effort = "medium"');
213    expect(codexAgent).toContain('nickname_candidates = ["Asset Plate"]');
214    expect(codexAgent).toContain('developer_instructions =');
215  });
216
217  test('integration: verify transformations are correct', () => {
218    const skillContent = `---
219name: audit
220description: Run technical quality checks
221user-invocable: true
222argument-hint: "[TARGET=<value>]"
223---
224
225Please audit {{target}} for technical quality. Ask {{model}} for help.`;
226
227    const skillDir = path.join(TEST_DIR, 'skill');
228    fs.mkdirSync(skillDir, { recursive: true });
229    fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
230
231    const DIST_DIR = path.join(TEST_DIR, 'dist');
232    const { skills } = utils.readSourceFiles(TEST_DIR);
233    const patterns = utils.readPatterns(TEST_DIR);
234
235    transformers.transformCursor(skills, DIST_DIR, patterns);
236    transformers.transformClaudeCode(skills, DIST_DIR, patterns);
237    transformers.transformGemini(skills, DIST_DIR, patterns);
238    transformers.transformCodex(skills, DIST_DIR, patterns);
239
240    // Verify Cursor: full frontmatter with user-invocable
241    const cursorContent = fs.readFileSync(path.join(DIST_DIR, 'cursor/.cursor/skills/audit/SKILL.md'), 'utf-8');
242    expect(cursorContent).toContain('---');
243    expect(cursorContent).toContain('name: audit');
244    expect(cursorContent).toContain('{{target}}');
245    expect(cursorContent).toContain('the model');
246
247    // Verify Claude Code: full frontmatter with user-invocable and argument-hint
248    const claudeContent = fs.readFileSync(path.join(DIST_DIR, 'claude-code/.claude/skills/audit/SKILL.md'), 'utf-8');
249    expect(claudeContent).toContain('---');
250    expect(claudeContent).toContain('name: audit');
251    expect(claudeContent).toContain('user-invocable: true');
252    expect(claudeContent).toContain('{{target}}');
253    expect(claudeContent).toContain('Claude');
254
255    // Verify Gemini: skill in skills directory
256    expect(fs.existsSync(path.join(DIST_DIR, 'gemini/.gemini/skills/audit/SKILL.md'))).toBe(true);
257    const geminiContent = fs.readFileSync(path.join(DIST_DIR, 'gemini/.gemini/skills/audit/SKILL.md'), 'utf-8');
258    expect(geminiContent).toContain('{{target}}'); // No body transform, placeholder preserved
259    expect(geminiContent).toContain('Gemini');
260
261    // Verify Codex: skill in skills directory
262    expect(fs.existsSync(path.join(DIST_DIR, 'codex/.codex/skills/audit/SKILL.md'))).toBe(true);
263    const codexContent = fs.readFileSync(path.join(DIST_DIR, 'codex/.codex/skills/audit/SKILL.md'), 'utf-8');
264    expect(codexContent).toContain('{{target}}'); // No body transform, placeholder preserved
265    expect(codexContent).toContain('GPT');
266  });
267
268  test('should call transformers in correct order', () => {
269    const callOrder = [];
270
271    const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({
272      skills: []
273    });
274    const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue({ patterns: [], antipatterns: [] });
275
276    const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {
277      callOrder.push('cursor');
278    });
279    const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {
280      callOrder.push('claude-code');
281    });
282    const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {
283      callOrder.push('gemini');
284    });
285    const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {
286      callOrder.push('codex');
287    });
288
289    const ROOT_DIR = TEST_DIR;
290    const DIST_DIR = path.join(ROOT_DIR, 'dist');
291
292    const { skills } = utils.readSourceFiles(ROOT_DIR);
293    const patterns = utils.readPatterns(ROOT_DIR);
294    transformers.transformCursor(skills, DIST_DIR, patterns);
295    transformers.transformClaudeCode(skills, DIST_DIR, patterns);
296    transformers.transformGemini(skills, DIST_DIR, patterns);
297    transformers.transformCodex(skills, DIST_DIR, patterns);
298
299    expect(callOrder).toEqual(['cursor', 'claude-code', 'gemini', 'codex']);
300
301    readSourceFilesSpy.mockRestore();
302    readPatternsSpy.mockRestore();
303    transformCursorSpy.mockRestore();
304    transformClaudeCodeSpy.mockRestore();
305    transformGeminiSpy.mockRestore();
306    transformCodexSpy.mockRestore();
307  });
308
309  test('should include agents and kiro transformers', () => {
310    const { skills } = utils.readSourceFiles(TEST_DIR);
311    const patterns = utils.readPatterns(TEST_DIR);
312    const DIST_DIR = path.join(TEST_DIR, 'dist');
313
314    // These should not throw
315    transformers.transformAgents(skills, DIST_DIR, patterns);
316    transformers.transformGitHub(skills, DIST_DIR, patterns);
317    transformers.transformKiro(skills, DIST_DIR, patterns);
318
319    // Verify outputs
320    expect(fs.existsSync(path.join(DIST_DIR, 'agents/.agents/skills'))).toBe(true);
321    expect(fs.existsSync(path.join(DIST_DIR, 'github/.github/skills'))).toBe(true);
322    expect(fs.existsSync(path.join(DIST_DIR, 'kiro/.kiro/skills'))).toBe(true);
323  });
324});