mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Codex auto-discovers subagents bundled inside an installed skill's own agents/ folder, so the separate .codex/agents/*.toml sidecar was redundant. - cli: remove installCodexAgents/isCodexLikely and their install/update calls - context.mjs: remove the CODEX_AGENT_MISSING self-heal directive - build: drop codex agentFormat so no top-level .codex/agents is emitted; the nested in-skill .toml bundling is the whole delivery - remove the tracked .codex/agents/*.toml and the gitignore exception - docs + build.test.js updated for the nested layout - CLI patch version bump; skill version unchanged Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
327 lines
14 KiB
JavaScript
327 lines
14 KiB
JavaScript
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);
|
|
|
|
// 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);
|
|
});
|
|
|
|
test('integration: emits native subagent files for Codex and Claude Code', () => {
|
|
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);
|
|
|
|
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');
|
|
|
|
expect(fs.existsSync(claudeAgentPath)).toBe(true);
|
|
expect(fs.existsSync(codexAgentPath)).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 =');
|
|
});
|
|
|
|
test('integration: verify transformations are correct', () => {
|
|
const skillContent = `---
|
|
name: audit
|
|
description: Run technical quality checks
|
|
user-invocable: true
|
|
argument-hint: "[TARGET=<value>]"
|
|
---
|
|
|
|
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);
|
|
});
|
|
});
|