Skip to main content
Glama
manifest.test.ts19.5 kB
/** * Tests for wpnavigator.jsonc manifest schema and loader * * @package WP_Navigator_MCP */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import { MANIFEST_SCHEMA_VERSION, MIN_MANIFEST_VERSION, CURRENT_SCHEMA_VERSION, stripJsonComments, parseJsonc, validateManifest, loadManifest, ManifestValidationError, SchemaVersionError, getBrandPalette, getBrandFonts, getBrandLayout, getManifestSafety, getBackupReminders, DEFAULT_BRAND_PALETTE, DEFAULT_BRAND_FONTS, DEFAULT_BRAND_LAYOUT, DEFAULT_MANIFEST_SAFETY, DEFAULT_BACKUP_REMINDERS, type WPNavManifest, } from './manifest.js'; // Mock fs module vi.mock('fs'); const mockFs = vi.mocked(fs); describe('manifest', () => { beforeEach(() => { vi.resetAllMocks(); }); describe('CURRENT_SCHEMA_VERSION', () => { it('should be defined as integer', () => { expect(CURRENT_SCHEMA_VERSION).toBe(1); expect(Number.isInteger(CURRENT_SCHEMA_VERSION)).toBe(true); }); it('should be >= 1', () => { expect(CURRENT_SCHEMA_VERSION).toBeGreaterThanOrEqual(1); }); }); describe('MANIFEST_SCHEMA_VERSION (legacy)', () => { it('should be defined', () => { expect(MANIFEST_SCHEMA_VERSION).toBe('1.0'); }); it('should be >= MIN_MANIFEST_VERSION', () => { const current = MANIFEST_SCHEMA_VERSION.split('.').map(Number); const min = MIN_MANIFEST_VERSION.split('.').map(Number); expect(current[0]).toBeGreaterThanOrEqual(min[0]); }); }); describe('stripJsonComments', () => { it('should strip single-line comments', () => { const input = `{ "key": "value" // this is a comment }`; const result = stripJsonComments(input); expect(result).not.toContain('//'); expect(result).not.toContain('this is a comment'); expect(result).toContain('"key": "value"'); }); it('should strip multi-line comments', () => { const input = `{ /* this is a multi-line comment */ "key": "value" }`; const result = stripJsonComments(input); expect(result).not.toContain('/*'); expect(result).not.toContain('*/'); expect(result).not.toContain('multi-line comment'); expect(result).toContain('"key": "value"'); }); it('should preserve strings containing comment-like sequences', () => { const input = `{ "url": "https://example.com", "comment": "this has // in it", "other": "and /* this */ too" }`; const result = stripJsonComments(input); expect(result).toContain('"https://example.com"'); expect(result).toContain('"this has // in it"'); expect(result).toContain('"and /* this */ too"'); }); it('should handle escaped quotes in strings', () => { const input = `{ "quote": "he said \\"hello\\"" // comment }`; const result = stripJsonComments(input); expect(result).toContain('"he said \\"hello\\""'); expect(result).not.toContain('// comment'); }); it('should handle empty input', () => { expect(stripJsonComments('')).toBe(''); }); it('should handle input with no comments', () => { const input = '{"key": "value"}'; expect(stripJsonComments(input)).toBe(input); }); it('should handle multiple comments', () => { const input = `{ // comment 1 "a": 1, // inline comment /* block comment */ "b": 2 }`; const result = stripJsonComments(input); expect(result).toContain('"a": 1,'); expect(result).toContain('"b": 2'); expect(result).not.toContain('comment 1'); expect(result).not.toContain('inline comment'); expect(result).not.toContain('block'); }); }); describe('parseJsonc', () => { it('should parse valid JSONC', () => { const input = `{ // Site manifest "manifest_version": "1.0", "meta": { "name": "Test Site" /* inline */ } }`; const result = parseJsonc<WPNavManifest>(input); expect(result.manifest_version).toBe('1.0'); expect(result.meta.name).toBe('Test Site'); }); it('should throw on invalid JSON after stripping comments', () => { const input = '{ invalid json }'; expect(() => parseJsonc(input)).toThrow(); }); }); describe('validateManifest', () => { const validManifest: WPNavManifest = { schema_version: 1, manifest_version: '1.0', meta: { name: 'Test Site', }, }; it('should validate a minimal manifest', () => { const result = validateManifest(validManifest, '/test/manifest.jsonc'); expect(result.schema_version).toBe(1); expect(result.manifest_version).toBe('1.0'); expect(result.meta.name).toBe('Test Site'); }); it('should throw for non-object input', () => { expect(() => validateManifest('string', '/test')).toThrow(ManifestValidationError); expect(() => validateManifest(null, '/test')).toThrow(ManifestValidationError); expect(() => validateManifest([], '/test')).toThrow(ManifestValidationError); }); // schema_version tests it('should throw SchemaVersionError for missing schema_version', () => { const manifest = { manifest_version: '1.0', meta: { name: 'Test' } }; expect(() => validateManifest(manifest, '/test')).toThrow(SchemaVersionError); expect(() => validateManifest(manifest, '/test')).toThrow(/schema_version is missing/); }); it('should throw SchemaVersionError for non-integer schema_version', () => { const manifest = { schema_version: '1', manifest_version: '1.0', meta: { name: 'Test' } }; expect(() => validateManifest(manifest, '/test')).toThrow(SchemaVersionError); expect(() => validateManifest(manifest, '/test')).toThrow(/expected integer/); }); it('should throw SchemaVersionError for unsupported schema_version', () => { const manifest = { schema_version: 99, manifest_version: '1.0', meta: { name: 'Test' } }; expect(() => validateManifest(manifest, '/test')).toThrow(SchemaVersionError); expect(() => validateManifest(manifest, '/test')).toThrow(/Unsupported manifest schema_version: 99/); }); it('should include upgrade instructions in SchemaVersionError', () => { const manifest = { schema_version: 2, manifest_version: '1.0', meta: { name: 'Test' } }; try { validateManifest(manifest, '/test'); expect.fail('Should have thrown'); } catch (e) { expect(e).toBeInstanceOf(SchemaVersionError); const err = e as SchemaVersionError; expect(err.upgradeInstructions).toContain('Update the wpnav CLI'); expect(err.exitCode).toBe(2); } }); it('should throw for schema_version < 1', () => { const manifest = { schema_version: 0, manifest_version: '1.0', meta: { name: 'Test' } }; expect(() => validateManifest(manifest, '/test')).toThrow(SchemaVersionError); expect(() => validateManifest(manifest, '/test')).toThrow(/must be >= 1/); }); // legacy manifest_version tests it('should throw for missing manifest_version', () => { const manifest = { schema_version: 1, meta: { name: 'Test' } }; expect(() => validateManifest(manifest, '/test')).toThrow(/manifest_version/); }); it('should throw for invalid manifest_version format', () => { const manifest = { schema_version: 1, manifest_version: 'invalid', meta: { name: 'Test' } }; expect(() => validateManifest(manifest, '/test')).toThrow(/Invalid manifest_version format/); }); it('should throw for too old manifest_version', () => { const manifest = { schema_version: 1, manifest_version: '0.1', meta: { name: 'Test' } }; expect(() => validateManifest(manifest, '/test')).toThrow(/older than minimum/); }); it('should throw for too new manifest_version', () => { const manifest = { schema_version: 1, manifest_version: '99.0', meta: { name: 'Test' } }; expect(() => validateManifest(manifest, '/test')).toThrow(/newer than supported/); }); // meta tests it('should throw for missing meta', () => { const manifest = { schema_version: 1, manifest_version: '1.0' }; expect(() => validateManifest(manifest, '/test')).toThrow(/meta/); }); it('should throw for missing meta.name', () => { const manifest = { schema_version: 1, manifest_version: '1.0', meta: {} }; expect(() => validateManifest(manifest, '/test')).toThrow(/meta.name/); }); it('should validate pages array', () => { const manifest = { ...validManifest, pages: [ { slug: 'about', title: 'About Us' }, { slug: 'contact', title: 'Contact' }, ], }; const result = validateManifest(manifest, '/test'); expect(result.pages).toHaveLength(2); }); it('should throw for pages not being an array', () => { const manifest = { ...validManifest, pages: 'not array' }; expect(() => validateManifest(manifest, '/test')).toThrow(/pages must be an array/); }); it('should throw for page missing slug', () => { const manifest = { ...validManifest, pages: [{ title: 'No Slug' }] }; expect(() => validateManifest(manifest, '/test')).toThrow(/pages\[0\].*slug/); }); it('should throw for page missing title', () => { const manifest = { ...validManifest, pages: [{ slug: 'no-title' }] }; expect(() => validateManifest(manifest, '/test')).toThrow(/pages\[0\].*title/); }); it('should validate plugins object', () => { const manifest = { ...validManifest, plugins: { woocommerce: { enabled: true }, 'yoast-seo': { enabled: false, settings: { sitemap: true } }, }, }; const result = validateManifest(manifest, '/test'); expect(result.plugins?.woocommerce.enabled).toBe(true); expect(result.plugins?.['yoast-seo'].enabled).toBe(false); }); it('should throw for plugins not being an object', () => { const manifest = { ...validManifest, plugins: [] }; expect(() => validateManifest(manifest, '/test')).toThrow(/plugins must be an object/); }); it('should throw for plugin missing enabled field', () => { const manifest = { ...validManifest, plugins: { woo: { settings: {} } } }; expect(() => validateManifest(manifest, '/test')).toThrow(/plugins\.woo\.enabled/); }); it('should throw for plugin enabled not being boolean', () => { const manifest = { ...validManifest, plugins: { woo: { enabled: 'yes' } } }; expect(() => validateManifest(manifest, '/test')).toThrow(/plugins\.woo\.enabled must be a boolean/); }); it('should validate full manifest with all sections', () => { const fullManifest: WPNavManifest = { schema_version: 1, manifest_version: '1.0', meta: { name: 'Full Test Site', description: 'A complete test', url: 'https://example.com', tags: ['test', 'example'], }, brand: { palette: { primary: '#ff0000' }, fonts: { heading: 'Roboto' }, layout: { spacing: 'compact' }, voice: { tone: 'friendly' }, }, pages: [ { slug: 'home', title: 'Home', status: 'publish' }, ], plugins: { woocommerce: { enabled: true }, }, safety: { allow_create_pages: true, require_confirmation: false, }, }; const result = validateManifest(fullManifest, '/test'); expect(result.brand?.palette?.primary).toBe('#ff0000'); expect(result.safety?.require_confirmation).toBe(false); }); }); describe('loadManifest', () => { const validManifestContent = JSON.stringify({ schema_version: 1, manifest_version: '1.0', meta: { name: 'Test Site' }, }); it('should load wpnavigator.jsonc from project root', () => { mockFs.existsSync.mockImplementation((p) => { return String(p).endsWith('wpnavigator.jsonc'); }); mockFs.readFileSync.mockReturnValue(validManifestContent); const result = loadManifest('/test/project'); expect(result.found).toBe(true); expect(result.manifest?.meta.name).toBe('Test Site'); expect(result.path).toContain('wpnavigator.jsonc'); }); it('should load wpnavigator.json if .jsonc not found', () => { mockFs.existsSync.mockImplementation((p) => { return String(p).endsWith('wpnavigator.json'); }); mockFs.readFileSync.mockReturnValue(validManifestContent); const result = loadManifest('/test/project'); expect(result.found).toBe(true); expect(result.path).toContain('wpnavigator.json'); }); it('should return found=false if no manifest exists', () => { mockFs.existsSync.mockReturnValue(false); const result = loadManifest('/test/project'); expect(result.found).toBe(false); expect(result.manifest).toBeUndefined(); expect(result.error).toBeUndefined(); }); it('should return error for invalid JSON', () => { mockFs.existsSync.mockReturnValue(true); mockFs.readFileSync.mockReturnValue('{ invalid json }'); const result = loadManifest('/test/project'); expect(result.found).toBe(true); expect(result.manifest).toBeUndefined(); expect(result.error).toContain('Invalid JSON'); }); it('should return error for validation failures (missing schema_version)', () => { mockFs.existsSync.mockReturnValue(true); mockFs.readFileSync.mockReturnValue(JSON.stringify({ manifest_version: '1.0', meta: { name: 'Test' } })); const result = loadManifest('/test/project'); expect(result.found).toBe(true); expect(result.manifest).toBeUndefined(); expect(result.error).toContain('schema_version'); }); it('should parse JSONC with comments', () => { const jsoncContent = `{ // Site manifest "schema_version": 1, "manifest_version": "1.0", "meta": { "name": "Test Site" /* with comment */ } }`; mockFs.existsSync.mockReturnValue(true); mockFs.readFileSync.mockReturnValue(jsoncContent); const result = loadManifest('/test/project'); expect(result.found).toBe(true); expect(result.manifest?.meta.name).toBe('Test Site'); }); it('should handle file read errors', () => { mockFs.existsSync.mockReturnValue(true); mockFs.readFileSync.mockImplementation(() => { throw new Error('Permission denied'); }); const result = loadManifest('/test/project'); expect(result.found).toBe(true); expect(result.error).toContain('Permission denied'); }); }); describe('default value helpers', () => { describe('getBrandPalette', () => { it('should return defaults when no brand provided', () => { const result = getBrandPalette(undefined); expect(result).toEqual(DEFAULT_BRAND_PALETTE); }); it('should merge with defaults', () => { const brand = { palette: { primary: '#custom' } }; const result = getBrandPalette(brand); expect(result.primary).toBe('#custom'); expect(result.secondary).toBe(DEFAULT_BRAND_PALETTE.secondary); }); }); describe('getBrandFonts', () => { it('should return defaults when no brand provided', () => { const result = getBrandFonts(undefined); expect(result).toEqual(DEFAULT_BRAND_FONTS); }); it('should merge with defaults', () => { const brand = { fonts: { heading: 'Custom Font' } }; const result = getBrandFonts(brand); expect(result.heading).toBe('Custom Font'); expect(result.body).toBe(DEFAULT_BRAND_FONTS.body); }); }); describe('getBrandLayout', () => { it('should return defaults when no brand provided', () => { const result = getBrandLayout(undefined); expect(result).toEqual(DEFAULT_BRAND_LAYOUT); }); it('should merge with defaults', () => { const brand = { layout: { spacing: 'compact' as const } }; const result = getBrandLayout(brand); expect(result.spacing).toBe('compact'); expect(result.containerWidth).toBe(DEFAULT_BRAND_LAYOUT.containerWidth); }); }); describe('getManifestSafety', () => { it('should return defaults when no manifest provided', () => { const result = getManifestSafety(undefined); expect(result).toEqual(DEFAULT_MANIFEST_SAFETY); }); it('should merge with defaults', () => { const manifest: WPNavManifest = { schema_version: 1, manifest_version: '1.0', meta: { name: 'Test' }, safety: { allow_delete_pages: true }, }; const result = getManifestSafety(manifest); expect(result.allow_delete_pages).toBe(true); expect(result.require_confirmation).toBe(DEFAULT_MANIFEST_SAFETY.require_confirmation); }); it('should include new safety fields', () => { const result = getManifestSafety(undefined); expect(result.require_sync_confirmation).toBe(true); expect(result.first_sync_acknowledged).toBe(false); expect(result.backup_reminders).toBeDefined(); }); it('should deep merge backup_reminders', () => { const manifest: WPNavManifest = { schema_version: 1, manifest_version: '1.0', meta: { name: 'Test' }, safety: { backup_reminders: { frequency: 'always' }, }, }; const result = getManifestSafety(manifest); expect(result.backup_reminders.frequency).toBe('always'); expect(result.backup_reminders.enabled).toBe(DEFAULT_BACKUP_REMINDERS.enabled); }); }); describe('getBackupReminders', () => { it('should return defaults when no manifest provided', () => { const result = getBackupReminders(undefined); expect(result).toEqual(DEFAULT_BACKUP_REMINDERS); }); it('should merge with defaults', () => { const manifest: WPNavManifest = { schema_version: 1, manifest_version: '1.0', meta: { name: 'Test' }, safety: { backup_reminders: { enabled: false, frequency: 'never' }, }, }; const result = getBackupReminders(manifest); expect(result.enabled).toBe(false); expect(result.frequency).toBe('never'); expect(result.before_sync).toBe(DEFAULT_BACKUP_REMINDERS.before_sync); }); }); }); describe('safety validation', () => { it('should accept valid backup_reminders.frequency values', () => { const validFrequencies = ['first_sync_only', 'always', 'daily', 'never']; for (const frequency of validFrequencies) { const manifest = { schema_version: 1, manifest_version: '1.0', meta: { name: 'Test' }, safety: { backup_reminders: { frequency } }, }; expect(() => validateManifest(manifest, '/test')).not.toThrow(); } }); it('should reject invalid backup_reminders.frequency', () => { const manifest = { schema_version: 1, manifest_version: '1.0', meta: { name: 'Test' }, safety: { backup_reminders: { frequency: 'invalid' } }, }; expect(() => validateManifest(manifest, '/test')).toThrow(/backup_reminders\.frequency/); }); }); });

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