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'; import * as transformers from '../scripts/lib/transformers/index.js'; const TEST_DIR = path.join(process.cwd(), 'test-tmp-build'); describe('build orchestration', () => { beforeEach(() => { if (fs.existsSync(TEST_DIR)) { fs.rmSync(TEST_DIR, { recursive: true, force: true }); } fs.mkdirSync(TEST_DIR, { recursive: true }); }); afterEach(() => { if (fs.existsSync(TEST_DIR)) { fs.rmSync(TEST_DIR, { recursive: true, force: true }); } }); test('should call readSourceFiles with root directory', () => { const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({ skills: [] }); const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {}); const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {}); const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {}); const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {}); // Simulate the build process const ROOT_DIR = TEST_DIR; const DIST_DIR = path.join(ROOT_DIR, 'dist'); 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(); transformGeminiSpy.mockRestore(); transformCodexSpy.mockRestore(); }); 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({ skills }); const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue(patterns); const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {}); const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {}); const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {}); const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {}); const ROOT_DIR = TEST_DIR; const DIST_DIR = path.join(ROOT_DIR, 'dist'); const sourceFiles = utils.readSourceFiles(ROOT_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(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(); transformCodexSpy.mockRestore(); }); test('should handle empty source files', () => { const patterns = { patterns: [], antipatterns: [] }; const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({ skills: [] }); const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue(patterns); const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => {}); const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => {}); const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => {}); const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => {}); const ROOT_DIR = TEST_DIR; const DIST_DIR = path.join(ROOT_DIR, 'dist'); 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, 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(); transformCodexSpy.mockRestore(); }); test('integration: full build creates all expected outputs', () => { // Create test source files const skillContent = `--- name: test-skill description: A test skill license: MIT --- This is a test skill body.`; const skillDir = path.join(TEST_DIR, 'skill'); fs.mkdirSync(skillDir, { recursive: true }); fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent); // Run the build process const DIST_DIR = path.join(TEST_DIR, 'dist'); 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); transformers.transformAntigravity(skills, DIST_DIR, patterns); // Verify Cursor outputs 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/.claude/skills/test-skill/SKILL.md'))).toBe(true); // Verify Gemini outputs 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/.codex/skills/test-skill/SKILL.md'))).toBe(true); // Verify Antigravity outputs expect(fs.existsSync(path.join(DIST_DIR, 'antigravity/.agent/skills/test-skill/SKILL.md'))).toBe(true); }); test('integration: emits native subagent files for Codex, Claude Code, GitHub Copilot, and Cursor', () => { const skillContent = `--- name: test-skill description: A test skill --- This is a test skill body.`; const agentContent = `--- name: asset-producer codex-name: asset_producer description: Produces assets from approved crops tools: Read, Write model: inherit effort: medium max-turns: 8 nickname-candidates: - Asset Plate --- Do not redesign the approved crop.`; const skillDir = path.join(TEST_DIR, 'skill'); fs.mkdirSync(path.join(skillDir, 'agents'), { recursive: true }); fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent); fs.writeFileSync(path.join(skillDir, 'agents/asset-producer.md'), agentContent); const DIST_DIR = path.join(TEST_DIR, 'dist'); const { skills } = utils.readSourceFiles(TEST_DIR); const patterns = utils.readPatterns(TEST_DIR); transformers.transformClaudeCode(skills, DIST_DIR, patterns); transformers.transformCodex(skills, DIST_DIR, patterns); transformers.transformGitHub(skills, DIST_DIR, patterns); transformers.transformCursor(skills, DIST_DIR, patterns); const claudeAgentPath = path.join(DIST_DIR, 'claude-code/.claude/agents/asset-producer.md'); // Codex auto-discovers agents nested inside an installed skill, so the .toml // ships in the skill's own agents/ folder rather than a top-level .codex/agents/. const codexAgentPath = path.join(DIST_DIR, 'codex/.codex/skills/test-skill/agents/asset_producer.toml'); // GitHub Copilot discovers repo-level custom agents at .github/agents/.agent.md. const copilotAgentPath = path.join(DIST_DIR, 'github/.github/agents/asset-producer.agent.md'); // Cursor discovers repo-level subagents at .cursor/agents/.md. const cursorAgentPath = path.join(DIST_DIR, 'cursor/.cursor/agents/asset-producer.md'); expect(fs.existsSync(claudeAgentPath)).toBe(true); expect(fs.existsSync(codexAgentPath)).toBe(true); expect(fs.existsSync(copilotAgentPath)).toBe(true); expect(fs.existsSync(cursorAgentPath)).toBe(true); const claudeAgent = fs.readFileSync(claudeAgentPath, 'utf-8'); expect(claudeAgent).toContain('name: asset-producer'); expect(claudeAgent).toContain('tools: Read, Write'); expect(claudeAgent).toContain('maxTurns: 8'); const codexAgent = fs.readFileSync(codexAgentPath, 'utf-8'); expect(codexAgent).toContain('name = "asset_producer"'); expect(codexAgent).toContain('model_reasoning_effort = "medium"'); expect(codexAgent).toContain('nickname_candidates = ["Asset Plate"]'); expect(codexAgent).toContain('developer_instructions ='); // Copilot's portable frontmatter is name + description only: omitting // `tools` grants access to all tools, and there are no documented // model/effort/max-turns equivalents. const copilotAgent = fs.readFileSync(copilotAgentPath, 'utf-8'); expect(copilotAgent).toContain('name: asset-producer'); expect(copilotAgent).toContain('description: Produces assets from approved crops'); expect(copilotAgent).toContain('Do not redesign the approved crop.'); expect(copilotAgent).not.toContain('tools:'); expect(copilotAgent).not.toContain('model:'); expect(copilotAgent).not.toContain('effort:'); expect(copilotAgent).not.toContain('maxTurns:'); // Cursor keeps model (inherit maps directly) and derives readonly from the // tool list; this agent carries Write, so no readonly field is emitted. const cursorAgent = fs.readFileSync(cursorAgentPath, 'utf-8'); expect(cursorAgent).toContain('name: asset-producer'); expect(cursorAgent).toContain('description: Produces assets from approved crops'); expect(cursorAgent).toContain('model: inherit'); expect(cursorAgent).toContain('is_background: false'); expect(cursorAgent).toContain('Do not redesign the approved crop.'); expect(cursorAgent).not.toContain('readonly:'); expect(cursorAgent).not.toContain('tools:'); expect(cursorAgent).not.toContain('effort:'); expect(cursorAgent).not.toContain('maxTurns:'); }); test('integration: verify transformations are correct', () => { const skillContent = `--- name: audit description: Run technical quality checks user-invocable: true argument-hint: "[TARGET=]" --- Please audit {{target}} for technical quality. Ask {{model}} for help.`; const skillDir = path.join(TEST_DIR, 'skill'); fs.mkdirSync(skillDir, { recursive: true }); fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent); const DIST_DIR = path.join(TEST_DIR, 'dist'); 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: full frontmatter with user-invocable 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 with user-invocable and argument-hint const claudeContent = fs.readFileSync(path.join(DIST_DIR, 'claude-code/.claude/skills/audit/SKILL.md'), 'utf-8'); expect(claudeContent).toContain('---'); expect(claudeContent).toContain('name: audit'); expect(claudeContent).toContain('user-invocable: true'); expect(claudeContent).toContain('{{target}}'); expect(claudeContent).toContain('Claude'); // 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('{{target}}'); // No body transform, placeholder preserved expect(geminiContent).toContain('Gemini'); // 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}}'); // No body transform, placeholder preserved expect(codexContent).toContain('GPT'); }); test('should call transformers in correct order', () => { const callOrder = []; const readSourceFilesSpy = spyOn(utils, 'readSourceFiles').mockReturnValue({ skills: [] }); const readPatternsSpy = spyOn(utils, 'readPatterns').mockReturnValue({ patterns: [], antipatterns: [] }); const transformCursorSpy = spyOn(transformers, 'transformCursor').mockImplementation(() => { callOrder.push('cursor'); }); const transformClaudeCodeSpy = spyOn(transformers, 'transformClaudeCode').mockImplementation(() => { callOrder.push('claude-code'); }); const transformGeminiSpy = spyOn(transformers, 'transformGemini').mockImplementation(() => { callOrder.push('gemini'); }); const transformCodexSpy = spyOn(transformers, 'transformCodex').mockImplementation(() => { callOrder.push('codex'); }); const ROOT_DIR = TEST_DIR; const DIST_DIR = path.join(ROOT_DIR, 'dist'); 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.transformGitHub(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, 'github/.github/skills'))).toBe(true); expect(fs.existsSync(path.join(DIST_DIR, 'kiro/.kiro/skills'))).toBe(true); }); test('Antigravity transformer emits skills under .agent/', () => { const { skills } = utils.readSourceFiles(TEST_DIR); const patterns = utils.readPatterns(TEST_DIR); const DIST_DIR = path.join(TEST_DIR, 'dist'); // Should not throw transformers.transformAntigravity(skills, DIST_DIR, patterns); // Verify the harness directory is created at the correct path expect(fs.existsSync(path.join(DIST_DIR, 'antigravity/.agent/skills'))).toBe(true); }); }); // The skill's scripts dir ships the launcher, its Windows twin, the pinned // engine VERSION, the page JS, and command-metadata.json. Nothing else: the // verbs live in the engine binary the launcher runs, and platform binaries // (scripts/bin/) are fetched per machine, never read as source. describe('skill scripts payload', () => { const ROOT_DIR = process.cwd(); const { skills } = utils.readSourceFiles(ROOT_DIR); const scripts = skills[0]?.scripts ?? []; const names = new Set(scripts.map((s) => s.name)); test('ships the launcher, VERSION, page JS, and command metadata', () => { for (const expected of [ 'impeccable', 'impeccable.cmd', 'VERSION', 'command-metadata.json', 'live-browser.js', 'live-browser-dom.js', 'live-browser-session.js', 'modern-screenshot.umd.js', ]) { expect(names.has(expected)).toBe(true); } }); test('ships no engine entry points and no bundled detector', () => { // The engine verbs live in the binary; the only Node scripts allowed in // the payload are the comp-fidelity build pipeline and its libs, which // have not moved into the engine yet. const allowedNodeScripts = new Set([ 'build-phase.mjs', 'comp-diff.mjs', 'comp-spec.mjs', 'font-match.mjs', 'lib/font-fingerprint.mjs', 'lib/font-index.mjs', 'lib/hero-checks.mjs', 'lib/image-metrics.mjs', 'lib/png.mjs', 'lib/raster.mjs', ]); const stray = [...names].filter((n) => (n.endsWith('.mjs') || n.startsWith('detector/') || n.startsWith('lib/')) && !allowedNodeScripts.has(n)); expect(stray).toEqual([]); }); test('never reads platform binaries as source', () => { expect([...names].filter((n) => n.startsWith('bin/'))).toEqual([]); }); test('the launcher is executable and VERSION matches ENGINE_VERSION', () => { const launcher = scripts.find((s) => s.name === 'impeccable'); expect(launcher.mode & 0o111).not.toBe(0); const version = scripts.find((s) => s.name === 'VERSION'); expect(version.content.trim()).toBe(fs.readFileSync(path.join(ROOT_DIR, 'ENGINE_VERSION'), 'utf-8').trim()); }); }); describe('degraded-mode fallback reference generation', () => { const ROOT = process.cwd(); const DEGRADED_TEST_DIR = path.join(ROOT, 'test-tmp-degraded'); const DIST = path.join(DEGRADED_TEST_DIR, 'dist'); const readDegraded = (provider, configDir, role) => fs.readFileSync( path.join(DIST, provider, configDir, 'skills', 'impeccable', 'reference', 'degraded', `${role}.md`), 'utf-8' ); beforeEach(() => { if (fs.existsSync(DEGRADED_TEST_DIR)) fs.rmSync(DEGRADED_TEST_DIR, { recursive: true, force: true }); fs.mkdirSync(DEGRADED_TEST_DIR, { recursive: true }); const { skills } = utils.readSourceFiles(ROOT); transformers.transformClaudeCode(skills, DIST); transformers.transformCodex(skills, DIST); }); afterEach(() => { if (fs.existsSync(DEGRADED_TEST_DIR)) fs.rmSync(DEGRADED_TEST_DIR, { recursive: true, force: true }); }); test('a build emits reference/degraded/.md for every agent, prefix-stripped', () => { const dir = path.join(DIST, 'codex', '.codex', 'skills', 'impeccable', 'reference', 'degraded'); const files = fs.readdirSync(dir).sort(); expect(files).toEqual([ 'asset-producer.md', 'documenter.md', 'finish-reviewer.md', 'manual-edit-applier.md', ]); }); test('finish-reviewer fallback opens with the preamble and carries a distinctive body phrase', () => { const content = readDegraded('codex', '.codex', 'finish-reviewer'); expect(content.startsWith('')) .toBe(true); expect(content).toContain('This harness has no subagent capability, so you are running this role inline.'); // Distinctive phrase from the agent body proves the source body was inlined. expect(content).toContain('material_fixes'); }); test('generated fallbacks pass through provider-block compilation (codex keeps its block, others strip it)', () => { // Standalone provider blocks are the shape compileProviderBlocks compiles. // A synthetic agent proves the degraded path runs the same compilation as // ordinary reference files, with the right provider tags per target. const synthetic = { name: 'impeccable', description: 'synthetic', body: 'Synthetic skill body.', agents: [ { name: 'impeccable-synthetic', body: 'Shared body line.\n\n\nCODEX_ONLY_MARKER for the codex target.\n\n\nMore shared body.', }, ], }; const synthDist = path.join(DEGRADED_TEST_DIR, 'synth'); transformers.transformCodex([synthetic], synthDist); transformers.transformClaudeCode([synthetic], synthDist); const read = (provider, configDir) => fs.readFileSync( path.join(synthDist, provider, configDir, 'skills', 'impeccable', 'reference', 'degraded', 'synthetic.md'), 'utf-8' ); const codex = read('codex', '.codex'); const claude = read('claude-code', '.claude'); expect(codex).toContain('CODEX_ONLY_MARKER'); expect(claude).not.toContain('CODEX_ONLY_MARKER'); // Both still carry the preamble and the shared body. expect(codex.startsWith('', }, ], }; const synthDist = path.join(AGENT_TEST_DIR, 'synth'); transformers.transformCodex([synthetic], synthDist); transformers.transformClaudeCode([synthetic], synthDist); const emitted = [ 'codex/.codex/skills/impeccable/agents/impeccable_synthetic.toml', 'codex/.codex/skills/impeccable/reference/degraded/synthetic.md', 'claude-code/.claude/agents/impeccable-synthetic.md', ]; for (const relPath of emitted) { const content = fs.readFileSync(path.join(synthDist, relPath), 'utf-8'); expect(content).not.toContain('{{'); expect(content).not.toMatch(/