import { describe, it, expect } from 'vitest'; import { parseSkillFrontmatter, nextScope, serializeFrontmatter, SKILL_SCOPE_ORDER } from '../src/skill-frontmatter.js'; describe('parseSkillFrontmatter', () => { it('parses frontmatter with name and description', () => { const content = `--- name: Deploy Helper description: Helps deploy applications --- # Deploy Helper Some content here.`; const { frontmatter, body } = parseSkillFrontmatter(content); expect(frontmatter.name).toBe('Deploy Helper'); expect(frontmatter.description).toBe('Helps deploy applications'); }); it('parses permissions block correctly', () => { const content = `--- permissions: network: true codeExecution: true fileSystem: false --- # Skill`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.permissions).toBeDefined(); expect(frontmatter.permissions!.network).toBe(true); expect(frontmatter.permissions!.codeExecution).toBe(true); expect(frontmatter.permissions!.fileSystem).toBe(false); }); it('returns empty frontmatter when no delimiters', () => { const content = `# Just a Skill No frontmatter here.`; const { frontmatter, body } = parseSkillFrontmatter(content); expect(frontmatter).toEqual({}); expect(body).toBe(content); }); it('returns empty frontmatter when no closing delimiter', () => { const content = `--- name: Broken description: Missing closing delimiter # Skill Content`; const { frontmatter, body } = parseSkillFrontmatter(content); expect(frontmatter).toEqual({}); expect(body).toBe(content); }); it('body is content after frontmatter', () => { const content = `--- name: Test --- # The Body This is the body content.`; const { body } = parseSkillFrontmatter(content); expect(body).toContain('# The Body'); expect(body).toContain('This is the body content.'); expect(body).not.toContain('name: Test'); }); it('mixed true/false permissions parsed correctly', () => { const content = `--- permissions: fileSystem: true network: false codeExecution: true externalServices: false secrets: true browserAutomation: false --- # Skill`; const { frontmatter } = parseSkillFrontmatter(content); const perms = frontmatter.permissions!; expect(perms.fileSystem).toBe(true); expect(perms.network).toBe(false); expect(perms.codeExecution).toBe(true); expect(perms.externalServices).toBe(false); expect(perms.secrets).toBe(true); expect(perms.browserAutomation).toBe(false); }); it('ignores comments in frontmatter', () => { const content = `--- # This is a comment name: My Skill # Another comment description: Does things --- # Content`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.name).toBe('My Skill'); expect(frontmatter.description).toBe('Does things'); }); it('works with content that has no frontmatter (backward compat)', () => { const content = `# Research Team -- Multi-Agent Investigation A coordinated 3-agent research workflow. ## When to use - Complex research questions`; const { frontmatter, body } = parseSkillFrontmatter(content); expect(frontmatter).toEqual({}); expect(body).toBe(content); }); it('parses all fields together (name, description, permissions)', () => { const content = `--- name: Full Skill description: A skill with everything permissions: network: true secrets: true --- # Full Skill Content.`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.name).toBe('Full Skill'); expect(frontmatter.description).toBe('A skill with everything'); expect(frontmatter.permissions!.network).toBe(true); expect(frontmatter.permissions!.secrets).toBe(true); }); it('handles permissions block followed by other keys', () => { const content = `--- permissions: fileSystem: true name: After Permissions --- # Skill`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.permissions!.fileSystem).toBe(true); expect(frontmatter.name).toBe('After Permissions'); }); // Skills 2.0 gap E — scope + promotion metadata describe('scope + promoted_from (gap E)', () => { it('parses an explicit scope field', () => { const content = `--- name: Team Skill scope: team --- # body`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.scope).toBe('team'); }); it('ignores an invalid scope value', () => { const content = `--- name: X scope: nonsense --- body`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.scope).toBeUndefined(); }); it('parses promoted_from as a JSON-array', () => { const content = `--- name: Promoted scope: team promoted_from: [personal, workspace] --- body`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.promoted_from).toEqual(['personal', 'workspace']); }); it('tolerates missing scope (backwards-compat)', () => { const content = `--- name: Legacy description: no scope field --- body`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.scope).toBeUndefined(); expect(frontmatter.name).toBe('Legacy'); }); }); describe('provenance — initiator + source (P5/D4)', () => { it('parses initiator and source', () => { const content = `--- name: Agent Skill initiator: agent source: chat --- body`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.initiator).toBe('agent'); expect(frontmatter.source).toBe('chat'); }); it('ignores an invalid initiator value', () => { const content = `--- name: X initiator: robot --- body`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.initiator).toBeUndefined(); }); it('leaves initiator/source undefined when absent (legacy = user)', () => { const content = `--- name: Legacy --- body`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.initiator).toBeUndefined(); expect(frontmatter.source).toBeUndefined(); }); }); // #15 requirement badges — requires: { env, bins } describe('requires — env + bins (#15)', () => { it('parses a requires block with env and bins', () => { const content = `--- name: Video Tool requires: env: [OPENAI_API_KEY, REPLICATE_API_TOKEN] bins: [ffmpeg, git] --- body`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.requires).toEqual({ env: ['OPENAI_API_KEY', 'REPLICATE_API_TOKEN'], bins: ['ffmpeg', 'git'], }); }); it('parses an env-only requires block (comma-separated form)', () => { const content = `--- requires: env: OPENAI_API_KEY, TAVILY_API_KEY --- body`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.requires).toEqual({ env: ['OPENAI_API_KEY', 'TAVILY_API_KEY'] }); }); it('leaves requires undefined when absent or empty', () => { const { frontmatter: absent } = parseSkillFrontmatter('---\nname: X\n---\nbody'); expect(absent.requires).toBeUndefined(); const { frontmatter: empty } = parseSkillFrontmatter('---\nrequires:\nname: X\n---\nbody'); expect(empty.requires).toBeUndefined(); expect(empty.name).toBe('X'); }); it('requires block coexists with a permissions block', () => { const content = `--- permissions: network: true requires: bins: [docker] name: Both --- body`; const { frontmatter } = parseSkillFrontmatter(content); expect(frontmatter.permissions!.network).toBe(true); expect(frontmatter.requires).toEqual({ bins: ['docker'] }); expect(frontmatter.name).toBe('Both'); }); }); describe('nextScope', () => { it('returns the next scope up', () => { expect(nextScope('personal')).toBe('workspace'); expect(nextScope('workspace')).toBe('team'); expect(nextScope('team')).toBe('enterprise'); }); it('returns null at the top', () => { expect(nextScope('enterprise')).toBeNull(); }); it('SKILL_SCOPE_ORDER is stable', () => { expect(SKILL_SCOPE_ORDER).toEqual(['personal', 'workspace', 'team', 'enterprise']); }); }); describe('serializeFrontmatter', () => { it('round-trips scope + promoted_from through parse', () => { const out = serializeFrontmatter( { name: 'RT', description: 'round-trip', scope: 'team', promoted_from: ['personal', 'workspace'] }, '# body line 1', ); const { frontmatter, body } = parseSkillFrontmatter(out); expect(frontmatter.name).toBe('RT'); expect(frontmatter.scope).toBe('team'); expect(frontmatter.promoted_from).toEqual(['personal', 'workspace']); expect(body).toBe('# body line 1'); }); it('omits scope/promoted_from when absent', () => { const out = serializeFrontmatter({ name: 'NoScope' }, 'body'); expect(out).not.toContain('scope:'); expect(out).not.toContain('promoted_from:'); expect(out).toContain('name: NoScope'); }); it('round-trips initiator + source through parse (P5/D4)', () => { const out = serializeFrontmatter( { name: 'Prov', initiator: 'agent', source: 'chat' }, '# body', ); const { frontmatter } = parseSkillFrontmatter(out); expect(frontmatter.initiator).toBe('agent'); expect(frontmatter.source).toBe('chat'); }); it('omits initiator/source when absent', () => { const out = serializeFrontmatter({ name: 'NoProv' }, 'body'); expect(out).not.toContain('initiator:'); expect(out).not.toContain('source:'); }); it('round-trips requires through parse (#15)', () => { const out = serializeFrontmatter( { name: 'Req', requires: { env: ['OPENAI_API_KEY'], bins: ['ffmpeg', 'git'] } }, '# body', ); const { frontmatter } = parseSkillFrontmatter(out); expect(frontmatter.requires).toEqual({ env: ['OPENAI_API_KEY'], bins: ['ffmpeg', 'git'] }); }); it('omits requires when absent or empty (#15)', () => { expect(serializeFrontmatter({ name: 'NoReq' }, 'body')).not.toContain('requires:'); expect(serializeFrontmatter({ name: 'EmptyReq', requires: {} }, 'body')).not.toContain('requires:'); }); }); });