Skip to main content
Glama
validate.test.ts15.2 kB
/** * Tests for enhanced validate command * * @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'; // ============================================================================= // Helper Functions (extracted from cli.ts for testing) // ============================================================================= /** * Snapshot validation result */ interface SnapshotValidation { checked: boolean; site_index?: { exists: boolean; valid: boolean; errors: string[] }; pages: Array<{ file: string; valid: boolean; errors?: string[] }>; plugins: Array<{ file: string; valid: boolean; errors?: string[] }>; } /** * Validate a single snapshot JSON file */ function validateSnapshotFile(filePath: string): { valid: boolean; errors: string[] } { const errors: string[] = []; try { const content = fs.readFileSync(filePath, 'utf8'); const data = JSON.parse(content); // Basic structure validation - must be an object (not array, null, or primitive) if (typeof data !== 'object' || data === null || Array.isArray(data)) { errors.push('Snapshot must be a JSON object'); } return { valid: errors.length === 0, errors }; } catch (err) { if (err instanceof SyntaxError) { errors.push(`Invalid JSON: ${err.message}`); } else { errors.push(`Cannot read file: ${err instanceof Error ? err.message : 'unknown error'}`); } return { valid: false, errors }; } } /** * Validate snapshots directory structure */ function validateSnapshots(cwd: string): SnapshotValidation { const snapshotsDir = path.join(cwd, 'snapshots'); const result: SnapshotValidation = { checked: true, pages: [], plugins: [], }; // Check site_index.json const siteIndexPath = path.join(snapshotsDir, 'site_index.json'); if (fs.existsSync(siteIndexPath)) { const validation = validateSnapshotFile(siteIndexPath); result.site_index = { exists: true, valid: validation.valid, errors: validation.errors, }; } else { result.site_index = { exists: false, valid: false, errors: ['site_index.json not found'], }; } // Check pages/*.json const pagesDir = path.join(snapshotsDir, 'pages'); if (fs.existsSync(pagesDir)) { try { const files = fs.readdirSync(pagesDir).filter((f) => f.endsWith('.json')); for (const file of files) { const filePath = path.join(pagesDir, file); const validation = validateSnapshotFile(filePath); result.pages.push({ file: `snapshots/pages/${file}`, valid: validation.valid, errors: validation.errors.length > 0 ? validation.errors : undefined, }); } } catch { // Directory not readable } } // Check plugins/*.json const pluginsDir = path.join(snapshotsDir, 'plugins'); if (fs.existsSync(pluginsDir)) { try { const files = fs.readdirSync(pluginsDir).filter((f) => f.endsWith('.json')); for (const file of files) { const filePath = path.join(pluginsDir, file); const validation = validateSnapshotFile(filePath); result.plugins.push({ file: `snapshots/plugins/${file}`, valid: validation.valid, errors: validation.errors.length > 0 ? validation.errors : undefined, }); } } catch { // Directory not readable } } return result; } // ============================================================================= // Tests // ============================================================================= describe('validateSnapshotFile', () => { let testDir: string; beforeEach(() => { testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wpnav-validate-test-')); }); afterEach(() => { try { fs.rmSync(testDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } }); it('should pass for valid JSON object', () => { const filePath = path.join(testDir, 'valid.json'); fs.writeFileSync(filePath, JSON.stringify({ title: 'Test', content: 'Hello' })); const result = validateSnapshotFile(filePath); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should pass for empty JSON object', () => { const filePath = path.join(testDir, 'empty.json'); fs.writeFileSync(filePath, '{}'); const result = validateSnapshotFile(filePath); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should fail for invalid JSON', () => { const filePath = path.join(testDir, 'invalid.json'); fs.writeFileSync(filePath, '{ invalid json }'); const result = validateSnapshotFile(filePath); expect(result.valid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); expect(result.errors[0]).toContain('Invalid JSON'); }); it('should fail for non-object JSON (array)', () => { const filePath = path.join(testDir, 'array.json'); fs.writeFileSync(filePath, '[1, 2, 3]'); const result = validateSnapshotFile(filePath); expect(result.valid).toBe(false); expect(result.errors).toContain('Snapshot must be a JSON object'); }); it('should fail for non-object JSON (primitive)', () => { const filePath = path.join(testDir, 'string.json'); fs.writeFileSync(filePath, '"just a string"'); const result = validateSnapshotFile(filePath); expect(result.valid).toBe(false); expect(result.errors).toContain('Snapshot must be a JSON object'); }); it('should fail for non-existent file', () => { const filePath = path.join(testDir, 'nonexistent.json'); const result = validateSnapshotFile(filePath); expect(result.valid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); expect(result.errors[0]).toContain('Cannot read file'); }); }); describe('validateSnapshots', () => { let testDir: string; beforeEach(() => { testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wpnav-validate-test-')); }); afterEach(() => { try { fs.rmSync(testDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } }); it('should return checked: true when snapshots dir exists', () => { const snapshotsDir = path.join(testDir, 'snapshots'); fs.mkdirSync(snapshotsDir); const result = validateSnapshots(testDir); expect(result.checked).toBe(true); }); it('should validate site_index.json when present and valid', () => { const snapshotsDir = path.join(testDir, 'snapshots'); fs.mkdirSync(snapshotsDir); fs.writeFileSync(path.join(snapshotsDir, 'site_index.json'), '{"name": "Test Site"}'); const result = validateSnapshots(testDir); expect(result.site_index).toBeDefined(); expect(result.site_index!.exists).toBe(true); expect(result.site_index!.valid).toBe(true); expect(result.site_index!.errors).toHaveLength(0); }); it('should report site_index.json not found when missing', () => { const snapshotsDir = path.join(testDir, 'snapshots'); fs.mkdirSync(snapshotsDir); const result = validateSnapshots(testDir); expect(result.site_index).toBeDefined(); expect(result.site_index!.exists).toBe(false); expect(result.site_index!.valid).toBe(false); expect(result.site_index!.errors).toContain('site_index.json not found'); }); it('should report errors for invalid site_index.json', () => { const snapshotsDir = path.join(testDir, 'snapshots'); fs.mkdirSync(snapshotsDir); fs.writeFileSync(path.join(snapshotsDir, 'site_index.json'), '{ broken }'); const result = validateSnapshots(testDir); expect(result.site_index).toBeDefined(); expect(result.site_index!.exists).toBe(true); expect(result.site_index!.valid).toBe(false); expect(result.site_index!.errors.length).toBeGreaterThan(0); }); it('should validate page snapshots in pages/ directory', () => { const snapshotsDir = path.join(testDir, 'snapshots'); const pagesDir = path.join(snapshotsDir, 'pages'); fs.mkdirSync(pagesDir, { recursive: true }); fs.writeFileSync(path.join(pagesDir, 'home.json'), '{"slug": "home", "title": "Home"}'); fs.writeFileSync(path.join(pagesDir, 'about.json'), '{"slug": "about", "title": "About"}'); const result = validateSnapshots(testDir); expect(result.pages).toHaveLength(2); expect(result.pages.every((p) => p.valid)).toBe(true); }); it('should report errors for invalid page snapshots', () => { const snapshotsDir = path.join(testDir, 'snapshots'); const pagesDir = path.join(snapshotsDir, 'pages'); fs.mkdirSync(pagesDir, { recursive: true }); fs.writeFileSync(path.join(pagesDir, 'good.json'), '{"slug": "good"}'); fs.writeFileSync(path.join(pagesDir, 'bad.json'), '{ broken json }'); const result = validateSnapshots(testDir); expect(result.pages).toHaveLength(2); // Find pages by their validity status since file order may vary const validPages = result.pages.filter((p) => p.valid); const invalidPages = result.pages.filter((p) => !p.valid); expect(validPages).toHaveLength(1); expect(invalidPages).toHaveLength(1); expect(invalidPages[0].errors).toBeDefined(); expect(invalidPages[0].errors!.length).toBeGreaterThan(0); expect(invalidPages[0].file).toContain('bad.json'); }); it('should validate plugin snapshots in plugins/ directory', () => { const snapshotsDir = path.join(testDir, 'snapshots'); const pluginsDir = path.join(snapshotsDir, 'plugins'); fs.mkdirSync(pluginsDir, { recursive: true }); fs.writeFileSync(path.join(pluginsDir, 'akismet.json'), '{"name": "Akismet", "active": true}'); const result = validateSnapshots(testDir); expect(result.plugins).toHaveLength(1); expect(result.plugins[0].valid).toBe(true); expect(result.plugins[0].file).toBe('snapshots/plugins/akismet.json'); }); it('should only count .json files', () => { const snapshotsDir = path.join(testDir, 'snapshots'); const pagesDir = path.join(snapshotsDir, 'pages'); fs.mkdirSync(pagesDir, { recursive: true }); fs.writeFileSync(path.join(pagesDir, 'home.json'), '{}'); fs.writeFileSync(path.join(pagesDir, 'readme.md'), '# Readme'); fs.writeFileSync(path.join(pagesDir, 'backup.txt'), 'backup'); const result = validateSnapshots(testDir); expect(result.pages).toHaveLength(1); expect(result.pages[0].file).toContain('home.json'); }); it('should handle empty pages directory', () => { const snapshotsDir = path.join(testDir, 'snapshots'); const pagesDir = path.join(snapshotsDir, 'pages'); fs.mkdirSync(pagesDir, { recursive: true }); const result = validateSnapshots(testDir); expect(result.pages).toHaveLength(0); }); it('should handle missing pages and plugins directories', () => { const snapshotsDir = path.join(testDir, 'snapshots'); fs.mkdirSync(snapshotsDir); const result = validateSnapshots(testDir); expect(result.pages).toHaveLength(0); expect(result.plugins).toHaveLength(0); }); }); describe('exit code logic', () => { it('should prioritize manifest errors (exit 2) over snapshot errors', () => { // Test the logic that manifest errors (exit 2) take precedence const hasManifestErrors = true; const hasSnapshotErrors = true; const hasConfigErrors = false; // Per the implementation: // if (hasManifestErrors) process.exit(2); // else if (hasSnapshotErrors) process.exit(4); // else if (hasConfigErrors || strictFail) process.exit(1); let exitCode = 0; if (hasManifestErrors) { exitCode = 2; } else if (hasSnapshotErrors) { exitCode = 4; } else if (hasConfigErrors) { exitCode = 1; } expect(exitCode).toBe(2); }); it('should use exit 4 for snapshot-only errors', () => { const hasManifestErrors = false; const hasSnapshotErrors = true; const hasConfigErrors = false; let exitCode = 0; if (hasManifestErrors) { exitCode = 2; } else if (hasSnapshotErrors) { exitCode = 4; } else if (hasConfigErrors) { exitCode = 1; } expect(exitCode).toBe(4); }); it('should use exit 1 for config errors', () => { const hasManifestErrors = false; const hasSnapshotErrors = false; const hasConfigErrors = true; let exitCode = 0; if (hasManifestErrors) { exitCode = 2; } else if (hasSnapshotErrors) { exitCode = 4; } else if (hasConfigErrors) { exitCode = 1; } expect(exitCode).toBe(1); }); it('should use exit 0 for success', () => { const hasManifestErrors = false; const hasSnapshotErrors = false; const hasConfigErrors = false; let exitCode = 0; if (hasManifestErrors) { exitCode = 2; } else if (hasSnapshotErrors) { exitCode = 4; } else if (hasConfigErrors) { exitCode = 1; } expect(exitCode).toBe(0); }); it('should use exit 1 for strict mode with warnings', () => { const hasManifestErrors = false; const hasSnapshotErrors = false; const hasConfigErrors = false; const strictMode = true; const hasWarnings = true; const strictFail = strictMode && hasWarnings; let exitCode = 0; if (hasManifestErrors) { exitCode = 2; } else if (hasSnapshotErrors) { exitCode = 4; } else if (hasConfigErrors || strictFail) { exitCode = 1; } expect(exitCode).toBe(1); }); }); describe('flag parsing', () => { it('should correctly identify manifest-only mode', () => { const options: Record<string, string> = { 'manifest-only': 'true' }; const validateManifestFlag = options.manifest === 'true' || options['manifest-only'] === 'true'; const manifestOnly = options['manifest-only'] === 'true'; expect(validateManifestFlag).toBe(true); expect(manifestOnly).toBe(true); }); it('should correctly identify json output mode', () => { const options: Record<string, string> = { json: 'true' }; const isJson = options.json === 'true'; expect(isJson).toBe(true); }); it('should correctly identify strict mode', () => { const options: Record<string, string> = { strict: 'true' }; const strictMode = options.strict === 'true'; expect(strictMode).toBe(true); }); it('should correctly identify snapshots flag', () => { const options: Record<string, string> = { snapshots: 'true' }; const validateSnapshotsFlag = options.snapshots === 'true'; expect(validateSnapshotsFlag).toBe(true); }); it('should default to false when flags not provided', () => { const options: Record<string, string> = {}; const isJson = options.json === 'true'; const strictMode = options.strict === 'true'; const validateSnapshotsFlag = options.snapshots === 'true'; const manifestOnly = options['manifest-only'] === 'true'; expect(isJson).toBe(false); expect(strictMode).toBe(false); expect(validateSnapshotsFlag).toBe(false); expect(manifestOnly).toBe(false); }); });

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