Skip to main content
Glama
index.test.ts11.4 kB
/** * Embeddings Module Tests * * Tests for the main search functionality using test tool vectors. * * @package WP_Navigator_MCP * @since 2.7.0 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { searchTools, searchByCategory, loadToolVectors, isVectorsLoaded, getCategories, getStats, _resetState, _setToolVectors, type ToolEmbedding, } from './index.js'; // Test tool data const TEST_TOOLS: ToolEmbedding[] = [ { name: 'wpnav_list_posts', description: 'List WordPress posts with filtering, pagination, and field selection', category: 'content', keywords: ['list', 'post', 'wordpress', 'filter', 'paginat', 'field', 'select'], }, { name: 'wpnav_create_post', description: 'Create a new WordPress post with title, content, and metadata', category: 'content', keywords: ['creat', 'post', 'wordpress', 'titl', 'content', 'metadata'], }, { name: 'wpnav_list_pages', description: 'List WordPress pages with hierarchical structure', category: 'content', keywords: ['list', 'page', 'wordpress', 'hierarch', 'structur'], }, { name: 'wpnav_list_plugins', description: 'List installed WordPress plugins with status information', category: 'plugins', keywords: ['list', 'plugin', 'wordpress', 'install', 'status', 'informat'], }, { name: 'wpnav_activate_plugin', description: 'Activate a WordPress plugin by slug', category: 'plugins', keywords: ['activ', 'plugin', 'wordpress', 'slug'], }, { name: 'wpnav_upload_media_from_url', description: 'Upload media from URL to WordPress media library', category: 'content', keywords: ['upload', 'media', 'url', 'wordpress', 'librari'], }, { name: 'wpnav_list_themes', description: 'List installed WordPress themes with activation status', category: 'themes', keywords: ['list', 'theme', 'wordpress', 'install', 'activ', 'status'], }, { name: 'wpnav_gutenberg_insert_block', description: 'Insert a Gutenberg block into page or post content', category: 'content', keywords: ['insert', 'gutenberg', 'block', 'page', 'post', 'content'], }, { name: 'wpnav_list_users', description: 'List WordPress users with role filtering', category: 'users', keywords: ['list', 'user', 'wordpress', 'role', 'filter'], }, { name: 'wpnav_introspect', description: 'Get WP Navigator API capabilities and site information', category: 'core', keywords: ['introspect', 'capabil', 'site', 'informat', 'api'], }, ]; describe('loadToolVectors', () => { beforeEach(() => { _resetState(); }); it('loads tool vectors via _setToolVectors', () => { _setToolVectors(TEST_TOOLS); const tools = loadToolVectors(); expect(tools.length).toBe(10); expect(tools[0].name).toBe('wpnav_list_posts'); }); it('caches vectors after first load', () => { _setToolVectors(TEST_TOOLS); const first = loadToolVectors(); const second = loadToolVectors(); expect(first).toBe(second); }); it('returns empty array and logs warning when vectors file not found', () => { // _resetState clears toolVectors to null // When loadToolVectors is called without _setToolVectors, // it tries to require the JSON file which may not exist const spy = vi.spyOn(console, 'warn').mockImplementation(() => {}); // In test environment, require('./tool-vectors.json') may or may not exist // If it doesn't exist, the catch block returns empty array const tools = loadToolVectors(); // Either loads real vectors OR returns empty array with warning if (tools.length === 0) { expect(spy).toHaveBeenCalledWith(expect.stringContaining('Tool vectors not found')); } spy.mockRestore(); }); }); describe('isVectorsLoaded', () => { beforeEach(() => { _resetState(); }); it('returns true after loading vectors', () => { _setToolVectors(TEST_TOOLS); expect(isVectorsLoaded()).toBe(true); }); it('returns false before loading vectors', () => { expect(isVectorsLoaded()).toBe(false); }); }); describe('searchTools', () => { beforeEach(() => { _resetState(); _setToolVectors(TEST_TOOLS); }); it('returns empty array for empty query', () => { expect(searchTools('')).toEqual([]); expect(searchTools(' ')).toEqual([]); }); it('finds tools matching query keywords', () => { const results = searchTools('list posts'); expect(results.length).toBeGreaterThan(0); const names = results.map((r) => r.name); expect(names).toContain('wpnav_list_posts'); }); it('respects limit option', () => { const results = searchTools('list', { limit: 3 }); expect(results.length).toBeLessThanOrEqual(3); }); it('filters by minimum score', () => { const resultsLow = searchTools('wordpress', { minScore: 0.01 }); const resultsHigh = searchTools('wordpress', { minScore: 0.99 }); expect(resultsLow.length).toBeGreaterThanOrEqual(resultsHigh.length); }); it('returns results with required fields', () => { const results = searchTools('plugins'); for (const result of results) { expect(result).toHaveProperty('name'); expect(result).toHaveProperty('description'); expect(result).toHaveProperty('category'); expect(result).toHaveProperty('score'); expect(typeof result.score).toBe('number'); expect(result.score).toBeGreaterThanOrEqual(0); } }); it('sorts results by score descending', () => { const results = searchTools('list wordpress'); for (let i = 1; i < results.length; i++) { expect(results[i - 1].score).toBeGreaterThanOrEqual(results[i].score); } }); }); describe('searchByCategory', () => { beforeEach(() => { _resetState(); _setToolVectors(TEST_TOOLS); }); it('returns all tools in a category', () => { const results = searchByCategory('content'); expect(results.length).toBeGreaterThan(0); for (const result of results) { expect(result.category).toBe('content'); } }); it('is case-insensitive', () => { const lower = searchByCategory('plugins'); const upper = searchByCategory('PLUGINS'); const mixed = searchByCategory('Plugins'); expect(lower.length).toBe(upper.length); expect(lower.length).toBe(mixed.length); }); it('returns empty array for unknown category', () => { const results = searchByCategory('nonexistent'); expect(results).toEqual([]); }); it('returns results with score 1.0', () => { const results = searchByCategory('themes'); for (const result of results) { expect(result.score).toBe(1.0); } }); it('returns empty array when no tools are loaded', () => { _resetState(); // Don't call _setToolVectors const spy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const results = searchByCategory('content'); // If tools couldn't be loaded, returns empty // This handles both cases: no file OR file exists expect(Array.isArray(results)).toBe(true); spy.mockRestore(); }); }); describe('getCategories', () => { beforeEach(() => { _resetState(); _setToolVectors(TEST_TOOLS); }); it('returns unique category names', () => { const categories = getCategories(); expect(categories.length).toBeGreaterThan(0); expect(new Set(categories).size).toBe(categories.length); }); it('returns sorted categories', () => { const categories = getCategories(); const sorted = [...categories].sort(); expect(categories).toEqual(sorted); }); it('includes expected categories', () => { const categories = getCategories(); expect(categories).toContain('content'); expect(categories).toContain('plugins'); expect(categories).toContain('themes'); expect(categories).toContain('users'); expect(categories).toContain('core'); }); }); describe('getStats', () => { beforeEach(() => { _resetState(); _setToolVectors(TEST_TOOLS); }); it('returns total tool count', () => { const stats = getStats(); expect(stats.total).toBe(10); }); it('returns count by category', () => { const stats = getStats(); expect(stats.byCategory).toHaveProperty('content'); expect(stats.byCategory).toHaveProperty('plugins'); expect(typeof stats.byCategory.content).toBe('number'); }); it('category counts sum to total', () => { const stats = getStats(); const sum = Object.values(stats.byCategory).reduce((a, b) => a + b, 0); expect(sum).toBe(stats.total); }); }); describe('useEmbeddings fallback', () => { beforeEach(() => { _resetState(); _setToolVectors(TEST_TOOLS); }); it('falls back to TF-IDF when useEmbeddings=true but no vectors', () => { // TEST_TOOLS don't have .vector property, so should fall back const results = searchTools('create post', { useEmbeddings: true }); expect(results.length).toBeGreaterThan(0); // Should still find relevant tools const names = results.map((r) => r.name); expect(names.some((n) => n.includes('post'))).toBe(true); }); it('logs warning when embeddings not available', () => { const spy = vi.spyOn(console, 'warn').mockImplementation(() => {}); _setToolVectors(TEST_TOOLS); searchTools('test query', { useEmbeddings: true }); expect(spy).toHaveBeenCalledWith(expect.stringContaining('falling back to TF-IDF')); spy.mockRestore(); }); it('works correctly with tools that have empty vector arrays', () => { const toolsWithEmptyVectors = TEST_TOOLS.map((t) => ({ ...t, vector: [], // Empty vector array })); _setToolVectors(toolsWithEmptyVectors); const spy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const results = searchTools('list posts', { useEmbeddings: true }); // Should still work via TF-IDF fallback expect(results.length).toBeGreaterThan(0); expect(spy).toHaveBeenCalled(); spy.mockRestore(); }); }); describe('search accuracy validation', () => { beforeEach(() => { _resetState(); _setToolVectors(TEST_TOOLS); }); it('finds posts tools for "create blog post"', () => { const results = searchTools('create blog post', { limit: 5 }); const names = results.map((r) => r.name); // Should find create_post related tools expect(names.some((n) => n.includes('post'))).toBe(true); }); it('finds plugin tools for "manage plugins"', () => { const results = searchTools('manage plugins', { limit: 5 }); const names = results.map((r) => r.name); expect(names.some((n) => n.includes('plugin'))).toBe(true); }); it('finds media tools for "upload image"', () => { const results = searchTools('upload image media', { limit: 5 }); const names = results.map((r) => r.name); expect(names.some((n) => n.includes('media') || n.includes('upload'))).toBe(true); }); it('finds gutenberg tools for "gutenberg blocks"', () => { const results = searchTools('gutenberg blocks', { limit: 5 }); const names = results.map((r) => r.name); expect(names.some((n) => n.includes('gutenberg'))).toBe(true); }); it('finds user tools for "user roles"', () => { const results = searchTools('user roles', { limit: 5 }); const names = results.map((r) => r.name); expect(names.some((n) => n.includes('user'))).toBe(true); }); });

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