Skip to main content
Glama
cleanup.test.ts14.4 kB
/** * Tests for WP Navigator Cleanup Command * * Tests file discovery, deletion, and protection logic. * 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 { findDeletableFiles, performCleanup, handleCleanup, type CleanupResult, } from './cleanup.js'; describe('Cleanup Command', () => { let tempDir: string; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wpnav-cleanup-test-')); }); afterEach(() => { // Clean up temp directory fs.rmSync(tempDir, { recursive: true, force: true }); }); describe('findDeletableFiles', () => { it('returns empty array when no onboarding files exist', () => { const result = findDeletableFiles(tempDir); expect(result).toEqual([]); }); it('finds docs/ai-onboarding-handoff.md when present', () => { const docsDir = path.join(tempDir, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); fs.writeFileSync(path.join(docsDir, 'ai-onboarding-handoff.md'), '# Handoff'); const result = findDeletableFiles(tempDir); expect(result).toContain('docs/ai-onboarding-handoff.md'); }); it('finds docs/onboarding-intro.md when present', () => { const docsDir = path.join(tempDir, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); fs.writeFileSync(path.join(docsDir, 'onboarding-intro.md'), '# Intro'); const result = findDeletableFiles(tempDir); expect(result).toContain('docs/onboarding-intro.md'); }); it('finds sample-prompts/self-test.txt when present', () => { const promptsDir = path.join(tempDir, 'sample-prompts'); fs.mkdirSync(promptsDir, { recursive: true }); fs.writeFileSync(path.join(promptsDir, 'self-test.txt'), '# Self Test'); const result = findDeletableFiles(tempDir); expect(result).toContain('sample-prompts/self-test.txt'); }); it('finds multiple onboarding files when all present', () => { // Create all deletable files const docsDir = path.join(tempDir, 'docs'); const promptsDir = path.join(tempDir, 'sample-prompts'); fs.mkdirSync(docsDir, { recursive: true }); fs.mkdirSync(promptsDir, { recursive: true }); fs.writeFileSync(path.join(docsDir, 'ai-onboarding-handoff.md'), '# Handoff'); fs.writeFileSync(path.join(docsDir, 'onboarding-intro.md'), '# Intro'); fs.writeFileSync(path.join(promptsDir, 'self-test.txt'), '# Self Test'); const result = findDeletableFiles(tempDir); expect(result).toHaveLength(3); expect(result).toContain('docs/ai-onboarding-handoff.md'); expect(result).toContain('docs/onboarding-intro.md'); expect(result).toContain('sample-prompts/self-test.txt'); }); it('does NOT find protected files', () => { // Create protected files that should NOT be found const docsDir = path.join(tempDir, 'docs'); const snapshotsDir = path.join(tempDir, 'snapshots'); const rolesDir = path.join(tempDir, 'roles'); fs.mkdirSync(docsDir, { recursive: true }); fs.mkdirSync(snapshotsDir, { recursive: true }); fs.mkdirSync(rolesDir, { recursive: true }); fs.writeFileSync(path.join(tempDir, 'wpnavigator.jsonc'), '{}'); fs.writeFileSync(path.join(tempDir, '.wpnav.env'), 'WP_BASE_URL='); fs.writeFileSync(path.join(docsDir, 'README.md'), '# README'); fs.writeFileSync(path.join(docsDir, 'ai-setup-wpnavigator.md'), '# Setup'); fs.writeFileSync(path.join(snapshotsDir, 'site_index.json'), '{}'); const result = findDeletableFiles(tempDir); expect(result).toEqual([]); // Verify protected files still 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(docsDir, 'README.md'))).toBe(true); expect(fs.existsSync(snapshotsDir)).toBe(true); expect(fs.existsSync(rolesDir)).toBe(true); }); }); describe('performCleanup', () => { it('deletes specified files successfully', () => { const docsDir = path.join(tempDir, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); fs.writeFileSync(path.join(docsDir, 'ai-onboarding-handoff.md'), '# Handoff'); const result = performCleanup(tempDir, ['docs/ai-onboarding-handoff.md']); expect(result.deleted).toContain('docs/ai-onboarding-handoff.md'); expect(result.notFound).toHaveLength(0); expect(result.errors).toHaveLength(0); expect(fs.existsSync(path.join(docsDir, 'ai-onboarding-handoff.md'))).toBe(false); }); it('reports files not found', () => { const result = performCleanup(tempDir, ['docs/nonexistent.md']); expect(result.deleted).toHaveLength(0); expect(result.notFound).toContain('docs/nonexistent.md'); expect(result.errors).toHaveLength(0); }); it('deletes multiple files', () => { const docsDir = path.join(tempDir, 'docs'); const promptsDir = path.join(tempDir, 'sample-prompts'); fs.mkdirSync(docsDir, { recursive: true }); fs.mkdirSync(promptsDir, { recursive: true }); fs.writeFileSync(path.join(docsDir, 'ai-onboarding-handoff.md'), '# Handoff'); fs.writeFileSync(path.join(promptsDir, 'self-test.txt'), '# Self Test'); const result = performCleanup(tempDir, [ 'docs/ai-onboarding-handoff.md', 'sample-prompts/self-test.txt', ]); expect(result.deleted).toHaveLength(2); expect(result.deleted).toContain('docs/ai-onboarding-handoff.md'); expect(result.deleted).toContain('sample-prompts/self-test.txt'); expect(fs.existsSync(path.join(docsDir, 'ai-onboarding-handoff.md'))).toBe(false); expect(fs.existsSync(path.join(promptsDir, 'self-test.txt'))).toBe(false); }); it('preserves protected files even if passed to performCleanup', () => { // This tests the second layer of protection - even if someone // somehow passed a protected file path, it wouldn't be in the // deletable list. This test verifies the overall safety. const docsDir = path.join(tempDir, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); fs.writeFileSync(path.join(docsDir, 'README.md'), '# Important docs'); // Note: performCleanup will delete if file exists - the protection // is in findDeletableFiles which never returns protected paths // This test confirms findDeletableFiles won't return README.md const deletable = findDeletableFiles(tempDir); expect(deletable).not.toContain('docs/README.md'); // README.md should still exist expect(fs.existsSync(path.join(docsDir, 'README.md'))).toBe(true); }); it('handles mixed success and not-found scenarios', () => { const docsDir = path.join(tempDir, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); fs.writeFileSync(path.join(docsDir, 'ai-onboarding-handoff.md'), '# Handoff'); const result = performCleanup(tempDir, [ 'docs/ai-onboarding-handoff.md', 'docs/onboarding-intro.md', // doesn't exist ]); expect(result.deleted).toContain('docs/ai-onboarding-handoff.md'); expect(result.notFound).toContain('docs/onboarding-intro.md'); }); it('returns empty result when empty list provided', () => { const result = performCleanup(tempDir, []); expect(result.deleted).toHaveLength(0); expect(result.notFound).toHaveLength(0); expect(result.errors).toHaveLength(0); }); }); describe('integration: full cleanup flow', () => { it('safely cleans up a fully scaffolded project', () => { // Set up a complete WP Navigator project structure const dirs = ['snapshots', 'snapshots/pages', 'roles', 'docs', 'sample-prompts']; for (const dir of dirs) { fs.mkdirSync(path.join(tempDir, dir), { recursive: true }); } // Create protected files fs.writeFileSync(path.join(tempDir, 'wpnavigator.jsonc'), '{}'); fs.writeFileSync(path.join(tempDir, '.wpnav.env'), 'WP_BASE_URL=test'); fs.writeFileSync(path.join(tempDir, 'docs', 'README.md'), '# Project README'); fs.writeFileSync(path.join(tempDir, 'snapshots', 'site_index.json'), '{"version":1}'); fs.writeFileSync(path.join(tempDir, 'roles', 'custom-role.yaml'), 'name: test'); // Create deletable onboarding files fs.writeFileSync(path.join(tempDir, 'docs', 'ai-onboarding-handoff.md'), '# Handoff'); fs.writeFileSync(path.join(tempDir, 'docs', 'onboarding-intro.md'), '# Intro'); fs.writeFileSync(path.join(tempDir, 'sample-prompts', 'self-test.txt'), '# Self Test'); // Also create other sample prompts that should NOT be deleted fs.writeFileSync(path.join(tempDir, 'sample-prompts', 'page-builder.txt'), '# Page Builder'); // Find deletable files const deletable = findDeletableFiles(tempDir); expect(deletable).toHaveLength(3); // Perform cleanup const result = performCleanup(tempDir, deletable); // Verify onboarding files were deleted expect(result.deleted).toHaveLength(3); expect(fs.existsSync(path.join(tempDir, 'docs', 'ai-onboarding-handoff.md'))).toBe(false); expect(fs.existsSync(path.join(tempDir, 'docs', 'onboarding-intro.md'))).toBe(false); expect(fs.existsSync(path.join(tempDir, 'sample-prompts', 'self-test.txt'))).toBe(false); // Verify protected files still 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, 'docs', 'README.md'))).toBe(true); expect(fs.existsSync(path.join(tempDir, 'snapshots', 'site_index.json'))).toBe(true); expect(fs.existsSync(path.join(tempDir, 'roles', 'custom-role.yaml'))).toBe(true); // Verify other sample prompts still exist expect(fs.existsSync(path.join(tempDir, 'sample-prompts', 'page-builder.txt'))).toBe(true); // Verify directories still exist expect(fs.existsSync(path.join(tempDir, 'snapshots'))).toBe(true); expect(fs.existsSync(path.join(tempDir, 'roles'))).toBe(true); expect(fs.existsSync(path.join(tempDir, 'docs'))).toBe(true); expect(fs.existsSync(path.join(tempDir, 'sample-prompts'))).toBe(true); }); it('handles project with no onboarding files', () => { // Set up a project without onboarding files const dirs = ['snapshots', 'roles', 'docs']; for (const dir of dirs) { fs.mkdirSync(path.join(tempDir, dir), { recursive: true }); } fs.writeFileSync(path.join(tempDir, 'wpnavigator.jsonc'), '{}'); fs.writeFileSync(path.join(tempDir, 'docs', 'README.md'), '# README'); // Find deletable files - should be empty const deletable = findDeletableFiles(tempDir); expect(deletable).toHaveLength(0); // Perform cleanup - should succeed with nothing to do const result = performCleanup(tempDir, deletable); expect(result.deleted).toHaveLength(0); expect(result.notFound).toHaveLength(0); expect(result.errors).toHaveLength(0); }); }); describe('handleCleanup with JSON output', () => { let consoleLogSpy: ReturnType<typeof vi.spyOn>; let consoleErrorSpy: ReturnType<typeof vi.spyOn>; let processCwdSpy: ReturnType<typeof vi.spyOn>; beforeEach(() => { consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); processCwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tempDir); }); afterEach(() => { consoleLogSpy.mockRestore(); consoleErrorSpy.mockRestore(); processCwdSpy.mockRestore(); }); it('returns JSON with success true when no files to clean', async () => { const exitCode = await handleCleanup({ json: true, yes: true }); expect(exitCode).toBe(0); expect(consoleLogSpy).toHaveBeenCalled(); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output).toEqual({ success: true, command: 'cleanup', data: { deleted: [], not_found: [], message: 'No onboarding files found', }, }); }); it('returns JSON with deleted files list on success', async () => { // Create deletable files const docsDir = path.join(tempDir, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); fs.writeFileSync(path.join(docsDir, 'ai-onboarding-handoff.md'), '# Handoff'); const exitCode = await handleCleanup({ json: true, yes: true }); expect(exitCode).toBe(0); const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); expect(output.success).toBe(true); expect(output.command).toBe('cleanup'); expect(output.data.deleted).toContain('docs/ai-onboarding-handoff.md'); }); it('requires --yes flag in JSON mode', async () => { // Create a file to trigger the confirmation check const docsDir = path.join(tempDir, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); fs.writeFileSync(path.join(docsDir, 'ai-onboarding-handoff.md'), '# Handoff'); const exitCode = await handleCleanup({ json: true }); // no yes flag 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('CONFIRMATION_REQUIRED'); }); it('suppresses TUI output in JSON mode', async () => { // Create deletable files const docsDir = path.join(tempDir, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); fs.writeFileSync(path.join(docsDir, 'ai-onboarding-handoff.md'), '# Handoff'); await handleCleanup({ json: true, yes: true }); // Console.error (TUI output) should not be called with cleanup messages // JSON output goes to console.log expect(consoleLogSpy).toHaveBeenCalledTimes(1); }); }); });

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