Skip to main content
Glama
parser.test.ts11.9 kB
/** * Tests for Role Parser * * @package WP_Navigator_MCP */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { parseYaml, validateRole, loadRoleFile } from './parser.js'; import { RoleSource, RoleValidationError, RoleSchemaVersionError, ROLE_SCHEMA_VERSION, } from './types.js'; // ============================================================================= // YAML Parser Tests // ============================================================================= describe('parseYaml', () => { it('parses simple key-value pairs', () => { const yaml = ` name: content-editor description: Content specialist priority: 10 `; const result = parseYaml(yaml); expect(result.name).toBe('content-editor'); expect(result.description).toBe('Content specialist'); expect(result.priority).toBe(10); }); it('parses literal block scalars (|)', () => { const yaml = ` name: test-role context: | Line one Line two Line three description: After multiline `; const result = parseYaml(yaml); // YAML 1.2 spec: literal blocks include trailing newline by default (clip mode) expect(result.context).toBe('Line one\nLine two\nLine three\n'); expect(result.description).toBe('After multiline'); }); it('parses folded block scalars (>)', () => { const yaml = ` name: test-role context: > This is a long description that spans multiple lines description: After folded `; const result = parseYaml(yaml); // YAML 1.2 spec: folded blocks include trailing newline by default (clip mode) expect(result.context).toBe('This is a long description that spans multiple lines\n'); expect(result.description).toBe('After folded'); }); it('parses arrays with dash syntax', () => { const yaml = ` name: test-role focus_areas: - Content editing - Media management - SEO metadata `; const result = parseYaml(yaml); expect(result.focus_areas).toEqual(['Content editing', 'Media management', 'SEO metadata']); }); it('parses inline arrays', () => { const yaml = ` name: test-role tags: [content, editing, wordpress] `; const result = parseYaml(yaml); expect(result.tags).toEqual(['content', 'editing', 'wordpress']); }); it('parses nested objects', () => { const yaml = ` name: test-role tools: allowed: - wpnav_list_posts - wpnav_update_post denied: - wpnav_update_plugin `; const result = parseYaml(yaml); expect(result.tools).toEqual({ allowed: ['wpnav_list_posts', 'wpnav_update_post'], denied: ['wpnav_update_plugin'], }); }); it('handles comments', () => { const yaml = ` # This is a comment name: test-role # Another comment description: A test role `; const result = parseYaml(yaml); expect(result.name).toBe('test-role'); expect(result.description).toBe('A test role'); }); it('handles boolean values', () => { const yaml = ` enabled: true disabled: false `; const result = parseYaml(yaml); expect(result.enabled).toBe(true); expect(result.disabled).toBe(false); }); it('handles null values', () => { const yaml = ` empty: null tilde: ~ `; const result = parseYaml(yaml); expect(result.empty).toBe(null); expect(result.tilde).toBe(null); }); it('handles quoted strings', () => { const yaml = ` single: 'single quoted' double: "double quoted" number_as_string: "123" `; const result = parseYaml(yaml); expect(result.single).toBe('single quoted'); expect(result.double).toBe('double quoted'); expect(result.number_as_string).toBe('123'); }); it('handles empty objects and arrays', () => { const yaml = ` empty_object: {} empty_array: [] `; const result = parseYaml(yaml); expect(result.empty_object).toEqual({}); expect(result.empty_array).toEqual([]); }); }); // ============================================================================= // Role Validation Tests // ============================================================================= describe('validateRole', () => { it('validates a minimal valid role', () => { const role = { name: 'content-editor', description: 'Content specialist', context: 'You are a content editor.', }; const result = validateRole(role, '/test/role.yaml'); expect(result.name).toBe('content-editor'); expect(result.description).toBe('Content specialist'); expect(result.context).toBe('You are a content editor.'); }); it('validates a complete role with all fields', () => { const role = { name: 'content-editor', description: 'Content specialist', context: 'You are a content editor.', focus_areas: ['Content editing', 'Media'], avoid: ['Plugin changes'], tools: { allowed: ['wpnav_list_posts'], denied: ['wpnav_update_plugin'], }, priority: 50, version: '1.0.0', tags: ['content', 'editing'], author: 'WP Navigator', schema_version: 1, }; const result = validateRole(role, '/test/role.yaml'); expect(result).toEqual(role); }); it('throws on missing name', () => { const role = { description: 'Test', context: 'Test context', }; expect(() => validateRole(role, '/test/role.yaml')).toThrow(RoleValidationError); expect(() => validateRole(role, '/test/role.yaml')).toThrow(/name/); }); it('throws on invalid name format', () => { const role = { name: 'Content Editor', // Should be slug format description: 'Test', context: 'Test context', }; expect(() => validateRole(role, '/test/role.yaml')).toThrow(RoleValidationError); expect(() => validateRole(role, '/test/role.yaml')).toThrow(/lowercase slug/); }); it('throws on missing description', () => { const role = { name: 'content-editor', context: 'Test context', }; expect(() => validateRole(role, '/test/role.yaml')).toThrow(RoleValidationError); expect(() => validateRole(role, '/test/role.yaml')).toThrow(/description/); }); it('throws on missing context', () => { const role = { name: 'content-editor', description: 'Test', }; expect(() => validateRole(role, '/test/role.yaml')).toThrow(RoleValidationError); expect(() => validateRole(role, '/test/role.yaml')).toThrow(/context/); }); it('throws on invalid focus_areas type', () => { const role = { name: 'content-editor', description: 'Test', context: 'Test context', focus_areas: 'not an array', }; expect(() => validateRole(role, '/test/role.yaml')).toThrow(RoleValidationError); expect(() => validateRole(role, '/test/role.yaml')).toThrow(/focus_areas must be an array/); }); it('throws on invalid tools.allowed type', () => { const role = { name: 'content-editor', description: 'Test', context: 'Test context', tools: { allowed: 'not an array', }, }; expect(() => validateRole(role, '/test/role.yaml')).toThrow(RoleValidationError); expect(() => validateRole(role, '/test/role.yaml')).toThrow(/tools.allowed must be an array/); }); it('throws on unsupported schema_version', () => { const role = { name: 'content-editor', description: 'Test', context: 'Test context', schema_version: ROLE_SCHEMA_VERSION + 1, }; expect(() => validateRole(role, '/test/role.yaml')).toThrow(RoleSchemaVersionError); }); it('accepts valid role name formats', () => { const validNames = ['a', 'ab', 'content-editor', 'seo-specialist', 'site-admin', 'dev1']; for (const name of validNames) { const role = { name, description: 'Test', context: 'Test context', }; expect(() => validateRole(role, '/test/role.yaml')).not.toThrow(); } }); it('rejects invalid role name formats', () => { const invalidNames = ['Content-Editor', 'content_editor', '1content', '-content', 'content-']; for (const name of invalidNames) { const role = { name, description: 'Test', context: 'Test context', }; expect(() => validateRole(role, '/test/role.yaml')).toThrow(RoleValidationError); } }); }); // ============================================================================= // Role File Loading Tests // ============================================================================= describe('loadRoleFile', () => { let tempDir: string; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wpnav-role-test-')); }); afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }); }); it('loads a valid YAML role file', () => { const yamlContent = ` name: content-editor description: Content specialist context: | You are a content specialist working on a WordPress site. Focus on clear, engaging content. focus_areas: - Page and post content - Media management `; const filePath = path.join(tempDir, 'content-editor.yaml'); fs.writeFileSync(filePath, yamlContent); const result = loadRoleFile(filePath, RoleSource.PROJECT); expect(result.success).toBe(true); expect(result.role).toBeDefined(); expect(result.role!.name).toBe('content-editor'); expect(result.role!.source).toBe(RoleSource.PROJECT); expect(result.role!.sourcePath).toBe(filePath); expect(result.role!.focus_areas).toEqual(['Page and post content', 'Media management']); }); it('loads a valid JSON role file', () => { const jsonContent = JSON.stringify({ name: 'site-admin', description: 'Full site administrator', context: 'You have full administrative access.', }); const filePath = path.join(tempDir, 'site-admin.json'); fs.writeFileSync(filePath, jsonContent); const result = loadRoleFile(filePath, RoleSource.BUNDLED); expect(result.success).toBe(true); expect(result.role).toBeDefined(); expect(result.role!.name).toBe('site-admin'); expect(result.role!.source).toBe(RoleSource.BUNDLED); }); it('returns error for non-existent file', () => { const result = loadRoleFile('/nonexistent/role.yaml', RoleSource.PROJECT); expect(result.success).toBe(false); expect(result.error).toContain('Failed to read file'); }); it('returns error for invalid YAML', () => { const filePath = path.join(tempDir, 'invalid.yaml'); fs.writeFileSync(filePath, 'name: test\n bad indent'); const result = loadRoleFile(filePath, RoleSource.PROJECT); // Our simple parser may not catch all YAML errors, so just verify we handle it expect(result.path).toBe(filePath); }); it('returns error for unsupported file extension', () => { const filePath = path.join(tempDir, 'role.txt'); fs.writeFileSync(filePath, 'name: test'); const result = loadRoleFile(filePath, RoleSource.PROJECT); expect(result.success).toBe(false); expect(result.error).toContain('Unsupported file extension'); }); it('returns error for validation failure', () => { const yamlContent = ` name: Invalid Name description: Test context: Test context `; const filePath = path.join(tempDir, 'invalid-role.yaml'); fs.writeFileSync(filePath, yamlContent); const result = loadRoleFile(filePath, RoleSource.PROJECT); expect(result.success).toBe(false); expect(result.error).toContain('lowercase slug'); }); it('handles .yml extension', () => { const yamlContent = ` name: test-role description: Test context: Test context `; const filePath = path.join(tempDir, 'test.yml'); fs.writeFileSync(filePath, yamlContent); const result = loadRoleFile(filePath, RoleSource.GLOBAL); expect(result.success).toBe(true); expect(result.role!.name).toBe('test-role'); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/littlebearapps/wp-navigator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server