Skip to main content
Glama
init.test.ts39.8 kB
/** * Tests for WP Navigator Init Command * * Tests scaffold functionality, file generation, and validation. * Note: Interactive TUI prompts are not tested here (requires manual testing). * * @package WP_Navigator_MCP * @since 1.1.0 */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { buildHandoffState, generateAIHandoff, writeHandoffFile, generateSelfTestPrompt, handleInit, type HandoffState, } from './init.js'; // Test scaffold-related functions directly from init module // Since the module exports handleInit as default, we need to test internal functions // For this test, we'll create a test directory and verify scaffold behavior describe('Init Command', () => { let tempDir: string; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wpnav-init-test-')); }); afterEach(() => { // Clean up temp directory fs.rmSync(tempDir, { recursive: true, force: true }); }); describe('scaffold structure', () => { it('creates expected directories', () => { // Simulate scaffold by creating directories const dirs = ['snapshots', 'snapshots/pages', 'roles', 'docs', 'sample-prompts']; for (const dir of dirs) { const dirPath = path.join(tempDir, dir); fs.mkdirSync(dirPath, { recursive: true }); expect(fs.existsSync(dirPath)).toBe(true); expect(fs.statSync(dirPath).isDirectory()).toBe(true); } }); it('creates wpnavigator.jsonc with valid structure', () => { // Create a minimal manifest template const manifestContent = `{ // WP Navigator Site Manifest "$schema": "https://wpnav.ai/schemas/wpnavigator.schema.json", "schema_version": 1, "site": { "name": "", "tagline": "", "url": "" }, "brand": {}, "pages": [], "plugins": {} } `; const manifestPath = path.join(tempDir, 'wpnavigator.jsonc'); fs.writeFileSync(manifestPath, manifestContent); expect(fs.existsSync(manifestPath)).toBe(true); // Verify it's parseable (remove comments for JSON.parse) const content = fs.readFileSync(manifestPath, 'utf8'); expect(content).toContain('schema_version'); expect(content).toContain('"site"'); }); it('creates .gitignore with .wpnav.env', () => { const gitignoreContent = `# WP Navigator .wpnav.env wp-config.json `; const gitignorePath = path.join(tempDir, '.gitignore'); fs.writeFileSync(gitignorePath, gitignoreContent); expect(fs.existsSync(gitignorePath)).toBe(true); const content = fs.readFileSync(gitignorePath, 'utf8'); expect(content).toContain('.wpnav.env'); }); it('creates docs/README.md with instructions', () => { const docsDir = path.join(tempDir, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); const readmeContent = `# WP Navigator Project ## How WP Navigator Works 1. **Your WordPress site** – Where your real content lives. 2. **This project folder** – Stores snapshots and configuration. 3. **Your AI assistant** – Reads these files and helps plan changes. `; const readmePath = path.join(docsDir, 'README.md'); fs.writeFileSync(readmePath, readmeContent); expect(fs.existsSync(readmePath)).toBe(true); const content = fs.readFileSync(readmePath, 'utf8'); expect(content).toContain('WP Navigator'); expect(content).toContain('WordPress'); }); }); describe('sample prompts', () => { it('creates self-test.txt prompt', () => { const promptsDir = path.join(tempDir, 'sample-prompts'); fs.mkdirSync(promptsDir, { recursive: true }); const promptContent = `# WP Navigator Self-Test Prompt Use this prompt to verify your WP Navigator setup is working correctly. Please perform these verification steps: 1. Validate wpnavigator.jsonc 2. Check snapshots 3. Review project structure `; const promptPath = path.join(promptsDir, 'self-test.txt'); fs.writeFileSync(promptPath, promptContent); expect(fs.existsSync(promptPath)).toBe(true); const content = fs.readFileSync(promptPath, 'utf8'); expect(content).toContain('Self-Test'); expect(content).toContain('Validate'); }); it('creates page-builder.txt prompt', () => { const promptsDir = path.join(tempDir, 'sample-prompts'); fs.mkdirSync(promptsDir, { recursive: true }); const promptContent = `# Page Builder Prompt Use this prompt to have AI help you design and modify WordPress pages. `; const promptPath = path.join(promptsDir, 'page-builder.txt'); fs.writeFileSync(promptPath, promptContent); expect(fs.existsSync(promptPath)).toBe(true); }); it('creates all expected prompt files', () => { const promptsDir = path.join(tempDir, 'sample-prompts'); fs.mkdirSync(promptsDir, { recursive: true }); const expectedPrompts = [ 'self-test.txt', 'page-builder.txt', 'manifest-refinement.txt', 'seo.txt', ]; for (const filename of expectedPrompts) { const promptPath = path.join(promptsDir, filename); fs.writeFileSync(promptPath, `# ${filename}\n\nPrompt content here.`); expect(fs.existsSync(promptPath)).toBe(true); } const files = fs.readdirSync(promptsDir); expect(files).toHaveLength(4); }); }); describe('AI handoff', () => { it('generates ai-onboarding-handoff.md with completed/pending steps', () => { const docsDir = path.join(tempDir, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); const handoffContent = `# WP Navigator AI Onboarding Handoff ## Instructions for AI Assistant ## Completed Steps - [x] Created project structure ## Pending Steps - [ ] Configure WordPress connection - [ ] Fill out site details - [ ] Generate first snapshot `; const handoffPath = path.join(docsDir, 'ai-onboarding-handoff.md'); fs.writeFileSync(handoffPath, handoffContent); expect(fs.existsSync(handoffPath)).toBe(true); const content = fs.readFileSync(handoffPath, 'utf8'); expect(content).toContain('Completed Steps'); expect(content).toContain('Pending Steps'); expect(content).toContain('[x]'); expect(content).toContain('[ ]'); }); }); describe('smart URL validation', () => { // Test URL validation logic it('auto-adds https:// to URLs without protocol', () => { const url = 'example.com'; const corrected = url.startsWith('http') ? url : `https://${url}`; expect(corrected).toBe('https://example.com'); }); it('preserves http:// for localhost', () => { const url = 'http://localhost:8080'; expect(url.startsWith('http://')).toBe(true); }); it('removes trailing slashes', () => { const url = 'https://example.com/'; const corrected = url.replace(/\/+$/, ''); expect(corrected).toBe('https://example.com'); }); it('detects URLs without TLD', () => { const url: string = 'myserver'; const hasTLD = url.includes('.') || url === 'localhost'; expect(hasTLD).toBe(false); }); it('accepts localhost without TLD', () => { const url = 'localhost'; const isLocalhost = url === 'localhost'; expect(isLocalhost).toBe(true); }); }); describe('app password validation', () => { it('accepts standard WordPress app password format', () => { const password = 'xxxx xxxx xxxx xxxx xxxx xxxx'; const cleaned = password.replace(/\s/g, ''); expect(cleaned.length).toBeGreaterThanOrEqual(16); expect(/^[a-zA-Z0-9]+$/.test(cleaned)).toBe(true); }); it('rejects passwords that are too short', () => { const password = 'abc123'; const cleaned = password.replace(/\s/g, ''); expect(cleaned.length).toBeLessThan(16); }); it('accepts passwords without spaces', () => { const password = 'abcd1234efgh5678ijkl9012'; const cleaned = password.replace(/\s/g, ''); expect(cleaned.length).toBeGreaterThanOrEqual(16); }); }); describe('.wpnav.env generation', () => { it('generates valid .wpnav.env content', () => { const siteUrl = 'https://example.com'; const username = 'admin'; const password = 'xxxx xxxx xxxx xxxx'; const envContent = `# WP Navigator Connection Settings WP_BASE_URL=${siteUrl} WP_REST_API=${siteUrl}/wp-json WPNAV_BASE=${siteUrl}/wp-json/wpnav/v1 WPNAV_INTROSPECT=${siteUrl}/wp-json/wpnav/v1/introspect WP_APP_USER=${username} WP_APP_PASS=${password} `; expect(envContent).toContain('WP_BASE_URL=https://example.com'); expect(envContent).toContain('WP_APP_USER=admin'); expect(envContent).toContain('WP_APP_PASS='); expect(envContent).toContain('/wp-json/wpnav/v1'); }); it('writes .wpnav.env with secure permissions', () => { const envPath = path.join(tempDir, '.wpnav.env'); const content = 'WP_APP_PASS=secret'; // Write with mode 0o600 (read/write for owner only) fs.writeFileSync(envPath, content, { mode: 0o600 }); const stats = fs.statSync(envPath); // Check that only owner has read/write (on Unix systems) if (process.platform !== 'win32') { expect(stats.mode & 0o777).toBe(0o600); } }); }); describe('MCP configuration', () => { it('generates valid MCP server config', () => { const mcpConfig = { mcpServers: { wpnav: { command: 'npx', args: ['-y', '@littlebearapps/wp-navigator-mcp'], env: { WPNAV_CONFIG: './.wpnav.env', }, }, }, }; const jsonStr = JSON.stringify(mcpConfig, null, 2); expect(jsonStr).toContain('"wpnav"'); expect(jsonStr).toContain('@littlebearapps/wp-navigator-mcp'); expect(jsonStr).toContain('WPNAV_CONFIG'); }); it('creates docs/ai-setup-wpnavigator.md guide', () => { const docsDir = path.join(tempDir, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); const guideContent = `# Setting Up WP Navigator MCP for AI Assistants ## What is MCP? MCP (Model Context Protocol) lets AI assistants call WP Navigator tools directly. ## Setup for Claude Code Add this to your Claude Code MCP settings: `; const guidePath = path.join(docsDir, 'ai-setup-wpnavigator.md'); fs.writeFileSync(guidePath, guideContent); expect(fs.existsSync(guidePath)).toBe(true); const content = fs.readFileSync(guidePath, 'utf8'); expect(content).toContain('MCP'); expect(content).toContain('Claude Code'); }); }); describe('skip existing files', () => { it('does not overwrite existing wpnavigator.jsonc', () => { const manifestPath = path.join(tempDir, 'wpnavigator.jsonc'); const originalContent = '{"existing": true}'; fs.writeFileSync(manifestPath, originalContent); // Simulate scaffold check const exists = fs.existsSync(manifestPath); expect(exists).toBe(true); // Read should return original content const content = fs.readFileSync(manifestPath, 'utf8'); expect(content).toBe(originalContent); }); it('appends to existing .gitignore if .wpnav.env not present', () => { const gitignorePath = path.join(tempDir, '.gitignore'); const originalContent = 'node_modules/\n'; fs.writeFileSync(gitignorePath, originalContent); // Check if .wpnav.env is already ignored const content = fs.readFileSync(gitignorePath, 'utf8'); const hasWpnavEnv = content.includes('.wpnav.env'); if (!hasWpnavEnv) { // Append fs.appendFileSync(gitignorePath, '\n.wpnav.env\n'); } const updatedContent = fs.readFileSync(gitignorePath, 'utf8'); expect(updatedContent).toContain('node_modules/'); expect(updatedContent).toContain('.wpnav.env'); }); }); // ========================================================================= // AI Handoff State & Generation Tests // ========================================================================= describe('buildHandoffState', () => { it('detects empty project (no files)', () => { const state = buildHandoffState(tempDir); expect(state.projectScaffolded).toBe(false); expect(state.manifestExists).toBe(false); expect(state.manifestConfigured).toBe(false); expect(state.credentialsExist).toBe(false); expect(state.connectionTested).toBe(false); expect(state.snapshotsExist).toBe(false); expect(state.mcpConfigured).toBe(false); }); it('detects scaffolded project', () => { // Create minimum scaffold structure fs.mkdirSync(path.join(tempDir, 'docs'), { recursive: true }); fs.mkdirSync(path.join(tempDir, 'snapshots'), { recursive: true }); const state = buildHandoffState(tempDir); expect(state.projectScaffolded).toBe(true); }); it('detects manifest exists but not configured', () => { const manifestPath = path.join(tempDir, 'wpnavigator.jsonc'); fs.writeFileSync(manifestPath, JSON.stringify({ site: { name: '', tagline: '', url: '' } })); const state = buildHandoffState(tempDir); expect(state.manifestExists).toBe(true); expect(state.manifestConfigured).toBe(false); }); it('detects manifest is configured when site.name is filled', () => { const manifestPath = path.join(tempDir, 'wpnavigator.jsonc'); fs.writeFileSync( manifestPath, JSON.stringify({ site: { name: 'My Site', tagline: '', url: '' } }) ); const state = buildHandoffState(tempDir); expect(state.manifestExists).toBe(true); expect(state.manifestConfigured).toBe(true); }); it('detects credentials exist', () => { const envPath = path.join(tempDir, '.wpnav.env'); fs.writeFileSync(envPath, 'WP_BASE_URL=https://example.com'); const state = buildHandoffState(tempDir); expect(state.credentialsExist).toBe(true); }); it('detects snapshots exist', () => { const snapshotsDir = path.join(tempDir, 'snapshots'); fs.mkdirSync(snapshotsDir, { recursive: true }); fs.writeFileSync(path.join(snapshotsDir, 'site_index.json'), JSON.stringify({ pages: [] })); const state = buildHandoffState(tempDir); expect(state.snapshotsExist).toBe(true); }); it('detects connection tested (credentials + snapshots)', () => { fs.mkdirSync(path.join(tempDir, 'snapshots'), { recursive: true }); fs.writeFileSync(path.join(tempDir, '.wpnav.env'), 'WP_BASE_URL=https://example.com'); fs.writeFileSync( path.join(tempDir, 'snapshots', 'site_index.json'), JSON.stringify({ pages: [] }) ); const state = buildHandoffState(tempDir); expect(state.connectionTested).toBe(true); }); it('detects MCP configured', () => { const mcpPath = path.join(tempDir, 'mcp-config.json'); fs.writeFileSync(mcpPath, JSON.stringify({ mcpServers: {} })); const state = buildHandoffState(tempDir); expect(state.mcpConfigured).toBe(true); }); it('detects fully configured project', () => { // Create full project structure fs.mkdirSync(path.join(tempDir, 'docs'), { recursive: true }); fs.mkdirSync(path.join(tempDir, 'snapshots'), { recursive: true }); fs.writeFileSync( path.join(tempDir, 'wpnavigator.jsonc'), JSON.stringify({ site: { name: 'My Site', url: 'https://example.com' } }) ); fs.writeFileSync(path.join(tempDir, '.wpnav.env'), 'WP_BASE_URL=https://example.com'); fs.writeFileSync( path.join(tempDir, 'snapshots', 'site_index.json'), JSON.stringify({ pages: [] }) ); fs.writeFileSync(path.join(tempDir, 'mcp-config.json'), '{}'); const state = buildHandoffState(tempDir); expect(state.projectScaffolded).toBe(true); expect(state.manifestExists).toBe(true); expect(state.manifestConfigured).toBe(true); expect(state.credentialsExist).toBe(true); expect(state.connectionTested).toBe(true); expect(state.snapshotsExist).toBe(true); expect(state.mcpConfigured).toBe(true); }); }); describe('generateAIHandoff', () => { it('includes all 5 required sections', () => { const state: HandoffState = { projectScaffolded: true, manifestExists: true, manifestConfigured: false, credentialsExist: false, connectionTested: false, snapshotsExist: false, mcpConfigured: false, }; const content = generateAIHandoff(state); // Section A: Setup Progress expect(content).toContain('## Section A: Setup Progress'); expect(content).toContain('[x] Project structure scaffolded'); expect(content).toContain('[ ] WordPress credentials saved'); // Section B: Key Project Files expect(content).toContain('## Section B: Key Project Files'); expect(content).toContain('wpnavigator.jsonc'); expect(content).toContain('.wpnav.env'); expect(content).toContain('snapshots/site_index.json'); // Section C: Next Steps expect(content).toContain('## Section C: Next Steps for AI'); // Section D: AI Instructions expect(content).toContain('## Section D: Instructions for AI Assistant'); expect(content).toContain('### You MAY:'); expect(content).toContain('### You MUST NOT:'); // Section E: Safety Notes expect(content).toContain('## Section E: Safety Notes'); expect(content).toContain('Snapshots are read-only'); }); it('uses [x] for completed steps and [ ] for pending', () => { const state: HandoffState = { projectScaffolded: true, manifestExists: true, manifestConfigured: true, credentialsExist: true, connectionTested: false, snapshotsExist: false, mcpConfigured: false, }; const content = generateAIHandoff(state); // Check completed items have [x] expect(content).toMatch(/\[x\] Project structure scaffolded/); expect(content).toMatch(/\[x\] wpnavigator\.jsonc created/); expect(content).toMatch(/\[x\] Site details configured in manifest/); expect(content).toMatch(/\[x\] WordPress credentials saved/); // Check pending items have [ ] expect(content).toMatch(/\[ \] WordPress connection tested/); expect(content).toMatch(/\[ \] First snapshot generated/); }); it('generates context-aware next steps when credentials missing', () => { const state: HandoffState = { projectScaffolded: true, manifestExists: true, manifestConfigured: false, credentialsExist: false, connectionTested: false, snapshotsExist: false, mcpConfigured: false, }; const content = generateAIHandoff(state); expect(content).toContain('Ask user for WordPress URL, username, and Application Password'); expect(content).toContain('npx wpnav configure'); }); it('generates context-aware next steps when credentials exist but no snapshot', () => { const state: HandoffState = { projectScaffolded: true, manifestExists: true, manifestConfigured: false, credentialsExist: true, connectionTested: false, snapshotsExist: false, mcpConfigured: false, }; const content = generateAIHandoff(state); expect(content).toContain('npx wpnav status'); expect(content).toContain('npx wpnav snapshot site'); }); it('generates advanced next steps when setup is complete', () => { const state: HandoffState = { projectScaffolded: true, manifestExists: true, manifestConfigured: true, credentialsExist: true, connectionTested: true, snapshotsExist: true, mcpConfigured: false, }; const content = generateAIHandoff(state); expect(content).toContain('npx wpnav validate'); expect(content).toContain('npx wpnav diff'); expect(content).toContain('npx wpnav sync --dry-run'); }); it('includes timestamp', () => { const state: HandoffState = { projectScaffolded: false, manifestExists: false, manifestConfigured: false, credentialsExist: false, connectionTested: false, snapshotsExist: false, mcpConfigured: false, }; const content = generateAIHandoff(state); // Should have ISO timestamp expect(content).toMatch(/Generated:.*\d{4}-\d{2}-\d{2}T/); // Should have human-readable timestamp expect(content).toContain('Last Updated:'); }); it('never contains credentials or secrets', () => { const state: HandoffState = { projectScaffolded: true, manifestExists: true, manifestConfigured: true, credentialsExist: true, connectionTested: true, snapshotsExist: true, mcpConfigured: true, }; const content = generateAIHandoff(state); // Should not contain any password-like patterns expect(content).not.toMatch(/password\s*[:=]\s*\S+/i); expect(content).not.toMatch(/WP_APP_PASS/); expect(content).not.toMatch(/secret/i); expect(content).not.toMatch(/xxxx/); // Should explicitly state not to share credentials expect(content).toContain('Credentials are local-only'); }); it('includes MAY and MUST NOT instructions', () => { const state: HandoffState = { projectScaffolded: true, manifestExists: true, manifestConfigured: false, credentialsExist: false, connectionTested: false, snapshotsExist: false, mcpConfigured: false, }; const content = generateAIHandoff(state); // MAY instructions expect(content).toContain('Edit `wpnavigator.jsonc`'); expect(content).toContain('Suggest CLI commands'); expect(content).toContain('Help interpret snapshot JSON'); // MUST NOT instructions expect(content).toContain("Access the user's WordPress site directly"); expect(content).toContain('Modify `.wpnav.env`'); expect(content).toContain('destructive changes without explicit user consent'); }); it('includes file status indicators in Section B', () => { const statePartial: HandoffState = { projectScaffolded: true, manifestExists: true, manifestConfigured: false, credentialsExist: false, connectionTested: false, snapshotsExist: false, mcpConfigured: false, }; const content = generateAIHandoff(statePartial); // Check status indicators expect(content).toMatch(/wpnavigator\.jsonc.*✓ exists/); expect(content).toMatch(/\.wpnav\.env.*✗ missing/); expect(content).toMatch(/site_index\.json.*✗ missing/); expect(content).toMatch(/mcp-config\.json.*○ optional/); }); it('includes regeneration instruction', () => { const state: HandoffState = { projectScaffolded: true, manifestExists: true, manifestConfigured: false, credentialsExist: false, connectionTested: false, snapshotsExist: false, mcpConfigured: false, }; const content = generateAIHandoff(state); expect(content).toContain('npx wpnav init --mode ai-handoff'); }); }); describe('writeHandoffFile', () => { it('creates docs directory if it does not exist', () => { const result = writeHandoffFile(tempDir); expect(result).toBe(true); expect(fs.existsSync(path.join(tempDir, 'docs'))).toBe(true); }); it('writes ai-onboarding-handoff.md to docs/', () => { const result = writeHandoffFile(tempDir); expect(result).toBe(true); const handoffPath = path.join(tempDir, 'docs', 'ai-onboarding-handoff.md'); expect(fs.existsSync(handoffPath)).toBe(true); const content = fs.readFileSync(handoffPath, 'utf8'); expect(content).toContain('# WP Navigator AI Onboarding Handoff'); }); it('regenerates file with updated state', () => { // First write with empty state writeHandoffFile(tempDir); const firstContent = fs.readFileSync( path.join(tempDir, 'docs', 'ai-onboarding-handoff.md'), 'utf8' ); expect(firstContent).toMatch(/\[ \] Project structure scaffolded/); // Create scaffold structure fs.mkdirSync(path.join(tempDir, 'snapshots'), { recursive: true }); // Write again - should reflect new state writeHandoffFile(tempDir); const secondContent = fs.readFileSync( path.join(tempDir, 'docs', 'ai-onboarding-handoff.md'), 'utf8' ); expect(secondContent).toMatch(/\[x\] Project structure scaffolded/); }); it('file content is safe to commit (no secrets)', () => { // Create credentials file fs.writeFileSync( path.join(tempDir, '.wpnav.env'), 'WP_APP_USER=admin\nWP_APP_PASS=supersecretpassword123' ); writeHandoffFile(tempDir); const content = fs.readFileSync( path.join(tempDir, 'docs', 'ai-onboarding-handoff.md'), 'utf8' ); // Should NOT contain actual credentials expect(content).not.toContain('supersecretpassword123'); expect(content).not.toContain('admin'); expect(content).not.toContain('WP_APP_PASS='); }); }); // ========================================================================= // Self-Test Prompt Generation Tests (task-59) // ========================================================================= describe('generateSelfTestPrompt', () => { it('includes clear AI instructions at the top', () => { const content = generateSelfTestPrompt(); // AC#8: Clear instructions for AI at top of file expect(content).toContain('## Instructions for AI Assistant'); expect(content).toContain('This is a comprehensive self-test prompt'); expect(content).toContain('**Your role:**'); expect(content).toContain('**Output format:**'); }); it('validates wpnavigator.jsonc structure (AC#2)', () => { const content = generateSelfTestPrompt(); // AC#2: Prompt validates wpnavigator.jsonc structure expect(content).toContain('## Step 1: Validate wpnavigator.jsonc'); expect(content).toContain('File exists and is readable'); expect(content).toContain('File is valid JSONC'); expect(content).toContain('`schema_version` field is present'); expect(content).toContain('`$schema` reference is present'); }); it('validates site section fields', () => { const content = generateSelfTestPrompt(); expect(content).toContain('### 1.2 Site Section'); expect(content).toContain('`site.name` is filled in'); expect(content).toContain('`site.tagline` is present'); expect(content).toContain('`site.url` is a valid URL'); }); it('validates snapshots/site_index.json exists (AC#3)', () => { const content = generateSelfTestPrompt(); // AC#3: Prompt validates snapshots/site_index.json exists expect(content).toContain('## Step 2: Validate Snapshots'); expect(content).toContain('### 2.1 Site Index'); expect(content).toContain('`snapshots/site_index.json`'); expect(content).toContain('File exists'); expect(content).toContain('File is valid JSON'); expect(content).toContain('Contains `pages` array'); }); it('validates page snapshots exist (AC#4)', () => { const content = generateSelfTestPrompt(); // AC#4: Prompt validates page snapshots exist expect(content).toContain('### 2.2 Page Snapshots'); expect(content).toContain('`snapshots/pages/`'); expect(content).toContain('Directory exists'); expect(content).toContain('Contains at least one `.json` file'); expect(content).toContain('page `id`, `slug`, and `title`'); }); it('identifies missing manifest fields (AC#5)', () => { const content = generateSelfTestPrompt(); // AC#5: Prompt identifies missing manifest fields (brand/style/pages/plugins) expect(content).toContain('## Step 3: Identify Missing Fields'); expect(content).toContain('### Missing Brand/Style Fields'); expect(content).toContain('Primary color'); expect(content).toContain('Secondary color'); expect(content).toContain('Heading font'); expect(content).toContain('Body font'); expect(content).toContain('### Missing Page Definitions'); expect(content).toContain('### Missing Plugin Configuration'); }); it('proposes non-destructive improvements (AC#6)', () => { const content = generateSelfTestPrompt(); // AC#6: Prompt proposes non-destructive improvements expect(content).toContain('## Step 4: Propose Non-Destructive Improvements'); expect(content).toContain('### Manifest Enhancements'); expect(content).toContain('MINIMAL edits'); expect(content).toContain("Only suggest changes that won't break anything"); expect(content).toContain('Never suggest removing existing configuration'); }); it('requests dry-run plan for wpnav sync (AC#7)', () => { const content = generateSelfTestPrompt(); // AC#7: Prompt requests dry-run plan for wpnav sync expect(content).toContain('## Step 5: Dry-Run Plan for wpnav sync'); expect(content).toContain('### Pre-Sync Checklist'); expect(content).toContain('### Recommended Commands'); expect(content).toContain('npx wpnav validate'); expect(content).toContain('npx wpnav diff'); expect(content).toContain('npx wpnav sync --dry-run'); expect(content).toContain('### After Dry-Run Review'); }); it('provides summary template', () => { const content = generateSelfTestPrompt(); expect(content).toContain('## Summary Template'); expect(content).toContain('### WP Navigator Self-Test Results'); expect(content).toContain('**Overall Status:**'); expect(content).toContain('**Validation Summary:**'); expect(content).toContain('**Critical Issues'); expect(content).toContain('**Recommended Improvements'); expect(content).toContain('**Next Steps:**'); }); it('includes AI notes at the end', () => { const content = generateSelfTestPrompt(); expect(content).toContain('## Notes for AI'); expect(content).toContain('Be thorough but concise'); expect(content).toContain('`wpnav sync --dry-run` is ALWAYS safe to run'); expect(content).toContain('Never suggest running `wpnav sync` without `--dry-run` first'); }); it('is human-readable and AI-friendly (AC#10)', () => { const content = generateSelfTestPrompt(); // AC#10: File is human-readable and AI-friendly // Human-readable: has clear section headers with === expect(content).toContain( '================================================================================' ); // Has numbered steps expect(content).toContain('## Step 1:'); expect(content).toContain('## Step 2:'); expect(content).toContain('## Step 3:'); expect(content).toContain('## Step 4:'); expect(content).toContain('## Step 5:'); // Has checkboxes for tracking expect(content).toContain('- [ ]'); // Has emoji status indicators expect(content).toContain('✅'); expect(content).toContain('❌'); expect(content).toContain('⚠️'); // Has code blocks for commands expect(content).toContain('```bash'); expect(content).toContain('```'); }); it('includes cross-reference validation', () => { const content = generateSelfTestPrompt(); expect(content).toContain('### 2.3 Cross-Reference'); expect(content).toContain('Pages in `site_index.json` have corresponding snapshots'); }); it('validates brand section optional fields', () => { const content = generateSelfTestPrompt(); expect(content).toContain('### 1.3 Brand Section'); expect(content).toContain('Optional but Recommended'); expect(content).toContain('`brand.primary_color`'); expect(content).toContain('`brand.secondary_color`'); expect(content).toContain('`brand.heading_font`'); expect(content).toContain('`brand.body_font`'); }); it('validates pages section', () => { const content = generateSelfTestPrompt(); expect(content).toContain('### 1.4 Pages Section'); expect(content).toContain('`pages` array exists'); expect(content).toContain('Each page has `slug` and `title`'); }); it('validates plugins section', () => { const content = generateSelfTestPrompt(); expect(content).toContain('### 1.5 Plugins Section'); expect(content).toContain('`plugins` object exists'); expect(content).toContain('valid `status` values'); }); }); }); describe('handleInit with JSON output', () => { let tempDir: string; let consoleLogSpy: ReturnType<typeof vi.spyOn>; let consoleErrorSpy: ReturnType<typeof vi.spyOn>; let processCwdSpy: ReturnType<typeof vi.spyOn>; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wpnav-init-json-test-')); consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); processCwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tempDir); }); afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }); consoleLogSpy.mockRestore(); consoleErrorSpy.mockRestore(); processCwdSpy.mockRestore(); }); it('returns JSON with scaffold mode files list', async () => { const exitCode = await handleInit({ json: true, mode: 'scaffold' }); expect(exitCode).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('init'); expect(output.data.mode).toBe('scaffold'); expect(output.data.files_created).toBeInstanceOf(Array); expect(output.data.files_created.length).toBeGreaterThan(0); }); it('returns JSON with ai-handoff mode', async () => { const exitCode = await handleInit({ json: true, mode: 'ai-handoff' }); expect(exitCode).toBe(0); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(true); expect(output.data.mode).toBe('ai-handoff'); expect(output.data.files_created).toContain('docs/ai-onboarding-handoff.md'); }); it('requires credentials for guided mode in JSON', async () => { const exitCode = await handleInit({ json: true, mode: 'guided' }); expect(exitCode).toBe(1); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(false); expect(output.error.code).toBe('MISSING_CREDENTIALS'); }); it('creates files with credentials in guided mode', async () => { const exitCode = await handleInit({ json: true, mode: 'guided', siteUrl: 'https://example.com', username: 'admin', password: 'test1234', skipSmokeTest: true, }); expect(exitCode).toBe(0); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(true); expect(output.data.mode).toBe('guided'); expect(output.data.files_created).toContain('.wpnav.env'); expect(output.data.connection.site_url).toBe('https://example.com'); // Verify .wpnav.env was actually created expect(fs.existsSync(path.join(tempDir, '.wpnav.env'))).toBe(true); }); it('suppresses TUI output in JSON mode', async () => { await handleInit({ json: true, mode: 'scaffold' }); // Only one console.log call for JSON output expect(consoleLogSpy).toHaveBeenCalledTimes(1); }); it('returns exit code 0 on success', async () => { const exitCode = await handleInit({ json: true, mode: 'scaffold' }); expect(exitCode).toBe(0); }); it('returns exit code 1 on error', async () => { const exitCode = await handleInit({ json: true, mode: 'guided' }); // missing credentials expect(exitCode).toBe(1); }); }); describe('handleInit with express mode', () => { let tempDir: string; let consoleLogSpy: ReturnType<typeof vi.spyOn>; let consoleErrorSpy: ReturnType<typeof vi.spyOn>; let processCwdSpy: ReturnType<typeof vi.spyOn>; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wpnav-init-express-test-')); consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); processCwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tempDir); }); afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }); consoleLogSpy.mockRestore(); consoleErrorSpy.mockRestore(); processCwdSpy.mockRestore(); }); it('requires --site, --user, --password flags', async () => { const exitCode = await handleInit({ express: true }); expect(exitCode).toBe(1); // Should show error message via TUI expect(consoleErrorSpy).toHaveBeenCalled(); }); it('creates project structure with credentials', async () => { const exitCode = await handleInit({ express: true, siteUrl: 'https://example.com', username: 'admin', password: 'test1234', skipSmokeTest: true, }); expect(exitCode).toBe(0); // Verify files created expect(fs.existsSync(path.join(tempDir, 'wpnavigator.jsonc'))).toBe(true); expect(fs.existsSync(path.join(tempDir, '.wpnav.env'))).toBe(true); expect(fs.existsSync(path.join(tempDir, 'snapshots'))).toBe(true); expect(fs.existsSync(path.join(tempDir, 'roles'))).toBe(true); }); it('saves credentials to .wpnav.env', async () => { await handleInit({ express: true, siteUrl: 'https://example.com', username: 'admin', password: 'test1234', skipSmokeTest: true, }); const envContent = fs.readFileSync(path.join(tempDir, '.wpnav.env'), 'utf8'); expect(envContent).toContain('WP_BASE_URL=https://example.com'); expect(envContent).toContain('WP_APP_USER=admin'); expect(envContent).toContain('WP_APP_PASS=test1234'); }); it('detects local environment for localhost URLs', async () => { await handleInit({ express: true, siteUrl: 'http://localhost:8080', username: 'admin', password: 'test1234', skipSmokeTest: true, }); // Should mention "local development" in TUI output const allCalls = consoleErrorSpy.mock.calls.flat().join(' '); expect(allCalls).toContain('local'); }); it('generates same files as guided mode', async () => { const expressResult = await handleInit({ express: true, siteUrl: 'https://example.com', username: 'admin', password: 'test1234', skipSmokeTest: true, }); expect(expressResult).toBe(0); // Check core files exist expect(fs.existsSync(path.join(tempDir, 'wpnavigator.jsonc'))).toBe(true); expect(fs.existsSync(path.join(tempDir, '.wpnav.env'))).toBe(true); expect(fs.existsSync(path.join(tempDir, '.gitignore'))).toBe(true); expect(fs.existsSync(path.join(tempDir, '.mcp.json'))).toBe(true); expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).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