mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 22:26:38 +03:00
* Add OpenAI plugin submission bundle Build a Codex-native OpenAI plugin with bundled hooks, public listing metadata, submission guidance, privacy coverage, and regression tests. AI assistance: OpenAI Codex prepared and validated these changes under maintainer direction. * Fix provider script command rendering Replace heuristic rewrites across executable scripts with one explicit provider marker, render pinned shortcuts per target harness, and remove the personal email from the public publisher manifest. Addresses automated review feedback on PR #363. AI assistance: OpenAI Codex prepared and validated these changes under maintainer direction.
304 lines
12 KiB
JavaScript
304 lines
12 KiB
JavaScript
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { createTransformer } from '../../../scripts/lib/transformers/factory.js';
|
|
import { parseFrontmatter } from '../../../scripts/lib/utils.js';
|
|
|
|
const TEST_DIR = path.join(process.cwd(), 'test-tmp-factory');
|
|
|
|
// Minimal config using 'cursor' as provider (has existing PROVIDER_PLACEHOLDERS)
|
|
const baseConfig = {
|
|
provider: 'cursor',
|
|
configDir: '.test',
|
|
displayName: 'Test Provider',
|
|
frontmatterFields: [],
|
|
};
|
|
|
|
describe('createTransformer factory', () => {
|
|
beforeEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('should create correct directory structure', () => {
|
|
const transform = createTransformer(baseConfig);
|
|
transform([], TEST_DIR);
|
|
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills'))).toBe(true);
|
|
});
|
|
|
|
test('should always emit name and description', () => {
|
|
const transform = createTransformer(baseConfig);
|
|
const skills = [{ name: 'test', description: 'A test skill', body: 'Body.' }];
|
|
transform(skills, TEST_DIR);
|
|
|
|
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8');
|
|
const parsed = parseFrontmatter(content);
|
|
expect(parsed.frontmatter.name).toBe('test');
|
|
expect(parsed.frontmatter.description).toBe('A test skill');
|
|
expect(parsed.body).toBe('Body.');
|
|
});
|
|
|
|
test('should only emit allowlisted fields', () => {
|
|
const config = { ...baseConfig, frontmatterFields: ['license'] };
|
|
const transform = createTransformer(config);
|
|
const skills = [{
|
|
name: 'test',
|
|
description: 'Test',
|
|
license: 'MIT',
|
|
compatibility: 'all',
|
|
metadata: 'meta',
|
|
body: 'Body'
|
|
}];
|
|
transform(skills, TEST_DIR);
|
|
|
|
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8');
|
|
const parsed = parseFrontmatter(content);
|
|
expect(parsed.frontmatter.license).toBe('MIT');
|
|
expect(parsed.frontmatter.compatibility).toBeUndefined();
|
|
expect(parsed.frontmatter.metadata).toBeUndefined();
|
|
});
|
|
|
|
test('should skip empty optional fields', () => {
|
|
const config = { ...baseConfig, frontmatterFields: ['license'] };
|
|
const transform = createTransformer(config);
|
|
const skills = [{ name: 'test', description: 'Test', license: '', body: 'Body' }];
|
|
transform(skills, TEST_DIR);
|
|
|
|
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8');
|
|
const parsed = parseFrontmatter(content);
|
|
expect(parsed.frontmatter.license).toBeUndefined();
|
|
});
|
|
|
|
test('should emit user-invocable as true when skill is user-invocable', () => {
|
|
const config = { ...baseConfig, frontmatterFields: ['user-invocable'] };
|
|
const transform = createTransformer(config);
|
|
const skills = [{ name: 'test', description: 'Test', userInvocable: true, body: 'Body' }];
|
|
transform(skills, TEST_DIR);
|
|
|
|
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8');
|
|
const parsed = parseFrontmatter(content);
|
|
expect(parsed.frontmatter['user-invocable']).toBe(true);
|
|
});
|
|
|
|
test('should not emit user-invocable when skill is not user-invocable', () => {
|
|
const config = { ...baseConfig, frontmatterFields: ['user-invocable'] };
|
|
const transform = createTransformer(config);
|
|
const skills = [{ name: 'test', description: 'Test', userInvocable: false, body: 'Body' }];
|
|
transform(skills, TEST_DIR);
|
|
|
|
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8');
|
|
const parsed = parseFrontmatter(content);
|
|
expect(parsed.frontmatter['user-invocable']).toBeUndefined();
|
|
});
|
|
|
|
test('should emit argument-hint only when user-invocable', () => {
|
|
const config = { ...baseConfig, frontmatterFields: ['argument-hint'] };
|
|
const transform = createTransformer(config);
|
|
|
|
// User-invocable with hint
|
|
const skills1 = [{ name: 'test', description: 'Test', userInvocable: true, argumentHint: '[target]', body: 'Body' }];
|
|
transform(skills1, TEST_DIR);
|
|
let content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8');
|
|
let parsed = parseFrontmatter(content);
|
|
expect(parsed.frontmatter['argument-hint']).toBe('[target]');
|
|
|
|
// Non-user-invocable with hint
|
|
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
const skills2 = [{ name: 'test', description: 'Test', userInvocable: false, argumentHint: '[target]', body: 'Body' }];
|
|
transform(skills2, TEST_DIR);
|
|
content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8');
|
|
parsed = parseFrontmatter(content);
|
|
expect(parsed.frontmatter['argument-hint']).toBeUndefined();
|
|
});
|
|
|
|
test('should apply bodyTransform after placeholder replacement', () => {
|
|
const config = {
|
|
...baseConfig,
|
|
bodyTransform: (body) => body.replace(/PLACEHOLDER/, 'TRANSFORMED'),
|
|
};
|
|
const transform = createTransformer(config);
|
|
const skills = [{ name: 'test', description: 'Test', body: 'PLACEHOLDER content' }];
|
|
transform(skills, TEST_DIR);
|
|
|
|
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8');
|
|
expect(content).toContain('TRANSFORMED content');
|
|
});
|
|
|
|
test('should copy reference files', () => {
|
|
const transform = createTransformer(baseConfig);
|
|
const skills = [{
|
|
name: 'test',
|
|
description: 'Test',
|
|
body: 'Body',
|
|
references: [
|
|
{ name: 'ref1', content: 'Reference 1 content', filePath: '/fake/ref1.md' },
|
|
{ name: 'ref2', content: 'Reference 2 content', filePath: '/fake/ref2.md' },
|
|
]
|
|
}];
|
|
transform(skills, TEST_DIR);
|
|
|
|
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills/test/reference/ref1.md'))).toBe(true);
|
|
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills/test/reference/ref2.md'))).toBe(true);
|
|
const ref1 = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/reference/ref1.md'), 'utf-8');
|
|
expect(ref1).toBe('Reference 1 content');
|
|
});
|
|
|
|
test('should render the explicit script provider marker without rewriting executable code', () => {
|
|
const config = {
|
|
...baseConfig,
|
|
provider: 'codex',
|
|
placeholderProvider: 'codex',
|
|
};
|
|
const transform = createTransformer(config);
|
|
const skills = [{
|
|
name: 'impeccable',
|
|
description: 'Test',
|
|
body: 'Body',
|
|
scripts: [{
|
|
name: 'example.mjs',
|
|
content: [
|
|
"export const IMPECCABLE_COMMAND_PREFIX = '/'; // @impeccable-provider-command-prefix",
|
|
'const command = `${IMPECCABLE_COMMAND_PREFIX}impeccable polish`;',
|
|
'const hint = "Run /impeccable audit";',
|
|
'const hook = ".github/hooks/impeccable.json";',
|
|
'const runtime = "/src/lib/impeccable/__runtime.js";',
|
|
'const regex = /impeccable\\b/gi;',
|
|
].join('\n'),
|
|
}],
|
|
}];
|
|
|
|
transform(skills, TEST_DIR);
|
|
|
|
const script = fs.readFileSync(
|
|
path.join(TEST_DIR, 'codex/.test/skills/impeccable/scripts/example.mjs'),
|
|
'utf-8',
|
|
);
|
|
expect(script).toContain('IMPECCABLE_COMMAND_PREFIX = "$"');
|
|
expect(script).toContain('`${IMPECCABLE_COMMAND_PREFIX}impeccable polish`');
|
|
expect(script).toContain('"Run /impeccable audit"');
|
|
expect(script).toContain('".github/hooks/impeccable.json"');
|
|
expect(script).toContain('"/src/lib/impeccable/__runtime.js"');
|
|
expect(script).toContain('/impeccable\\b/gi');
|
|
});
|
|
|
|
test('should clean existing directory before writing', () => {
|
|
const transform = createTransformer(baseConfig);
|
|
const existingDir = path.join(TEST_DIR, 'cursor/.test/skills/old');
|
|
fs.mkdirSync(existingDir, { recursive: true });
|
|
fs.writeFileSync(path.join(existingDir, 'SKILL.md'), 'old');
|
|
|
|
const skills = [{ name: 'new', description: 'New', body: 'New' }];
|
|
transform(skills, TEST_DIR);
|
|
|
|
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills/old/SKILL.md'))).toBe(false);
|
|
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills/new/SKILL.md'))).toBe(true);
|
|
});
|
|
|
|
test('should log correct summary', () => {
|
|
const consoleMock = mock(() => {});
|
|
const originalLog = console.log;
|
|
console.log = consoleMock;
|
|
|
|
const transform = createTransformer(baseConfig);
|
|
const skills = [
|
|
{ name: 's1', description: 'Test', userInvocable: true, body: 'body' },
|
|
{ name: 's2', description: 'Test', userInvocable: false, body: 'body' }
|
|
];
|
|
transform(skills, TEST_DIR);
|
|
|
|
console.log = originalLog;
|
|
// v3.0 summary format: `✓ <provider>: <n> skills` (no user-invocable count).
|
|
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Test Provider:'));
|
|
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
|
|
});
|
|
|
|
test('should handle empty skills array', () => {
|
|
const transform = createTransformer(baseConfig);
|
|
transform([], TEST_DIR);
|
|
|
|
const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'cursor/.test/skills'));
|
|
expect(skillDirs).toHaveLength(0);
|
|
});
|
|
|
|
test('should replace {{model}} placeholder', () => {
|
|
const transform = createTransformer(baseConfig);
|
|
const skills = [{ name: 'test', description: 'Test', body: 'Ask {{model}} for help.' }];
|
|
transform(skills, TEST_DIR);
|
|
|
|
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8');
|
|
expect(content).toContain('Ask the model for help.');
|
|
});
|
|
|
|
test('should replace {{config_file}} placeholder', () => {
|
|
const transform = createTransformer(baseConfig);
|
|
const skills = [{ name: 'test', description: 'Test', body: 'See {{config_file}}.' }];
|
|
transform(skills, TEST_DIR);
|
|
|
|
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8');
|
|
expect(content).toContain('See .cursorrules.');
|
|
});
|
|
|
|
test('should handle multiple skills', () => {
|
|
const transform = createTransformer(baseConfig);
|
|
const skills = [
|
|
{ name: 'skill1', description: 'Skill 1', body: 'Body 1' },
|
|
{ name: 'skill2', description: 'Skill 2', body: 'Body 2' },
|
|
];
|
|
transform(skills, TEST_DIR);
|
|
|
|
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills/skill1/SKILL.md'))).toBe(true);
|
|
expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills/skill2/SKILL.md'))).toBe(true);
|
|
});
|
|
|
|
test('should preserve multiline body content', () => {
|
|
const transform = createTransformer(baseConfig);
|
|
const skills = [{
|
|
name: 'test',
|
|
description: 'Test',
|
|
body: `First paragraph.\n\nSecond paragraph.\n\n- List item 1\n- List item 2`
|
|
}];
|
|
transform(skills, TEST_DIR);
|
|
|
|
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8');
|
|
const parsed = parseFrontmatter(content);
|
|
expect(parsed.body).toContain('First paragraph.');
|
|
expect(parsed.body).toContain('Second paragraph.');
|
|
expect(parsed.body).toContain('- List item 1');
|
|
});
|
|
|
|
test('should emit all spec fields when configured', () => {
|
|
const config = {
|
|
...baseConfig,
|
|
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'],
|
|
};
|
|
const transform = createTransformer(config);
|
|
const skills = [{
|
|
name: 'test',
|
|
description: 'Test',
|
|
userInvocable: true,
|
|
argumentHint: '[target]',
|
|
license: 'MIT',
|
|
compatibility: 'claude-code',
|
|
metadata: 'v1',
|
|
allowedTools: 'Bash,Edit',
|
|
body: 'Body'
|
|
}];
|
|
transform(skills, TEST_DIR);
|
|
|
|
const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8');
|
|
expect(content).toContain('user-invocable: true');
|
|
expect(content).toContain('argument-hint:');
|
|
expect(content).toContain('license: MIT');
|
|
expect(content).toContain('compatibility: claude-code');
|
|
expect(content).toContain('metadata: v1');
|
|
expect(content).toContain('allowed-tools: Bash,Edit');
|
|
});
|
|
});
|