This commit is contained in:
284
packages/sdk/tests/validate-skill.test.ts
Normal file
284
packages/sdk/tests/validate-skill.test.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
validateSkillMd,
|
||||
isValidSemver,
|
||||
compareSemver,
|
||||
checkSkillDependencies,
|
||||
checkVersionDowngrade,
|
||||
} from '../src/validate-skill.js';
|
||||
|
||||
describe('validateSkillMd', () => {
|
||||
it('parses valid SKILL.md', () => {
|
||||
const content = `---
|
||||
name: summarizer
|
||||
description: Summarizes long documents
|
||||
version: 1.0.0
|
||||
author: waggle-team
|
||||
---
|
||||
|
||||
You are an expert summarizer. Given a document, produce a concise summary.
|
||||
`;
|
||||
|
||||
const result = validateSkillMd(content);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
expect(result.warnings).toHaveLength(0);
|
||||
expect(result.metadata).toBeDefined();
|
||||
expect(result.metadata!.name).toBe('summarizer');
|
||||
expect(result.metadata!.description).toBe('Summarizes long documents');
|
||||
expect(result.metadata!.version).toBe('1.0.0');
|
||||
expect(result.metadata!.author).toBe('waggle-team');
|
||||
expect(result.metadata!.systemPrompt).toBe(
|
||||
'You are an expert summarizer. Given a document, produce a concise summary.',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects missing name', () => {
|
||||
const content = `---
|
||||
description: A skill without a name
|
||||
---
|
||||
|
||||
Some prompt.
|
||||
`;
|
||||
|
||||
const result = validateSkillMd(content);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('Missing required field: name');
|
||||
});
|
||||
|
||||
it('rejects missing description', () => {
|
||||
const content = `---
|
||||
name: no-desc
|
||||
---
|
||||
|
||||
Some prompt.
|
||||
`;
|
||||
|
||||
const result = validateSkillMd(content);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('Missing required field: description');
|
||||
});
|
||||
|
||||
it('rejects missing frontmatter', () => {
|
||||
const content = `Just a plain markdown file with no frontmatter.`;
|
||||
|
||||
const result = validateSkillMd(content);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
'Missing YAML frontmatter (must be wrapped in --- delimiters)',
|
||||
);
|
||||
});
|
||||
|
||||
it('extracts system prompt from body', () => {
|
||||
const content = `---
|
||||
name: coder
|
||||
description: Writes code
|
||||
---
|
||||
|
||||
You are a coding assistant.
|
||||
|
||||
Always use TypeScript.
|
||||
Write clean, tested code.
|
||||
`;
|
||||
|
||||
const result = validateSkillMd(content);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.metadata!.systemPrompt).toBe(
|
||||
'You are a coding assistant.\n\nAlways use TypeScript.\nWrite clean, tested code.',
|
||||
);
|
||||
});
|
||||
|
||||
// F18: Version validation tests
|
||||
it('warns on invalid semver version', () => {
|
||||
const content = `---
|
||||
name: bad-version
|
||||
description: Has invalid version
|
||||
version: abc
|
||||
---
|
||||
|
||||
Prompt.
|
||||
`;
|
||||
|
||||
const result = validateSkillMd(content);
|
||||
expect(result.valid).toBe(true); // still valid, just warns
|
||||
expect(result.warnings).toHaveLength(1);
|
||||
expect(result.warnings[0]).toContain('Invalid semver version');
|
||||
expect(result.warnings[0]).toContain('abc');
|
||||
});
|
||||
|
||||
it('accepts valid semver with pre-release', () => {
|
||||
const content = `---
|
||||
name: prerelease
|
||||
description: Has pre-release version
|
||||
version: 2.0.0-beta.1
|
||||
---
|
||||
|
||||
Prompt.
|
||||
`;
|
||||
|
||||
const result = validateSkillMd(content);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.warnings).toHaveLength(0);
|
||||
expect(result.metadata!.version).toBe('2.0.0-beta.1');
|
||||
});
|
||||
|
||||
// F18: Dependency parsing tests
|
||||
it('parses dependencies from bracket syntax', () => {
|
||||
const content = `---
|
||||
name: researcher
|
||||
description: Research skill
|
||||
dependencies: [search_memory, save_memory, query_knowledge]
|
||||
---
|
||||
|
||||
Research prompt.
|
||||
`;
|
||||
|
||||
const result = validateSkillMd(content);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.metadata!.dependencies).toEqual(['search_memory', 'save_memory', 'query_knowledge']);
|
||||
});
|
||||
|
||||
it('parses dependencies from comma-separated syntax', () => {
|
||||
const content = `---
|
||||
name: researcher
|
||||
description: Research skill
|
||||
dependencies: search_memory, save_memory
|
||||
---
|
||||
|
||||
Research prompt.
|
||||
`;
|
||||
|
||||
const result = validateSkillMd(content);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.metadata!.dependencies).toEqual(['search_memory', 'save_memory']);
|
||||
});
|
||||
|
||||
it('returns undefined dependencies when field is absent', () => {
|
||||
const content = `---
|
||||
name: no-deps
|
||||
description: No dependencies
|
||||
---
|
||||
|
||||
Prompt.
|
||||
`;
|
||||
|
||||
const result = validateSkillMd(content);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.metadata!.dependencies).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidSemver', () => {
|
||||
it('accepts standard semver', () => {
|
||||
expect(isValidSemver('1.0.0')).toBe(true);
|
||||
expect(isValidSemver('0.1.0')).toBe(true);
|
||||
expect(isValidSemver('12.34.56')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts semver with pre-release', () => {
|
||||
expect(isValidSemver('1.0.0-alpha')).toBe(true);
|
||||
expect(isValidSemver('1.0.0-beta.1')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts semver with build metadata', () => {
|
||||
expect(isValidSemver('1.0.0+build.123')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid semver', () => {
|
||||
expect(isValidSemver('1.0')).toBe(false);
|
||||
expect(isValidSemver('abc')).toBe(false);
|
||||
expect(isValidSemver('v1.0.0')).toBe(false);
|
||||
expect(isValidSemver('1')).toBe(false);
|
||||
expect(isValidSemver('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareSemver', () => {
|
||||
it('compares equal versions', () => {
|
||||
expect(compareSemver('1.0.0', '1.0.0')).toBe(0);
|
||||
});
|
||||
|
||||
it('compares different major versions', () => {
|
||||
expect(compareSemver('2.0.0', '1.0.0')).toBe(1);
|
||||
expect(compareSemver('1.0.0', '2.0.0')).toBe(-1);
|
||||
});
|
||||
|
||||
it('compares different minor versions', () => {
|
||||
expect(compareSemver('1.2.0', '1.1.0')).toBe(1);
|
||||
expect(compareSemver('1.0.0', '1.1.0')).toBe(-1);
|
||||
});
|
||||
|
||||
it('compares different patch versions', () => {
|
||||
expect(compareSemver('1.0.2', '1.0.1')).toBe(1);
|
||||
expect(compareSemver('1.0.0', '1.0.1')).toBe(-1);
|
||||
});
|
||||
|
||||
it('ignores pre-release when comparing', () => {
|
||||
expect(compareSemver('1.0.0-alpha', '1.0.0-beta')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkSkillDependencies', () => {
|
||||
it('returns no warnings when all dependencies are available', () => {
|
||||
const metadata = {
|
||||
name: 'test-skill',
|
||||
description: 'test',
|
||||
dependencies: ['search_memory', 'save_memory'],
|
||||
systemPrompt: '',
|
||||
};
|
||||
const warnings = checkSkillDependencies(metadata, ['search_memory', 'save_memory', 'get_identity']);
|
||||
expect(warnings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('warns about missing dependency tools', () => {
|
||||
const metadata = {
|
||||
name: 'test-skill',
|
||||
description: 'test',
|
||||
dependencies: ['search_memory', 'nonexistent_tool'],
|
||||
systemPrompt: '',
|
||||
};
|
||||
const warnings = checkSkillDependencies(metadata, ['search_memory', 'save_memory']);
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toContain('nonexistent_tool');
|
||||
expect(warnings[0]).toContain('not available');
|
||||
});
|
||||
|
||||
it('returns no warnings when no dependencies declared', () => {
|
||||
const metadata = {
|
||||
name: 'test-skill',
|
||||
description: 'test',
|
||||
systemPrompt: '',
|
||||
};
|
||||
const warnings = checkSkillDependencies(metadata, ['search_memory']);
|
||||
expect(warnings).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkVersionDowngrade', () => {
|
||||
it('detects downgrade', () => {
|
||||
const warning = checkVersionDowngrade('my-skill', '2.0.0', '1.0.0');
|
||||
expect(warning).toContain('Downgrade detected');
|
||||
expect(warning).toContain('1.0.0 < 2.0.0');
|
||||
});
|
||||
|
||||
it('detects same version re-install', () => {
|
||||
const warning = checkVersionDowngrade('my-skill', '1.0.0', '1.0.0');
|
||||
expect(warning).toContain('Same version re-install');
|
||||
});
|
||||
|
||||
it('returns null for upgrade', () => {
|
||||
const warning = checkVersionDowngrade('my-skill', '1.0.0', '2.0.0');
|
||||
expect(warning).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when versions are missing', () => {
|
||||
expect(checkVersionDowngrade('s', undefined, '1.0.0')).toBeNull();
|
||||
expect(checkVersionDowngrade('s', '1.0.0', undefined)).toBeNull();
|
||||
expect(checkVersionDowngrade('s', undefined, undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for invalid semver', () => {
|
||||
expect(checkVersionDowngrade('s', 'abc', '1.0.0')).toBeNull();
|
||||
expect(checkVersionDowngrade('s', '1.0.0', 'xyz')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user