UNPKG

sfcc-dev-mcp

Version:

MCP server for Salesforce B2C Commerce Cloud development assistance including logs, debugging, and development tools

360 lines (305 loc) 12.6 kB
import { DocsToolHandler } from '../src/core/handlers/docs-handler.js'; import { HandlerContext } from '../src/core/handlers/base-handler.js'; import { Logger } from '../src/utils/logger.js'; // Mock the SFCCDocumentationClient const mockSFCCDocumentationClient = { getClassDetailsExpanded: jest.fn(), getAvailableClasses: jest.fn(), searchClasses: jest.fn(), searchMethods: jest.fn(), getClassDocumentation: jest.fn(), }; jest.mock('../src/clients/docs-client.js', () => ({ SFCCDocumentationClient: jest.fn(() => mockSFCCDocumentationClient), })); describe('DocsToolHandler', () => { let mockLogger: jest.Mocked<Logger>; let mockDocsClient: typeof mockSFCCDocumentationClient; let context: HandlerContext; let handler: DocsToolHandler; beforeEach(() => { mockLogger = { debug: jest.fn(), log: jest.fn(), error: jest.fn(), timing: jest.fn(), methodEntry: jest.fn(), methodExit: jest.fn(), } as any; // Reset mocks jest.clearAllMocks(); // Use the mock client directly mockDocsClient = mockSFCCDocumentationClient; jest.spyOn(Logger, 'getChildLogger').mockReturnValue(mockLogger); context = { logger: mockLogger, config: null as any, capabilities: { canAccessLogs: false, canAccessOCAPI: false }, }; handler = new DocsToolHandler(context, 'Docs'); }); afterEach(() => { jest.restoreAllMocks(); }); // Helper function to initialize handler for tests that need it const initializeHandler = async () => { await (handler as any).initialize(); }; describe('canHandle', () => { it('should handle docs-related tools', () => { expect(handler.canHandle('get_sfcc_class_info')).toBe(true); expect(handler.canHandle('list_sfcc_classes')).toBe(true); expect(handler.canHandle('search_sfcc_classes')).toBe(true); expect(handler.canHandle('search_sfcc_methods')).toBe(true); expect(handler.canHandle('get_sfcc_class_documentation')).toBe(true); }); it('should not handle non-docs tools', () => { expect(handler.canHandle('get_latest_error')).toBe(false); expect(handler.canHandle('unknown_tool')).toBe(false); }); }); describe('initialization', () => { it('should initialize docs client', async () => { await initializeHandler(); const MockedConstructor = jest.requireMock('../src/clients/docs-client.js').SFCCDocumentationClient; expect(MockedConstructor).toHaveBeenCalled(); expect(mockLogger.debug).toHaveBeenCalledWith('Documentation client initialized'); }); }); describe('disposal', () => { it('should dispose docs client properly', async () => { await initializeHandler(); await (handler as any).dispose(); expect(mockLogger.debug).toHaveBeenCalledWith('Documentation client disposed'); }); }); describe('get_sfcc_class_info tool', () => { beforeEach(async () => { await initializeHandler(); mockDocsClient.getClassDetailsExpanded.mockResolvedValue({ className: 'Product', packageName: 'dw.catalog', description: 'Product class description', constants: [], properties: [ { name: 'ID', type: 'String', description: 'Product ID' }, { name: 'name', type: 'String', description: 'Product name' }, ], methods: [ { name: 'getID', signature: 'getID() : String', description: 'Get product ID' }, { name: 'getName', signature: 'getName() : String', description: 'Get product name' }, ], }); }); it('should handle get_sfcc_class_info with className', async () => { const args = { className: 'Product', expand: true }; const result = await handler.handle('get_sfcc_class_info', args, Date.now()); expect(mockDocsClient.getClassDetailsExpanded).toHaveBeenCalledWith('Product', true, { includeDescription: true, includeConstants: true, includeProperties: true, includeMethods: true, includeInheritance: true, search: undefined, }); expect(result.content[0].text).toContain('Product'); expect(result.content[0].text).toContain('Product class description'); }); it('should handle get_sfcc_class_info with default expand', async () => { const args = { className: 'Customer' }; await handler.handle('get_sfcc_class_info', args, Date.now()); expect(mockDocsClient.getClassDetailsExpanded).toHaveBeenCalledWith('Customer', false, { includeDescription: true, includeConstants: true, includeProperties: true, includeMethods: true, includeInheritance: true, search: undefined, }); }); it('should throw error when className is missing', async () => { const result = await handler.handle('get_sfcc_class_info', {}, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('className must be a non-empty string'); }); it('should handle get_sfcc_class_info with filtering options', async () => { const args = { className: 'Product', includeDescription: false, includeConstants: false, includeProperties: true, includeMethods: false, includeInheritance: false, }; const result = await handler.handle('get_sfcc_class_info', args, Date.now()); expect(mockDocsClient.getClassDetailsExpanded).toHaveBeenCalledWith('Product', false, { includeDescription: false, includeConstants: false, includeProperties: true, includeMethods: false, includeInheritance: false, search: undefined, }); expect(result.content[0].text).toContain('Product'); }); it('should handle get_sfcc_class_info with search parameter', async () => { const args = { className: 'Product', search: 'image' }; const result = await handler.handle('get_sfcc_class_info', args, Date.now()); expect(mockDocsClient.getClassDetailsExpanded).toHaveBeenCalledWith('Product', false, { includeDescription: true, includeConstants: true, includeProperties: true, includeMethods: true, includeInheritance: true, search: 'image', }); expect(result.content[0].text).toContain('Product'); }); it('should handle get_sfcc_class_info with combined filtering and search', async () => { const args = { className: 'Product', expand: true, includeDescription: false, includeMethods: true, includeProperties: false, search: 'price', }; const result = await handler.handle('get_sfcc_class_info', args, Date.now()); expect(mockDocsClient.getClassDetailsExpanded).toHaveBeenCalledWith('Product', true, { includeDescription: false, includeConstants: true, includeProperties: false, includeMethods: true, includeInheritance: true, search: 'price', }); expect(result.content[0].text).toContain('Product'); }); }); describe('list_sfcc_classes tool', () => { beforeEach(async () => { await initializeHandler(); mockDocsClient.getAvailableClasses.mockResolvedValue([ 'Product', 'Customer', 'Order', 'Catalog', ]); }); it('should handle list_sfcc_classes', async () => { const result = await handler.handle('list_sfcc_classes', {}, Date.now()); expect(mockDocsClient.getAvailableClasses).toHaveBeenCalled(); expect(result.content[0].text).toContain('Product'); expect(result.content[0].text).toContain('Customer'); }); }); describe('search_sfcc_classes tool', () => { beforeEach(async () => { await initializeHandler(); mockDocsClient.searchClasses.mockResolvedValue([ 'Product', 'ProductSearchModel', ]); }); it('should handle search_sfcc_classes with query', async () => { const args = { query: 'product' }; const result = await handler.handle('search_sfcc_classes', args, Date.now()); expect(mockDocsClient.searchClasses).toHaveBeenCalledWith('product'); expect(result.content[0].text).toContain('Product'); expect(result.content[0].text).toContain('ProductSearchModel'); }); it('should throw error when query is missing', async () => { const result = await handler.handle('search_sfcc_classes', {}, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('query must be a non-empty string'); }); it('should throw error when query is empty', async () => { const result = await handler.handle('search_sfcc_classes', { query: '' }, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('query must be a non-empty string'); }); }); describe('search_sfcc_methods tool', () => { beforeEach(async () => { await initializeHandler(); mockDocsClient.searchMethods.mockResolvedValue([ { className: 'Product', method: { name: 'getID', signature: 'getID() : String', description: 'Get product ID', }, }, { className: 'Customer', method: { name: 'getID', signature: 'getID() : String', description: 'Get customer ID', }, }, ]); }); it('should handle search_sfcc_methods with methodName', async () => { const args = { methodName: 'getID' }; const result = await handler.handle('search_sfcc_methods', args, Date.now()); expect(mockDocsClient.searchMethods).toHaveBeenCalledWith('getID'); expect(result.content[0].text).toContain('getID'); expect(result.content[0].text).toContain('Product'); }); it('should throw error when methodName is missing', async () => { const result = await handler.handle('search_sfcc_methods', {}, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('methodName must be a non-empty string'); }); }); describe('get_sfcc_class_documentation tool', () => { beforeEach(async () => { await initializeHandler(); mockDocsClient.getClassDocumentation.mockResolvedValue( 'Detailed documentation for Product class with examples and usage patterns.', ); }); it('should handle get_sfcc_class_documentation with className', async () => { const args = { className: 'Product' }; const result = await handler.handle('get_sfcc_class_documentation', args, Date.now()); expect(mockDocsClient.getClassDocumentation).toHaveBeenCalledWith('Product'); expect(result.content[0].text).toContain('Detailed documentation'); }); it('should throw error when className is missing', async () => { const result = await handler.handle('get_sfcc_class_documentation', {}, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('className must be a non-empty string'); }); }); describe('error handling', () => { beforeEach(async () => { await initializeHandler(); }); it('should handle client errors gracefully', async () => { mockDocsClient.getClassDetailsExpanded.mockRejectedValue(new Error('Documentation not found')); const result = await handler.handle('get_sfcc_class_info', { className: 'UnknownClass' }, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Documentation not found'); }); it('should throw error for unsupported tools', async () => { await expect(handler.handle('unsupported_tool', {}, Date.now())) .rejects.toThrow('Unsupported tool'); }); }); describe('timing and logging', () => { beforeEach(async () => { await initializeHandler(); mockDocsClient.getAvailableClasses.mockResolvedValue(['Product', 'Customer']); }); it('should log timing information', async () => { const startTime = Date.now(); await handler.handle('list_sfcc_classes', {}, startTime); expect(mockLogger.timing).toHaveBeenCalledWith('list_sfcc_classes', startTime); }); it('should log execution details', async () => { await handler.handle('list_sfcc_classes', {}, Date.now()); expect(mockLogger.debug).toHaveBeenCalledWith( 'list_sfcc_classes completed successfully', expect.any(Object), ); }); }); });