Skip to main content
Glama

n8n-MCP

by 88-888
breaking-change-detector.test.ts25 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { BreakingChangeDetector, type DetectedChange, type VersionUpgradeAnalysis } from '@/services/breaking-change-detector'; import { NodeRepository } from '@/database/node-repository'; import * as BreakingChangesRegistry from '@/services/breaking-changes-registry'; vi.mock('@/database/node-repository'); vi.mock('@/services/breaking-changes-registry'); describe('BreakingChangeDetector', () => { let detector: BreakingChangeDetector; let mockRepository: NodeRepository; const createMockVersionData = (version: string, properties: any[] = []) => ({ nodeType: 'nodes-base.httpRequest', version, packageName: 'n8n-nodes-base', displayName: 'HTTP Request', isCurrentMax: false, propertiesSchema: properties, breakingChanges: [], deprecatedProperties: [], addedProperties: [] }); const createMockProperty = (name: string, type: string = 'string', required = false) => ({ name, displayName: name, type, required, default: null }); beforeEach(() => { vi.clearAllMocks(); mockRepository = new NodeRepository({} as any); detector = new BreakingChangeDetector(mockRepository); }); describe('analyzeVersionUpgrade', () => { it('should combine registry and dynamic changes', async () => { const registryChange: BreakingChangesRegistry.BreakingChange = { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'registryProp', changeType: 'removed', isBreaking: true, migrationHint: 'From registry', autoMigratable: true, severity: 'HIGH', migrationStrategy: { type: 'remove_property' } }; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([registryChange]); const v1 = createMockVersionData('1.0', [createMockProperty('dynamicProp')]); const v2 = createMockVersionData('2.0', []); vi.spyOn(mockRepository, 'getNodeVersion') .mockReturnValueOnce(v1) .mockReturnValueOnce(v2); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.changes.length).toBeGreaterThan(0); expect(result.changes.some(c => c.source === 'registry')).toBe(true); expect(result.changes.some(c => c.source === 'dynamic')).toBe(true); }); it('should detect breaking changes', async () => { const breakingChange: BreakingChangesRegistry.BreakingChange = { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'criticalProp', changeType: 'removed', isBreaking: true, migrationHint: 'This is breaking', autoMigratable: false, severity: 'HIGH', migrationStrategy: undefined }; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([breakingChange]); vi.spyOn(mockRepository, 'getNodeVersion').mockReturnValue(null); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.hasBreakingChanges).toBe(true); }); it('should calculate auto-migratable and manual counts', async () => { const changes: BreakingChangesRegistry.BreakingChange[] = [ { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'autoProp', changeType: 'added', isBreaking: false, migrationHint: 'Auto', autoMigratable: true, severity: 'LOW', migrationStrategy: { type: 'add_property', defaultValue: null } }, { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'manualProp', changeType: 'requirement_changed', isBreaking: true, migrationHint: 'Manual', autoMigratable: false, severity: 'HIGH', migrationStrategy: undefined } ]; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue(changes); vi.spyOn(mockRepository, 'getNodeVersion').mockReturnValue(null); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.autoMigratableCount).toBe(1); expect(result.manualRequiredCount).toBe(1); }); it('should determine overall severity', async () => { const highSeverityChange: BreakingChangesRegistry.BreakingChange = { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'criticalProp', changeType: 'removed', isBreaking: true, migrationHint: 'Critical', autoMigratable: false, severity: 'HIGH', migrationStrategy: undefined }; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([highSeverityChange]); vi.spyOn(mockRepository, 'getNodeVersion').mockReturnValue(null); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.overallSeverity).toBe('HIGH'); }); it('should generate recommendations', async () => { const changes: BreakingChangesRegistry.BreakingChange[] = [ { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'prop1', changeType: 'removed', isBreaking: true, migrationHint: 'Remove this', autoMigratable: true, severity: 'MEDIUM', migrationStrategy: { type: 'remove_property' } }, { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'prop2', changeType: 'requirement_changed', isBreaking: true, migrationHint: 'Manual work needed', autoMigratable: false, severity: 'HIGH', migrationStrategy: undefined } ]; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue(changes); vi.spyOn(mockRepository, 'getNodeVersion').mockReturnValue(null); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.recommendations.length).toBeGreaterThan(0); expect(result.recommendations.some(r => r.includes('breaking change'))).toBe(true); expect(result.recommendations.some(r => r.includes('automatically migrated'))).toBe(true); expect(result.recommendations.some(r => r.includes('manual intervention'))).toBe(true); }); }); describe('dynamic change detection', () => { it('should detect added properties', async () => { vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([]); const v1 = createMockVersionData('1.0', []); const v2 = createMockVersionData('2.0', [createMockProperty('newProp')]); vi.spyOn(mockRepository, 'getNodeVersion') .mockReturnValueOnce(v1) .mockReturnValueOnce(v2); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); const addedChange = result.changes.find(c => c.changeType === 'added'); expect(addedChange).toBeDefined(); expect(addedChange?.propertyName).toBe('newProp'); expect(addedChange?.source).toBe('dynamic'); }); it('should mark required added properties as breaking', async () => { vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([]); const v1 = createMockVersionData('1.0', []); const v2 = createMockVersionData('2.0', [createMockProperty('requiredProp', 'string', true)]); vi.spyOn(mockRepository, 'getNodeVersion') .mockReturnValueOnce(v1) .mockReturnValueOnce(v2); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); const addedChange = result.changes.find(c => c.changeType === 'added'); expect(addedChange?.isBreaking).toBe(true); expect(addedChange?.severity).toBe('HIGH'); expect(addedChange?.autoMigratable).toBe(false); }); it('should mark optional added properties as non-breaking', async () => { vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([]); const v1 = createMockVersionData('1.0', []); const v2 = createMockVersionData('2.0', [createMockProperty('optionalProp', 'string', false)]); vi.spyOn(mockRepository, 'getNodeVersion') .mockReturnValueOnce(v1) .mockReturnValueOnce(v2); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); const addedChange = result.changes.find(c => c.changeType === 'added'); expect(addedChange?.isBreaking).toBe(false); expect(addedChange?.severity).toBe('LOW'); expect(addedChange?.autoMigratable).toBe(true); }); it('should detect removed properties', async () => { vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([]); const v1 = createMockVersionData('1.0', [createMockProperty('oldProp')]); const v2 = createMockVersionData('2.0', []); vi.spyOn(mockRepository, 'getNodeVersion') .mockReturnValueOnce(v1) .mockReturnValueOnce(v2); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); const removedChange = result.changes.find(c => c.changeType === 'removed'); expect(removedChange).toBeDefined(); expect(removedChange?.propertyName).toBe('oldProp'); expect(removedChange?.isBreaking).toBe(true); expect(removedChange?.autoMigratable).toBe(true); }); it('should detect requirement changes', async () => { vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([]); const v1 = createMockVersionData('1.0', [createMockProperty('prop', 'string', false)]); const v2 = createMockVersionData('2.0', [createMockProperty('prop', 'string', true)]); vi.spyOn(mockRepository, 'getNodeVersion') .mockReturnValueOnce(v1) .mockReturnValueOnce(v2); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); const requirementChange = result.changes.find(c => c.changeType === 'requirement_changed'); expect(requirementChange).toBeDefined(); expect(requirementChange?.isBreaking).toBe(true); expect(requirementChange?.oldValue).toBe('optional'); expect(requirementChange?.newValue).toBe('required'); }); it('should detect when property becomes optional', async () => { vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([]); const v1 = createMockVersionData('1.0', [createMockProperty('prop', 'string', true)]); const v2 = createMockVersionData('2.0', [createMockProperty('prop', 'string', false)]); vi.spyOn(mockRepository, 'getNodeVersion') .mockReturnValueOnce(v1) .mockReturnValueOnce(v2); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); const requirementChange = result.changes.find(c => c.changeType === 'requirement_changed'); expect(requirementChange).toBeDefined(); expect(requirementChange?.isBreaking).toBe(false); expect(requirementChange?.severity).toBe('LOW'); }); it('should handle missing version data gracefully', async () => { vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([]); vi.spyOn(mockRepository, 'getNodeVersion').mockReturnValue(null); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.changes.filter(c => c.source === 'dynamic')).toHaveLength(0); }); it('should handle missing properties schema', async () => { vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([]); const v1 = { ...createMockVersionData('1.0'), propertiesSchema: null }; const v2 = { ...createMockVersionData('2.0'), propertiesSchema: null }; vi.spyOn(mockRepository, 'getNodeVersion') .mockReturnValueOnce(v1 as any) .mockReturnValueOnce(v2 as any); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.changes.filter(c => c.source === 'dynamic')).toHaveLength(0); }); }); describe('change merging and deduplication', () => { it('should prioritize registry changes over dynamic', async () => { const registryChange: BreakingChangesRegistry.BreakingChange = { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'sharedProp', changeType: 'removed', isBreaking: true, migrationHint: 'From registry', autoMigratable: true, severity: 'HIGH', migrationStrategy: { type: 'remove_property' } }; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([registryChange]); const v1 = createMockVersionData('1.0', [createMockProperty('sharedProp')]); const v2 = createMockVersionData('2.0', []); vi.spyOn(mockRepository, 'getNodeVersion') .mockReturnValueOnce(v1) .mockReturnValueOnce(v2); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); const sharedChanges = result.changes.filter(c => c.propertyName === 'sharedProp'); expect(sharedChanges).toHaveLength(1); expect(sharedChanges[0].source).toBe('registry'); }); it('should sort changes by severity', async () => { const changes: BreakingChangesRegistry.BreakingChange[] = [ { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'lowProp', changeType: 'added', isBreaking: false, migrationHint: 'Low', autoMigratable: true, severity: 'LOW', migrationStrategy: { type: 'add_property', defaultValue: null } }, { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'highProp', changeType: 'removed', isBreaking: true, migrationHint: 'High', autoMigratable: false, severity: 'HIGH', migrationStrategy: undefined }, { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'medProp', changeType: 'renamed', isBreaking: true, migrationHint: 'Medium', autoMigratable: true, severity: 'MEDIUM', migrationStrategy: { type: 'rename_property', sourceProperty: 'old', targetProperty: 'new' } } ]; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue(changes); vi.spyOn(mockRepository, 'getNodeVersion').mockReturnValue(null); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.changes[0].severity).toBe('HIGH'); expect(result.changes[result.changes.length - 1].severity).toBe('LOW'); }); }); describe('hasBreakingChanges', () => { it('should return true when breaking changes exist', () => { const breakingChange: BreakingChangesRegistry.BreakingChange = { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'prop', changeType: 'removed', isBreaking: true, migrationHint: 'Breaking', autoMigratable: false, severity: 'HIGH', migrationStrategy: undefined }; vi.spyOn(BreakingChangesRegistry, 'getBreakingChangesForNode').mockReturnValue([breakingChange]); const result = detector.hasBreakingChanges('nodes-base.httpRequest', '1.0', '2.0'); expect(result).toBe(true); }); it('should return false when no breaking changes', () => { vi.spyOn(BreakingChangesRegistry, 'getBreakingChangesForNode').mockReturnValue([]); const result = detector.hasBreakingChanges('nodes-base.httpRequest', '1.0', '2.0'); expect(result).toBe(false); }); }); describe('getChangedProperties', () => { it('should return list of changed property names', () => { const changes: BreakingChangesRegistry.BreakingChange[] = [ { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'prop1', changeType: 'added', isBreaking: false, migrationHint: '', autoMigratable: true, severity: 'LOW', migrationStrategy: undefined }, { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'prop2', changeType: 'removed', isBreaking: true, migrationHint: '', autoMigratable: true, severity: 'MEDIUM', migrationStrategy: undefined } ]; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue(changes); const result = detector.getChangedProperties('nodes-base.httpRequest', '1.0', '2.0'); expect(result).toEqual(['prop1', 'prop2']); }); it('should return empty array when no changes', () => { vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([]); const result = detector.getChangedProperties('nodes-base.httpRequest', '1.0', '2.0'); expect(result).toEqual([]); }); }); describe('recommendations generation', () => { it('should recommend safe upgrade when no breaking changes', async () => { const changes: BreakingChangesRegistry.BreakingChange[] = [ { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'prop', changeType: 'added', isBreaking: false, migrationHint: 'Safe', autoMigratable: true, severity: 'LOW', migrationStrategy: { type: 'add_property', defaultValue: null } } ]; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue(changes); vi.spyOn(mockRepository, 'getNodeVersion').mockReturnValue(null); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.recommendations.some(r => r.includes('No breaking changes'))).toBe(true); expect(result.recommendations.some(r => r.includes('safe'))).toBe(true); }); it('should warn about breaking changes', async () => { const changes: BreakingChangesRegistry.BreakingChange[] = [ { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'prop', changeType: 'removed', isBreaking: true, migrationHint: 'Breaking', autoMigratable: false, severity: 'HIGH', migrationStrategy: undefined } ]; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue(changes); vi.spyOn(mockRepository, 'getNodeVersion').mockReturnValue(null); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.recommendations.some(r => r.includes('breaking change'))).toBe(true); }); it('should list manual changes required', async () => { const changes: BreakingChangesRegistry.BreakingChange[] = [ { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'manualProp', changeType: 'requirement_changed', isBreaking: true, migrationHint: 'Manually configure this', autoMigratable: false, severity: 'HIGH', migrationStrategy: undefined } ]; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue(changes); vi.spyOn(mockRepository, 'getNodeVersion').mockReturnValue(null); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.recommendations.some(r => r.includes('manual intervention'))).toBe(true); expect(result.recommendations.some(r => r.includes('manualProp'))).toBe(true); }); }); describe('nested properties', () => { it('should flatten nested properties for comparison', async () => { vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue([]); const nestedProp = { name: 'parent', displayName: 'Parent', type: 'options', options: [ createMockProperty('child1'), createMockProperty('child2') ] }; const v1 = createMockVersionData('1.0', [nestedProp]); const v2 = createMockVersionData('2.0', []); vi.spyOn(mockRepository, 'getNodeVersion') .mockReturnValueOnce(v1) .mockReturnValueOnce(v2); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); // Should detect removal of parent and nested properties expect(result.changes.some(c => c.propertyName.includes('parent'))).toBe(true); }); }); describe('overall severity calculation', () => { it('should return HIGH when any change is HIGH severity', async () => { const changes: BreakingChangesRegistry.BreakingChange[] = [ { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'lowProp', changeType: 'added', isBreaking: false, migrationHint: '', autoMigratable: true, severity: 'LOW', migrationStrategy: undefined }, { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'highProp', changeType: 'removed', isBreaking: true, migrationHint: '', autoMigratable: false, severity: 'HIGH', migrationStrategy: undefined } ]; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue(changes); vi.spyOn(mockRepository, 'getNodeVersion').mockReturnValue(null); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.overallSeverity).toBe('HIGH'); }); it('should return MEDIUM when no HIGH but has MEDIUM', async () => { const changes: BreakingChangesRegistry.BreakingChange[] = [ { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'lowProp', changeType: 'added', isBreaking: false, migrationHint: '', autoMigratable: true, severity: 'LOW', migrationStrategy: undefined }, { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'medProp', changeType: 'renamed', isBreaking: true, migrationHint: '', autoMigratable: true, severity: 'MEDIUM', migrationStrategy: undefined } ]; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue(changes); vi.spyOn(mockRepository, 'getNodeVersion').mockReturnValue(null); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.overallSeverity).toBe('MEDIUM'); }); it('should return LOW when all changes are LOW severity', async () => { const changes: BreakingChangesRegistry.BreakingChange[] = [ { nodeType: 'nodes-base.httpRequest', fromVersion: '1.0', toVersion: '2.0', propertyName: 'prop', changeType: 'added', isBreaking: false, migrationHint: '', autoMigratable: true, severity: 'LOW', migrationStrategy: undefined } ]; vi.spyOn(BreakingChangesRegistry, 'getAllChangesForNode').mockReturnValue(changes); vi.spyOn(mockRepository, 'getNodeVersion').mockReturnValue(null); const result = await detector.analyzeVersionUpgrade('nodes-base.httpRequest', '1.0', '2.0'); expect(result.overallSeverity).toBe('LOW'); }); }); });

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/88-888/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server