Files
waggle-os/packages/agent/tests/skill-frontmatter.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

352 lines
10 KiB
TypeScript

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:');
});
});
});