Skip to main content
Glama
role.test.ts21.4 kB
/** * Role Command Tests * * @package WP_Navigator_MCP * @since 2.7.0 */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { handleRole, handleRoleList, handleRoleShow, handleRoleUse, handleRoleClear, } from './role.js'; // ============================================================================= // Mocks // ============================================================================= // Mock the roles module vi.mock('../../roles/index.js', () => ({ discoverRoles: vi.fn(), getRole: vi.fn(), listAvailableRoles: vi.fn(), })); // Mock runtime state module vi.mock('../../roles/runtime-state.js', () => ({ runtimeRoleState: { initialize: vi.fn(), getRole: vi.fn(), setRole: vi.fn(), clear: vi.fn(), }, STATE_FILE_NAME: '.wpnav-state.json', })); import { discoverRoles, getRole, listAvailableRoles } from '../../roles/index.js'; import { runtimeRoleState } from '../../roles/runtime-state.js'; // ============================================================================= // Test Fixtures // ============================================================================= function createMockRoles() { return new Map([ [ 'content-editor', { name: 'content-editor', description: 'Focus on content creation and editing', context: 'You are a content editor specializing in WordPress', source: 'bundled' as const, sourcePath: '/bundled/content-editor.yaml', focus_areas: ['Post editing', 'Page management', 'Media handling'], avoid: ['Plugin changes', 'Theme modifications'], tools: { allowed: ['wpnav_list_posts', 'wpnav_create_post', 'wpnav_update_post'], denied: ['wpnav_activate_plugin', 'wpnav_delete_plugin'], }, }, ], [ 'developer', { name: 'developer', description: 'Full access for development tasks', context: 'You are a WordPress developer with full access', source: 'bundled' as const, sourcePath: '/bundled/developer.yaml', focus_areas: ['Plugin management', 'Theme development', 'Site configuration'], avoid: [], tools: { allowed: [], denied: [], }, }, ], [ 'seo-specialist', { name: 'seo-specialist', description: 'SEO and content optimization', context: 'You are an SEO specialist', source: 'project' as const, sourcePath: './roles/seo-specialist.yaml', focus_areas: ['Meta optimization', 'Content structure'], avoid: ['Direct database access'], tools: { allowed: ['wpnav_list_posts', 'wpnav_update_post'], denied: ['wpnav_delete_post'], }, }, ], ]); } function createMockDiscoveryResult() { return { roles: createMockRoles(), sources: { bundled: ['content-editor', 'developer'], global: [] as string[], project: ['seo-specialist'], }, errors: [] as Array<{ success: boolean; error?: string; path: string }>, }; } // ============================================================================= // Tests // ============================================================================= describe('Role Command', () => { let consoleLogSpy: ReturnType<typeof vi.spyOn>; let consoleErrorSpy: ReturnType<typeof vi.spyOn>; beforeEach(() => { consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); vi.clearAllMocks(); }); afterEach(() => { consoleLogSpy.mockRestore(); consoleErrorSpy.mockRestore(); }); // =========================================================================== // handleRoleList tests // =========================================================================== describe('handleRoleList', () => { it('should list all roles with JSON output', async () => { vi.mocked(discoverRoles).mockReturnValue(createMockDiscoveryResult()); vi.mocked(runtimeRoleState.getRole).mockReturnValue(null); const result = await handleRoleList({ json: true }); expect(result).toBe(0); expect(consoleLogSpy).toHaveBeenCalled(); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(true); expect(output.command).toBe('role list'); expect(output.data.total).toBe(3); expect(output.data.roles).toHaveLength(3); }); it('should include role details in JSON output', async () => { vi.mocked(discoverRoles).mockReturnValue(createMockDiscoveryResult()); vi.mocked(runtimeRoleState.getRole).mockReturnValue(null); await handleRoleList({ json: true }); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); const contentEditor = output.data.roles.find( (r: { slug: string }) => r.slug === 'content-editor' ); expect(contentEditor).toBeDefined(); expect(contentEditor.name).toBe('content-editor'); expect(contentEditor.description).toBe('Focus on content creation and editing'); expect(contentEditor.source).toBe('bundled'); expect(contentEditor.tools_allowed_count).toBe(3); expect(contentEditor.tools_denied_count).toBe(2); }); it('should include source counts in JSON output', async () => { vi.mocked(discoverRoles).mockReturnValue(createMockDiscoveryResult()); vi.mocked(runtimeRoleState.getRole).mockReturnValue(null); await handleRoleList({ json: true }); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.data.sources.bundled).toBe(2); expect(output.data.sources.global).toBe(0); expect(output.data.sources.project).toBe(1); }); it('should show active role in JSON output', async () => { vi.mocked(discoverRoles).mockReturnValue(createMockDiscoveryResult()); vi.mocked(runtimeRoleState.getRole).mockReturnValue('developer'); await handleRoleList({ json: true }); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.data.active).toBe('developer'); }); it('should show null when no active role', async () => { vi.mocked(discoverRoles).mockReturnValue(createMockDiscoveryResult()); vi.mocked(runtimeRoleState.getRole).mockReturnValue(null); await handleRoleList({ json: true }); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.data.active).toBeNull(); }); it('should render TUI output without error', async () => { vi.mocked(discoverRoles).mockReturnValue(createMockDiscoveryResult()); vi.mocked(runtimeRoleState.getRole).mockReturnValue(null); const result = await handleRoleList({}); expect(result).toBe(0); // TUI outputs to stderr expect(consoleErrorSpy).toHaveBeenCalled(); }); it('should show active role indicator in TUI', async () => { vi.mocked(discoverRoles).mockReturnValue(createMockDiscoveryResult()); vi.mocked(runtimeRoleState.getRole).mockReturnValue('content-editor'); await handleRoleList({}); // Should include active role info const allCalls = consoleErrorSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n'); expect(allCalls).toContain('Active role'); }); it('should handle empty roles list', async () => { vi.mocked(discoverRoles).mockReturnValue({ roles: new Map(), sources: { bundled: [] as string[], global: [] as string[], project: [] as string[] }, errors: [] as Array<{ success: boolean; error?: string; path: string }>, }); vi.mocked(runtimeRoleState.getRole).mockReturnValue(null); const result = await handleRoleList({ json: true }); expect(result).toBe(0); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.data.total).toBe(0); expect(output.data.roles).toHaveLength(0); }); }); // =========================================================================== // handleRoleShow tests // =========================================================================== describe('handleRoleShow', () => { it('should show role details with JSON output', async () => { const mockRole = createMockRoles().get('content-editor')!; vi.mocked(getRole).mockReturnValue(mockRole); const result = await handleRoleShow('content-editor', { json: true }); expect(result).toBe(0); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(true); expect(output.command).toBe('role show'); expect(output.data.name).toBe('content-editor'); expect(output.data.description).toBe('Focus on content creation and editing'); }); it('should include focus areas in output', async () => { const mockRole = createMockRoles().get('content-editor')!; vi.mocked(getRole).mockReturnValue(mockRole); await handleRoleShow('content-editor', { json: true }); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.data.focus_areas).toContain('Post editing'); expect(output.data.focus_areas).toContain('Page management'); }); it('should include avoid list in output', async () => { const mockRole = createMockRoles().get('content-editor')!; vi.mocked(getRole).mockReturnValue(mockRole); await handleRoleShow('content-editor', { json: true }); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.data.avoid).toContain('Plugin changes'); }); it('should include tools in output', async () => { const mockRole = createMockRoles().get('content-editor')!; vi.mocked(getRole).mockReturnValue(mockRole); await handleRoleShow('content-editor', { json: true }); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.data.tools.allowed).toContain('wpnav_list_posts'); expect(output.data.tools.denied).toContain('wpnav_activate_plugin'); }); it('should return error when slug missing (JSON)', async () => { const result = await handleRoleShow('', { json: true }); expect(result).toBe(1); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(false); expect(output.error.code).toBe('MISSING_SLUG'); }); it('should return error when slug missing (TUI)', async () => { const result = await handleRoleShow('', {}); expect(result).toBe(1); const allCalls = consoleErrorSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n'); expect(allCalls).toContain('Role slug required'); }); it('should return error when role not found (JSON)', async () => { vi.mocked(getRole).mockReturnValue(null); vi.mocked(listAvailableRoles).mockReturnValue(['content-editor', 'developer']); const result = await handleRoleShow('nonexistent', { json: true }); expect(result).toBe(1); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(false); expect(output.error.code).toBe('ROLE_NOT_FOUND'); expect(output.error.available).toContain('content-editor'); }); it('should return error when role not found (TUI)', async () => { vi.mocked(getRole).mockReturnValue(null); vi.mocked(listAvailableRoles).mockReturnValue(['content-editor', 'developer']); const result = await handleRoleShow('nonexistent', {}); expect(result).toBe(1); const allCalls = consoleErrorSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n'); expect(allCalls).toContain('Role not found'); expect(allCalls).toContain('content-editor'); }); it('should render TUI output without error', async () => { const mockRole = createMockRoles().get('content-editor')!; vi.mocked(getRole).mockReturnValue(mockRole); const result = await handleRoleShow('content-editor', {}); expect(result).toBe(0); expect(consoleErrorSpy).toHaveBeenCalled(); }); }); // =========================================================================== // handleRoleUse tests // =========================================================================== describe('handleRoleUse', () => { it('should set active role with JSON output', async () => { const mockRole = createMockRoles().get('developer')!; vi.mocked(runtimeRoleState.setRole).mockReturnValue({ success: true }); vi.mocked(getRole).mockReturnValue(mockRole); const result = await handleRoleUse('developer', { json: true }); expect(result).toBe(0); expect(runtimeRoleState.initialize).toHaveBeenCalled(); expect(runtimeRoleState.setRole).toHaveBeenCalledWith('developer', 'cli'); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(true); expect(output.command).toBe('role use'); expect(output.data.slug).toBe('developer'); }); it('should include state file path in output', async () => { const mockRole = createMockRoles().get('developer')!; vi.mocked(runtimeRoleState.setRole).mockReturnValue({ success: true }); vi.mocked(getRole).mockReturnValue(mockRole); await handleRoleUse('developer', { json: true }); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.data.state_file).toBe('.wpnav-state.json'); }); it('should return error when slug missing (JSON)', async () => { const result = await handleRoleUse('', { json: true }); expect(result).toBe(1); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(false); expect(output.error.code).toBe('MISSING_SLUG'); }); it('should return error when slug missing (TUI)', async () => { const result = await handleRoleUse('', {}); expect(result).toBe(1); const allCalls = consoleErrorSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n'); expect(allCalls).toContain('Role slug required'); }); it('should return error when setRole fails (JSON)', async () => { vi.mocked(runtimeRoleState.setRole).mockReturnValue({ success: false, error: 'Role not found: "invalid"', }); vi.mocked(listAvailableRoles).mockReturnValue(['content-editor', 'developer']); const result = await handleRoleUse('invalid', { json: true }); expect(result).toBe(1); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(false); expect(output.error.code).toBe('ROLE_NOT_FOUND'); }); it('should return error when setRole fails (TUI)', async () => { vi.mocked(runtimeRoleState.setRole).mockReturnValue({ success: false, error: 'Role not found: "invalid"', }); vi.mocked(listAvailableRoles).mockReturnValue(['content-editor', 'developer']); const result = await handleRoleUse('invalid', {}); expect(result).toBe(1); const allCalls = consoleErrorSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n'); expect(allCalls).toContain('Role not found'); }); it('should render TUI output without error on success', async () => { const mockRole = createMockRoles().get('developer')!; vi.mocked(runtimeRoleState.setRole).mockReturnValue({ success: true }); vi.mocked(getRole).mockReturnValue(mockRole); const result = await handleRoleUse('developer', {}); expect(result).toBe(0); expect(consoleErrorSpy).toHaveBeenCalled(); const allCalls = consoleErrorSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n'); expect(allCalls).toContain('Role activated'); }); }); // =========================================================================== // handleRoleClear tests // =========================================================================== describe('handleRoleClear', () => { it('should clear active role with JSON output', async () => { vi.mocked(runtimeRoleState.getRole).mockReturnValue('developer'); const result = await handleRoleClear({ json: true }); expect(result).toBe(0); expect(runtimeRoleState.initialize).toHaveBeenCalled(); expect(runtimeRoleState.clear).toHaveBeenCalled(); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(true); expect(output.command).toBe('role clear'); expect(output.data.previous_role).toBe('developer'); }); it('should handle clearing when no role was set (JSON)', async () => { vi.mocked(runtimeRoleState.getRole).mockReturnValue(null); const result = await handleRoleClear({ json: true }); expect(result).toBe(0); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(true); expect(output.data.previous_role).toBeNull(); }); it('should render TUI output with previous role', async () => { vi.mocked(runtimeRoleState.getRole).mockReturnValue('developer'); const result = await handleRoleClear({}); expect(result).toBe(0); const allCalls = consoleErrorSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n'); expect(allCalls).toContain('Role cleared'); }); it('should render TUI output when no role was set', async () => { vi.mocked(runtimeRoleState.getRole).mockReturnValue(null); const result = await handleRoleClear({}); expect(result).toBe(0); const allCalls = consoleErrorSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n'); expect(allCalls).toContain('No active role'); }); }); // =========================================================================== // handleRole (main handler) tests // =========================================================================== describe('handleRole (main handler)', () => { it('should route "list" subcommand correctly', async () => { vi.mocked(discoverRoles).mockReturnValue(createMockDiscoveryResult()); vi.mocked(runtimeRoleState.getRole).mockReturnValue(null); const result = await handleRole('list', [], { json: true }); expect(result).toBe(0); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.command).toBe('role list'); }); it('should default to list when no subcommand', async () => { vi.mocked(discoverRoles).mockReturnValue(createMockDiscoveryResult()); vi.mocked(runtimeRoleState.getRole).mockReturnValue(null); const result = await handleRole(undefined, [], { json: true }); expect(result).toBe(0); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.command).toBe('role list'); }); it('should route "show" subcommand with args', async () => { const mockRole = createMockRoles().get('content-editor')!; vi.mocked(getRole).mockReturnValue(mockRole); const result = await handleRole('show', ['content-editor'], { json: true }); expect(result).toBe(0); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.command).toBe('role show'); expect(output.data.name).toBe('content-editor'); }); it('should route "use" subcommand with args', async () => { const mockRole = createMockRoles().get('developer')!; vi.mocked(runtimeRoleState.setRole).mockReturnValue({ success: true }); vi.mocked(getRole).mockReturnValue(mockRole); const result = await handleRole('use', ['developer'], { json: true }); expect(result).toBe(0); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.command).toBe('role use'); }); it('should route "clear" subcommand', async () => { vi.mocked(runtimeRoleState.getRole).mockReturnValue(null); const result = await handleRole('clear', [], { json: true }); expect(result).toBe(0); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.command).toBe('role clear'); }); it('should return error for unknown subcommand (JSON)', async () => { const result = await handleRole('unknown', [], { json: true }); expect(result).toBe(1); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(false); expect(output.error.code).toBe('UNKNOWN_SUBCOMMAND'); expect(output.error.available).toContain('list'); expect(output.error.available).toContain('show'); expect(output.error.available).toContain('use'); expect(output.error.available).toContain('clear'); }); it('should return error for unknown subcommand (TUI)', async () => { const result = await handleRole('unknown', [], {}); expect(result).toBe(1); const allCalls = consoleErrorSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n'); expect(allCalls).toContain('Unknown role subcommand'); expect(allCalls).toContain('Available subcommands'); }); }); });

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