Open-source marketplace for AI Agent skills. Features: - Next.js 15 web app with i18n (en/fa) - CLI tool for skill installation (npx skillhub) - GitHub crawler/indexer with multi-strategy discovery - Security scanning for all indexed skills - Self-hostable with Docker Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
192 lines
5.0 KiB
TypeScript
192 lines
5.0 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { validateSkill, isValidSkill, formatValidationSummary } from './validator.js';
|
|
import type { ParsedSkill } from './types.js';
|
|
|
|
// Helper function to create a mock parsed skill
|
|
function createMockSkill(overrides: Partial<ParsedSkill> = {}): ParsedSkill {
|
|
return {
|
|
metadata: {
|
|
name: 'test-skill',
|
|
description: 'A test skill',
|
|
version: '1.0.0',
|
|
license: 'MIT',
|
|
compatibility: {
|
|
platforms: ['claude', 'codex'],
|
|
},
|
|
...overrides.metadata,
|
|
},
|
|
content: `# Test Skill
|
|
|
|
This is a test skill with good content structure.
|
|
|
|
## Usage
|
|
|
|
Here's how to use this skill with an example:
|
|
|
|
\`\`\`bash
|
|
npx skillhub install test/test-skill
|
|
\`\`\`
|
|
`,
|
|
resources: {
|
|
scripts: [],
|
|
references: [],
|
|
...overrides.resources,
|
|
},
|
|
validation: {
|
|
isValid: true,
|
|
errors: [],
|
|
warnings: [],
|
|
...overrides.validation,
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('validateSkill', () => {
|
|
it('should return valid for a well-formed skill', () => {
|
|
const skill = createMockSkill();
|
|
const result = validateSkill(skill);
|
|
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should detect empty content', () => {
|
|
const skill = createMockSkill({ content: '' });
|
|
const result = validateSkill(skill);
|
|
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors.some(e => e.code === 'EMPTY_CONTENT')).toBe(true);
|
|
});
|
|
|
|
it('should warn about short content', () => {
|
|
const skill = createMockSkill({ content: 'Very short.' });
|
|
const result = validateSkill(skill);
|
|
|
|
expect(result.warnings.some(w => w.code === 'CONTENT_TOO_SHORT')).toBe(true);
|
|
});
|
|
|
|
it('should warn about placeholder content', () => {
|
|
const skill = createMockSkill({
|
|
content: `# Test Skill
|
|
|
|
TODO: Add more content here.
|
|
|
|
## Usage
|
|
|
|
FIXME: Write usage instructions.
|
|
`,
|
|
});
|
|
const result = validateSkill(skill);
|
|
|
|
expect(result.warnings.some(w => w.code === 'PLACEHOLDER_CONTENT')).toBe(true);
|
|
});
|
|
|
|
it('should warn about missing version', () => {
|
|
const skill = createMockSkill({
|
|
metadata: {
|
|
name: 'test-skill',
|
|
description: 'A test skill',
|
|
version: undefined,
|
|
},
|
|
});
|
|
const result = validateSkill(skill);
|
|
|
|
expect(result.warnings.some(w => w.code === 'NO_VERSION')).toBe(true);
|
|
});
|
|
|
|
it('should warn about missing license', () => {
|
|
const skill = createMockSkill({
|
|
metadata: {
|
|
name: 'test-skill',
|
|
description: 'A test skill',
|
|
license: undefined,
|
|
},
|
|
});
|
|
const result = validateSkill(skill);
|
|
|
|
expect(result.warnings.some(w => w.code === 'NO_LICENSE')).toBe(true);
|
|
});
|
|
|
|
it('should warn about missing platforms', () => {
|
|
const skill = createMockSkill({
|
|
metadata: {
|
|
name: 'test-skill',
|
|
description: 'A test skill',
|
|
compatibility: {},
|
|
},
|
|
});
|
|
const result = validateSkill(skill);
|
|
|
|
expect(result.warnings.some(w => w.code === 'NO_PLATFORMS')).toBe(true);
|
|
});
|
|
|
|
it('should warn about unusual script extensions', () => {
|
|
const skill = createMockSkill({
|
|
resources: {
|
|
scripts: [{ name: 'script.xyz', content: 'echo hello' }],
|
|
references: [],
|
|
},
|
|
});
|
|
const result = validateSkill(skill);
|
|
|
|
expect(result.warnings.some(w => w.code === 'UNUSUAL_SCRIPT_EXT')).toBe(true);
|
|
});
|
|
|
|
it('should warn about large reference files', () => {
|
|
const skill = createMockSkill({
|
|
resources: {
|
|
scripts: [],
|
|
references: [{ name: 'huge-file.md', content: 'x'.repeat(150000) }],
|
|
},
|
|
});
|
|
const result = validateSkill(skill);
|
|
|
|
expect(result.warnings.some(w => w.code === 'LARGE_REFERENCE')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isValidSkill', () => {
|
|
it('should return true for valid skills', () => {
|
|
const skill = createMockSkill();
|
|
expect(isValidSkill(skill)).toBe(true);
|
|
});
|
|
|
|
it('should return false for invalid skills', () => {
|
|
const skill = createMockSkill({
|
|
validation: {
|
|
isValid: false,
|
|
errors: [{ code: 'MISSING_NAME', message: 'Name is required' }],
|
|
warnings: [],
|
|
},
|
|
});
|
|
expect(isValidSkill(skill)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('formatValidationSummary', () => {
|
|
it('should format valid skill summary', () => {
|
|
const result = {
|
|
isValid: true,
|
|
errors: [],
|
|
warnings: [],
|
|
};
|
|
const summary = formatValidationSummary(result);
|
|
|
|
expect(summary).toContain('Skill is valid');
|
|
});
|
|
|
|
it('should format errors and warnings', () => {
|
|
const result = {
|
|
isValid: false,
|
|
errors: [{ code: 'EMPTY_CONTENT', message: 'Content is empty' }],
|
|
warnings: [{ code: 'NO_LICENSE', message: 'No license', suggestion: 'Add MIT' }],
|
|
};
|
|
const summary = formatValidationSummary(result);
|
|
|
|
expect(summary).toContain('ERROR: Content is empty');
|
|
expect(summary).toContain('WARNING: No license');
|
|
expect(summary).toContain('Suggestion: Add MIT');
|
|
});
|
|
});
|