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