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});