Skip to main content
Glama
rollback.test.ts21.2 kB
/** * Tests for Sync Rollback Module * * @package WP_Navigator_MCP */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import { generateSyncId, getPreSyncDir, getPreSyncSnapshotPath, createPreSyncSnapshot, savePreSyncSnapshot, listPreSyncSnapshots, loadPreSyncSnapshot, executeRollback, deletePreSyncSnapshot, cleanupOldSnapshots, formatRollbackText, formatRollbackJson, type PreSyncSnapshot, type RollbackResult, } from './rollback.js'; import type { DiffResult, WordPressPage, WordPressPlugin } from './diff.js'; // Test directory for snapshot files const TEST_DIR = '/tmp/wpnav-test-rollback'; describe('Rollback', () => { beforeEach(() => { // Clean up test directory if (fs.existsSync(TEST_DIR)) { fs.rmSync(TEST_DIR, { recursive: true }); } fs.mkdirSync(TEST_DIR, { recursive: true }); }); afterEach(() => { // Clean up test directory if (fs.existsSync(TEST_DIR)) { fs.rmSync(TEST_DIR, { recursive: true }); } }); describe('generateSyncId', () => { it('should generate unique sync IDs', () => { const id1 = generateSyncId(); const id2 = generateSyncId(); expect(id1).not.toBe(id2); }); it('should generate IDs with timestamp format', () => { const id = generateSyncId(); // Should match: YYYY-MM-DDTHH-MM-SS-xxxx expect(id).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-[a-z0-9]{4}$/); }); }); describe('getPreSyncDir', () => { it('should return correct pre-sync directory path', () => { const dir = getPreSyncDir('/project'); expect(dir).toBe('/project/snapshots/pre-sync'); }); }); describe('getPreSyncSnapshotPath', () => { it('should return correct snapshot file path', () => { const syncId = '2024-01-01T12-00-00-abcd'; const snapshotPath = getPreSyncSnapshotPath('/project', syncId); expect(snapshotPath).toBe('/project/snapshots/pre-sync/pre-sync-2024-01-01T12-00-00-abcd.json'); }); }); describe('createPreSyncSnapshot', () => { it('should create snapshot with page updates', () => { const diff: DiffResult = { timestamp: new Date().toISOString(), summary: { additions: 0, removals: 0, modifications: 1, matches: 0, total: 1, hasDifferences: true }, pages: [{ slug: 'about', title: 'About Us', change: 'modify', severity: 'info', inManifest: true, inWordPress: true, wpId: 42, fields: [{ field: 'title', expected: 'About Us', actual: 'Old Title' }], }], plugins: [], }; const wpPages: WordPressPage[] = [{ id: 42, slug: 'about', title: 'Old Title', status: 'publish', template: 'page-about.php', parent: 0, menu_order: 1, }]; const snapshot = createPreSyncSnapshot(diff, wpPages, [], 'test-sync-id'); expect(snapshot.sync_id).toBe('test-sync-id'); expect(snapshot.pages).toHaveLength(1); expect(snapshot.pages[0].wpId).toBe(42); expect(snapshot.pages[0].title).toBe('Old Title'); expect(snapshot.pages[0].planned_operation).toBe('update'); expect(snapshot.planned_operations.page_updates).toBe(1); }); it('should create snapshot with page additions', () => { const diff: DiffResult = { timestamp: new Date().toISOString(), summary: { additions: 1, removals: 0, modifications: 0, matches: 0, total: 1, hasDifferences: true }, pages: [{ slug: 'new-page', title: 'New Page', change: 'add', severity: 'warning', inManifest: true, inWordPress: false, }], plugins: [], }; const snapshot = createPreSyncSnapshot(diff, [], [], 'test-sync-id'); expect(snapshot.pages).toHaveLength(1); expect(snapshot.pages[0].slug).toBe('new-page'); expect(snapshot.pages[0].planned_operation).toBe('create'); expect(snapshot.planned_operations.page_creates).toBe(1); }); it('should create snapshot with page deletions', () => { const diff: DiffResult = { timestamp: new Date().toISOString(), summary: { additions: 0, removals: 1, modifications: 0, matches: 0, total: 1, hasDifferences: true }, pages: [{ slug: 'old-page', title: 'Old Page', change: 'remove', severity: 'warning', inManifest: false, inWordPress: true, wpId: 99, }], plugins: [], }; const wpPages: WordPressPage[] = [{ id: 99, slug: 'old-page', title: 'Old Page', status: 'publish', template: '', parent: 0, menu_order: 0, }]; const snapshot = createPreSyncSnapshot(diff, wpPages, [], 'test-sync-id'); expect(snapshot.pages).toHaveLength(1); expect(snapshot.pages[0].wpId).toBe(99); expect(snapshot.pages[0].planned_operation).toBe('delete'); expect(snapshot.planned_operations.page_deletes).toBe(1); }); it('should create snapshot with plugin state changes', () => { const diff: DiffResult = { timestamp: new Date().toISOString(), summary: { additions: 0, removals: 0, modifications: 1, matches: 0, total: 1, hasDifferences: true }, pages: [], plugins: [{ slug: 'akismet', name: 'Akismet', change: 'modify', severity: 'info', inManifest: true, isActive: false, expectedEnabled: true, }], }; const wpPlugins: WordPressPlugin[] = [{ slug: 'akismet', name: 'Akismet', active: false, version: '5.0.0', }]; const snapshot = createPreSyncSnapshot(diff, [], wpPlugins, 'test-sync-id'); expect(snapshot.plugins).toHaveLength(1); expect(snapshot.plugins[0].slug).toBe('akismet'); expect(snapshot.plugins[0].wasActive).toBe(false); expect(snapshot.plugins[0].planned_operation).toBe('activate'); expect(snapshot.planned_operations.plugin_activations).toBe(1); }); it('should skip matched items', () => { const diff: DiffResult = { timestamp: new Date().toISOString(), summary: { additions: 0, removals: 0, modifications: 0, matches: 1, total: 1, hasDifferences: false }, pages: [{ slug: 'home', title: 'Home', change: 'match', severity: 'info', inManifest: true, inWordPress: true, wpId: 1, }], plugins: [], }; const snapshot = createPreSyncSnapshot(diff, [], [], 'test-sync-id'); expect(snapshot.pages).toHaveLength(0); expect(snapshot.plugins).toHaveLength(0); }); }); describe('savePreSyncSnapshot', () => { it('should save snapshot to file', () => { const snapshot: PreSyncSnapshot = { snapshot_version: '1.0', captured_at: new Date().toISOString(), sync_id: 'test-sync-123', pages: [], plugins: [], planned_operations: { page_creates: 0, page_updates: 0, page_deletes: 0, plugin_activations: 0, plugin_deactivations: 0, }, }; const savedPath = savePreSyncSnapshot(TEST_DIR, snapshot); expect(fs.existsSync(savedPath)).toBe(true); const content = JSON.parse(fs.readFileSync(savedPath, 'utf-8')); expect(content.sync_id).toBe('test-sync-123'); }); it('should create pre-sync directory if not exists', () => { const preSyncDir = getPreSyncDir(TEST_DIR); expect(fs.existsSync(preSyncDir)).toBe(false); const snapshot: PreSyncSnapshot = { snapshot_version: '1.0', captured_at: new Date().toISOString(), sync_id: 'test-sync-456', pages: [], plugins: [], planned_operations: { page_creates: 0, page_updates: 0, page_deletes: 0, plugin_activations: 0, plugin_deactivations: 0, }, }; savePreSyncSnapshot(TEST_DIR, snapshot); expect(fs.existsSync(preSyncDir)).toBe(true); }); }); describe('listPreSyncSnapshots', () => { it('should return empty array for non-existent directory', () => { const snapshots = listPreSyncSnapshots('/nonexistent/path'); expect(snapshots).toEqual([]); }); it('should list available snapshots', () => { // Create some snapshot files const preSyncDir = getPreSyncDir(TEST_DIR); fs.mkdirSync(preSyncDir, { recursive: true }); const snapshot1: PreSyncSnapshot = { snapshot_version: '1.0', captured_at: '2024-01-01T10:00:00.000Z', sync_id: 'sync-1', pages: [{ wpId: 1, slug: 'a', title: 'A', status: 'publish', template: '', parent: 0, menu_order: 0, planned_operation: 'update' }], plugins: [], planned_operations: { page_creates: 0, page_updates: 1, page_deletes: 0, plugin_activations: 0, plugin_deactivations: 0 }, }; const snapshot2: PreSyncSnapshot = { snapshot_version: '1.0', captured_at: '2024-01-02T10:00:00.000Z', sync_id: 'sync-2', pages: [], plugins: [{ slug: 'test', name: 'Test', wasActive: true, planned_operation: 'deactivate' }], planned_operations: { page_creates: 0, page_updates: 0, page_deletes: 0, plugin_activations: 0, plugin_deactivations: 1 }, }; fs.writeFileSync(path.join(preSyncDir, 'pre-sync-sync-1.json'), JSON.stringify(snapshot1)); fs.writeFileSync(path.join(preSyncDir, 'pre-sync-sync-2.json'), JSON.stringify(snapshot2)); const snapshots = listPreSyncSnapshots(TEST_DIR); expect(snapshots).toHaveLength(2); // Should be sorted by date, most recent first expect(snapshots[0].syncId).toBe('sync-2'); expect(snapshots[1].syncId).toBe('sync-1'); expect(snapshots[0].summary.plugins).toBe(1); expect(snapshots[1].summary.pages).toBe(1); }); it('should skip invalid files', () => { const preSyncDir = getPreSyncDir(TEST_DIR); fs.mkdirSync(preSyncDir, { recursive: true }); // Create invalid files fs.writeFileSync(path.join(preSyncDir, 'pre-sync-invalid.json'), 'not json'); fs.writeFileSync(path.join(preSyncDir, 'other-file.txt'), 'random content'); const snapshots = listPreSyncSnapshots(TEST_DIR); expect(snapshots).toHaveLength(0); }); }); describe('loadPreSyncSnapshot', () => { it('should load snapshot by sync ID', () => { const preSyncDir = getPreSyncDir(TEST_DIR); fs.mkdirSync(preSyncDir, { recursive: true }); const snapshot: PreSyncSnapshot = { snapshot_version: '1.0', captured_at: new Date().toISOString(), sync_id: 'test-load', pages: [], plugins: [], planned_operations: { page_creates: 0, page_updates: 0, page_deletes: 0, plugin_activations: 0, plugin_deactivations: 0 }, }; fs.writeFileSync(path.join(preSyncDir, 'pre-sync-test-load.json'), JSON.stringify(snapshot)); const loaded = loadPreSyncSnapshot(TEST_DIR, 'test-load'); expect(loaded).not.toBeNull(); expect(loaded?.sync_id).toBe('test-load'); }); it('should return null for non-existent snapshot', () => { const loaded = loadPreSyncSnapshot(TEST_DIR, 'nonexistent'); expect(loaded).toBeNull(); }); }); describe('executeRollback', () => { it('should restore plugin state', async () => { const snapshot: PreSyncSnapshot = { snapshot_version: '1.0', captured_at: new Date().toISOString(), sync_id: 'test-rollback', pages: [], plugins: [{ slug: 'akismet', name: 'Akismet', wasActive: true, planned_operation: 'deactivate', }], planned_operations: { page_creates: 0, page_updates: 0, page_deletes: 0, plugin_activations: 0, plugin_deactivations: 1 }, }; const wpRequest = vi.fn().mockResolvedValue({}); const result = await executeRollback(snapshot, wpRequest); expect(result.success).toBe(true); expect(result.pluginsRestored).toBe(1); expect(wpRequest).toHaveBeenCalledWith( '/wp/v2/plugins/akismet%2Fakismet.php', expect.objectContaining({ method: 'POST', body: JSON.stringify({ status: 'active' }), }) ); }); it('should restore page updates', async () => { const snapshot: PreSyncSnapshot = { snapshot_version: '1.0', captured_at: new Date().toISOString(), sync_id: 'test-rollback', pages: [{ wpId: 42, slug: 'about', title: 'Original Title', status: 'publish', template: 'page-about.php', parent: 0, menu_order: 1, planned_operation: 'update', }], plugins: [], planned_operations: { page_creates: 0, page_updates: 1, page_deletes: 0, plugin_activations: 0, plugin_deactivations: 0 }, }; const wpRequest = vi.fn().mockResolvedValue({}); const result = await executeRollback(snapshot, wpRequest); expect(result.success).toBe(true); expect(result.pagesRestored).toBe(1); expect(wpRequest).toHaveBeenCalledWith( '/wp/v2/pages/42', expect.objectContaining({ method: 'POST', body: expect.stringContaining('Original Title'), }) ); }); it('should handle rollback of created pages by deleting them', async () => { const snapshot: PreSyncSnapshot = { snapshot_version: '1.0', captured_at: new Date().toISOString(), sync_id: 'test-rollback', pages: [{ wpId: 0, slug: 'new-page', title: 'New Page', status: 'draft', template: '', parent: 0, menu_order: 0, planned_operation: 'create', }], plugins: [], planned_operations: { page_creates: 1, page_updates: 0, page_deletes: 0, plugin_activations: 0, plugin_deactivations: 0 }, }; const wpRequest = vi.fn() .mockResolvedValueOnce([{ id: 123 }]) // GET to find page by slug .mockResolvedValueOnce({}); // DELETE const result = await executeRollback(snapshot, wpRequest); expect(result.success).toBe(true); expect(result.pagesRestored).toBe(1); expect(wpRequest).toHaveBeenCalledWith( expect.stringContaining('/wp/v2/pages?slug=new-page'), ); expect(wpRequest).toHaveBeenCalledWith( '/wp/v2/pages/123?force=true', { method: 'DELETE' } ); }); it('should support dry run mode', async () => { const snapshot: PreSyncSnapshot = { snapshot_version: '1.0', captured_at: new Date().toISOString(), sync_id: 'test-rollback', pages: [{ wpId: 42, slug: 'about', title: 'Original', status: 'publish', template: '', parent: 0, menu_order: 0, planned_operation: 'update', }], plugins: [], planned_operations: { page_creates: 0, page_updates: 1, page_deletes: 0, plugin_activations: 0, plugin_deactivations: 0 }, }; const wpRequest = vi.fn(); const result = await executeRollback(snapshot, wpRequest, { dryRun: true }); expect(result.pagesRestored).toBe(1); expect(wpRequest).not.toHaveBeenCalled(); }); it('should collect errors without stopping', async () => { const snapshot: PreSyncSnapshot = { snapshot_version: '1.0', captured_at: new Date().toISOString(), sync_id: 'test-rollback', pages: [ { wpId: 1, slug: 'good', title: 'Good', status: 'publish', template: '', parent: 0, menu_order: 0, planned_operation: 'update' }, { wpId: 2, slug: 'bad', title: 'Bad', status: 'publish', template: '', parent: 0, menu_order: 0, planned_operation: 'update' }, ], plugins: [], planned_operations: { page_creates: 0, page_updates: 2, page_deletes: 0, plugin_activations: 0, plugin_deactivations: 0 }, }; const wpRequest = vi.fn() .mockResolvedValueOnce({}) .mockRejectedValueOnce(new Error('Network error')); const result = await executeRollback(snapshot, wpRequest); expect(result.success).toBe(false); expect(result.pagesRestored).toBe(1); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain('Network error'); }); }); describe('deletePreSyncSnapshot', () => { it('should delete snapshot file', () => { const preSyncDir = getPreSyncDir(TEST_DIR); fs.mkdirSync(preSyncDir, { recursive: true }); const filePath = path.join(preSyncDir, 'pre-sync-to-delete.json'); fs.writeFileSync(filePath, '{}'); expect(fs.existsSync(filePath)).toBe(true); const result = deletePreSyncSnapshot(TEST_DIR, 'to-delete'); expect(result).toBe(true); expect(fs.existsSync(filePath)).toBe(false); }); it('should return false for non-existent file', () => { const result = deletePreSyncSnapshot(TEST_DIR, 'nonexistent'); expect(result).toBe(false); }); }); describe('cleanupOldSnapshots', () => { it('should keep most recent N snapshots', () => { const preSyncDir = getPreSyncDir(TEST_DIR); fs.mkdirSync(preSyncDir, { recursive: true }); // Create 5 snapshots with different dates for (let i = 1; i <= 5; i++) { const snapshot: PreSyncSnapshot = { snapshot_version: '1.0', captured_at: new Date(2024, 0, i).toISOString(), sync_id: `sync-${i}`, pages: [], plugins: [], planned_operations: { page_creates: 0, page_updates: 0, page_deletes: 0, plugin_activations: 0, plugin_deactivations: 0 }, }; fs.writeFileSync(path.join(preSyncDir, `pre-sync-sync-${i}.json`), JSON.stringify(snapshot)); } expect(listPreSyncSnapshots(TEST_DIR)).toHaveLength(5); const deleted = cleanupOldSnapshots(TEST_DIR, 3); expect(deleted).toBe(2); expect(listPreSyncSnapshots(TEST_DIR)).toHaveLength(3); }); it('should do nothing if fewer than keepCount snapshots', () => { const preSyncDir = getPreSyncDir(TEST_DIR); fs.mkdirSync(preSyncDir, { recursive: true }); const snapshot: PreSyncSnapshot = { snapshot_version: '1.0', captured_at: new Date().toISOString(), sync_id: 'only-one', pages: [], plugins: [], planned_operations: { page_creates: 0, page_updates: 0, page_deletes: 0, plugin_activations: 0, plugin_deactivations: 0 }, }; fs.writeFileSync(path.join(preSyncDir, 'pre-sync-only-one.json'), JSON.stringify(snapshot)); const deleted = cleanupOldSnapshots(TEST_DIR, 10); expect(deleted).toBe(0); }); }); describe('formatRollbackText', () => { it('should format successful rollback', () => { const result: RollbackResult = { success: true, timestamp: new Date().toISOString(), pagesRestored: 2, pluginsRestored: 1, errors: [], }; const text = formatRollbackText(result); expect(text).toContain('Rollback Results'); expect(text).toContain('2 page(s) restored'); expect(text).toContain('1 plugin(s) restored'); expect(text).toContain('Rollback completed successfully'); }); it('should format dry run rollback', () => { const result: RollbackResult = { success: true, timestamp: new Date().toISOString(), pagesRestored: 3, pluginsRestored: 0, errors: [], }; const text = formatRollbackText(result, true); expect(text).toContain('Dry Run'); expect(text).toContain('3 resource(s) would be restored'); }); it('should format rollback with errors', () => { const result: RollbackResult = { success: false, timestamp: new Date().toISOString(), pagesRestored: 1, pluginsRestored: 0, errors: ['Failed to restore page', 'Network error'], }; const text = formatRollbackText(result); expect(text).toContain('Errors:'); expect(text).toContain('Failed to restore page'); expect(text).toContain('Network error'); expect(text).toContain('completed with errors'); }); }); describe('formatRollbackJson', () => { it('should output valid JSON', () => { const result: RollbackResult = { success: true, timestamp: '2024-01-01T00:00:00.000Z', pagesRestored: 1, pluginsRestored: 1, errors: [], }; const json = formatRollbackJson(result); const parsed = JSON.parse(json); expect(parsed.success).toBe(true); expect(parsed.pagesRestored).toBe(1); expect(parsed.pluginsRestored).toBe(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