fix: update tests for template compression, pagination, and quality filtering

- Fix parameter validation tests to expect mode parameter in getTemplate calls
- Update database utils tests to use totalViews > 10 for quality filter
- Add comprehensive tests for template service functionality
- Fix integration tests for new pagination parameters

All CI tests now passing after template system enhancements
This commit is contained in:
czlonkowski
2025-09-14 15:42:35 +02:00
parent e7895d2e01
commit 3b767c798c
8 changed files with 1425 additions and 16 deletions

View File

@@ -371,7 +371,7 @@ describe('Parameter Validation', () => {
templateId: 123
});
expect(mockGetTemplate).toHaveBeenCalledWith(123);
expect(mockGetTemplate).toHaveBeenCalledWith(123, 'full');
});
it('should convert string templateId to number', async () => {
@@ -381,7 +381,7 @@ describe('Parameter Validation', () => {
templateId: '123'
});
expect(mockGetTemplate).toHaveBeenCalledWith(123);
expect(mockGetTemplate).toHaveBeenCalledWith(123, 'full');
});
});
});

View File

@@ -0,0 +1,481 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
import { NodeRepository } from '../../../src/database/node-repository';
import { TemplateService, PaginatedResponse, TemplateMinimal } from '../../../src/templates/template-service';
import { DatabaseAdapter } from '../../../src/database/database-adapter';
// Mock dependencies
vi.mock('../../../src/utils/logger', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn()
},
Logger: vi.fn().mockImplementation(() => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn()
}))
}));
vi.mock('../../../src/database/node-repository');
vi.mock('../../../src/templates/template-service');
vi.mock('../../../src/database/database-adapter');
// Create testable server class to access private methods
class TestableMCPServer extends N8NDocumentationMCPServer {
public async testListTemplates(limit?: number, offset?: number, sortBy?: 'views' | 'created_at' | 'name'): Promise<any> {
return (this as any).listTemplates(limit, offset, sortBy);
}
public async testGetTemplate(templateId: number, mode?: 'full' | 'nodes_only' | 'structure'): Promise<any> {
return (this as any).getTemplate(templateId, mode);
}
public async testGetDatabaseStatistics(): Promise<any> {
return (this as any).getDatabaseStatistics();
}
public async testListNodeTemplates(nodeTypes: string[], limit?: number, offset?: number): Promise<any> {
return (this as any).listNodeTemplates(nodeTypes, limit, offset);
}
public async testSearchTemplates(query: string, limit?: number, offset?: number): Promise<any> {
return (this as any).searchTemplates(query, limit, offset);
}
public async testGetTemplatesForTask(task: string, limit?: number, offset?: number): Promise<any> {
return (this as any).getTemplatesForTask(task, limit, offset);
}
}
describe('MCP Template Handlers', () => {
let server: TestableMCPServer;
let mockDb: DatabaseAdapter;
let mockNodeRepository: NodeRepository;
let mockTemplateService: TemplateService;
beforeEach(() => {
vi.clearAllMocks();
mockDb = {
prepare: vi.fn(),
exec: vi.fn(),
close: vi.fn(),
pragma: vi.fn(),
transaction: vi.fn(),
checkFTS5Support: vi.fn(() => true),
inTransaction: false
} as any;
mockNodeRepository = {
getTotalNodes: vi.fn(() => 500),
getAIToolsCount: vi.fn(() => 263),
getTriggersCount: vi.fn(() => 104),
getDocsCount: vi.fn(() => 435)
} as any;
mockTemplateService = {
listTemplates: vi.fn(),
getTemplate: vi.fn(),
listNodeTemplates: vi.fn(),
searchTemplates: vi.fn(),
getTemplatesForTask: vi.fn(),
getTemplateStats: vi.fn()
} as any;
(NodeRepository as any).mockImplementation(() => mockNodeRepository);
(TemplateService as any).mockImplementation(() => mockTemplateService);
// Set environment variable for in-memory database
process.env.NODE_DB_PATH = ':memory:';
server = new TestableMCPServer();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('listTemplates', () => {
it('should return paginated template list with default parameters', async () => {
const mockResponse: PaginatedResponse<TemplateMinimal> = {
items: [
{ id: 1, name: 'Template A', views: 200, nodeCount: 3 },
{ id: 2, name: 'Template B', views: 150, nodeCount: 2 }
],
total: 25,
limit: 10,
offset: 0,
hasMore: true
};
mockTemplateService.listTemplates = vi.fn().mockResolvedValue(mockResponse);
const result = await server.testListTemplates();
expect(result).toEqual({
templates: mockResponse.items,
pagination: {
total: 25,
limit: 10,
offset: 0,
hasMore: true
}
});
expect(mockTemplateService.listTemplates).toHaveBeenCalledWith(10, 0, 'views');
});
it('should handle custom pagination parameters', async () => {
const mockResponse: PaginatedResponse<TemplateMinimal> = {
items: [
{ id: 3, name: 'Template C', views: 100, nodeCount: 1 }
],
total: 25,
limit: 5,
offset: 20,
hasMore: false
};
mockTemplateService.listTemplates = vi.fn().mockResolvedValue(mockResponse);
const result = await server.testListTemplates(5, 20, 'name');
expect(result).toEqual({
templates: mockResponse.items,
pagination: {
total: 25,
limit: 5,
offset: 20,
hasMore: false
}
});
expect(mockTemplateService.listTemplates).toHaveBeenCalledWith(5, 20, 'name');
});
it('should handle empty results', async () => {
const mockResponse: PaginatedResponse<TemplateMinimal> = {
items: [],
total: 0,
limit: 10,
offset: 0,
hasMore: false
};
mockTemplateService.listTemplates = vi.fn().mockResolvedValue(mockResponse);
const result = await server.testListTemplates();
expect(result.templates).toHaveLength(0);
expect(result.pagination.total).toBe(0);
});
});
describe('getTemplate with mode parameter', () => {
it('should return full template by default', async () => {
const mockTemplate = {
id: 1,
name: 'Test Template',
description: 'Test description',
author: { name: 'Test Author', username: 'test', verified: true },
nodes: ['n8n-nodes-base.webhook'],
views: 100,
created: '2024-01-01T00:00:00Z',
url: 'https://n8n.io/workflows/1',
workflow: {
nodes: [{ id: 'node1', type: 'n8n-nodes-base.webhook' }],
connections: {},
settings: {}
}
};
mockTemplateService.getTemplate = vi.fn().mockResolvedValue(mockTemplate);
const result = await server.testGetTemplate(1);
expect(result).toEqual(mockTemplate);
expect(mockTemplateService.getTemplate).toHaveBeenCalledWith(1, 'full');
});
it('should return nodes_only mode correctly', async () => {
const mockTemplate = {
id: 1,
name: 'Test Template',
nodes: [
{ type: 'n8n-nodes-base.webhook', name: 'Webhook' },
{ type: 'n8n-nodes-base.slack', name: 'Slack' }
]
};
mockTemplateService.getTemplate = vi.fn().mockResolvedValue(mockTemplate);
const result = await server.testGetTemplate(1, 'nodes_only');
expect(result).toEqual(mockTemplate);
expect(mockTemplateService.getTemplate).toHaveBeenCalledWith(1, 'nodes_only');
});
it('should return structure mode correctly', async () => {
const mockTemplate = {
id: 1,
name: 'Test Template',
nodes: [
{ id: 'node1', type: 'n8n-nodes-base.webhook', name: 'Webhook', position: [100, 100] }
],
connections: { node1: { main: [[{ node: 'node2', type: 'main', index: 0 }]] } }
};
mockTemplateService.getTemplate = vi.fn().mockResolvedValue(mockTemplate);
const result = await server.testGetTemplate(1, 'structure');
expect(result).toEqual(mockTemplate);
expect(mockTemplateService.getTemplate).toHaveBeenCalledWith(1, 'structure');
});
it('should handle non-existent template', async () => {
mockTemplateService.getTemplate = vi.fn().mockResolvedValue(null);
const result = await server.testGetTemplate(999);
expect(result).toEqual({
error: 'Template not found',
tip: "Use list_templates, list_node_templates or search_templates to find available templates"
});
});
});
describe('Enhanced template tools with pagination', () => {
describe('listNodeTemplates', () => {
it('should handle pagination correctly', async () => {
const mockResponse = {
items: [
{ id: 1, name: 'Webhook Template', nodes: ['n8n-nodes-base.webhook'], views: 200 }
],
total: 15,
limit: 10,
offset: 5,
hasMore: true
};
mockTemplateService.listNodeTemplates = vi.fn().mockResolvedValue(mockResponse);
const result = await server.testListNodeTemplates(['n8n-nodes-base.webhook'], 10, 5);
expect(result).toEqual({
templates: mockResponse.items,
pagination: {
total: 15,
limit: 10,
offset: 5,
hasMore: true
}
});
expect(mockTemplateService.listNodeTemplates).toHaveBeenCalledWith(['n8n-nodes-base.webhook'], 10, 5);
});
});
describe('searchTemplates', () => {
it('should handle pagination correctly', async () => {
const mockResponse = {
items: [
{ id: 2, name: 'Search Result', description: 'Found template', views: 150 }
],
total: 8,
limit: 20,
offset: 0,
hasMore: false
};
mockTemplateService.searchTemplates = vi.fn().mockResolvedValue(mockResponse);
const result = await server.testSearchTemplates('webhook', 20, 0);
expect(result).toEqual({
templates: mockResponse.items,
pagination: {
total: 8,
limit: 20,
offset: 0,
hasMore: false
}
});
expect(mockTemplateService.searchTemplates).toHaveBeenCalledWith('webhook', 20, 0);
});
});
describe('getTemplatesForTask', () => {
it('should handle pagination correctly', async () => {
const mockResponse = {
items: [
{ id: 3, name: 'AI Template', nodes: ['@n8n/n8n-nodes-langchain.openAi'], views: 300 }
],
total: 12,
limit: 10,
offset: 10,
hasMore: true
};
mockTemplateService.getTemplatesForTask = vi.fn().mockResolvedValue(mockResponse);
const result = await server.testGetTemplatesForTask('ai_automation', 10, 10);
expect(result).toEqual({
templates: mockResponse.items,
pagination: {
total: 12,
limit: 10,
offset: 10,
hasMore: true
}
});
expect(mockTemplateService.getTemplatesForTask).toHaveBeenCalledWith('ai_automation', 10, 10);
});
});
});
describe('getDatabaseStatistics with template metrics', () => {
it('should include template statistics', async () => {
const mockTemplateStats = {
totalTemplates: 100,
averageViews: 250,
minViews: 10,
maxViews: 1000,
topUsedNodes: [
{ node: 'n8n-nodes-base.webhook', count: 45 },
{ node: 'n8n-nodes-base.slack', count: 30 }
]
};
mockTemplateService.getTemplateStats = vi.fn().mockResolvedValue(mockTemplateStats);
const result = await server.testGetDatabaseStatistics();
expect(result).toEqual(expect.objectContaining({
nodeStatistics: {
totalNodes: 500,
aiTools: 263,
triggers: 104,
docsAvailable: 435,
docsCoverage: '87%'
},
templateStatistics: {
totalTemplates: 100,
averageViews: 250,
minViews: 10,
maxViews: 1000,
topUsedNodes: [
{ node: 'n8n-nodes-base.webhook', count: 45 },
{ node: 'n8n-nodes-base.slack', count: 30 }
]
}
}));
expect(mockTemplateService.getTemplateStats).toHaveBeenCalled();
});
it('should handle template service errors gracefully', async () => {
mockTemplateService.getTemplateStats = vi.fn().mockRejectedValue(new Error('Template stats failed'));
const result = await server.testGetDatabaseStatistics();
expect(result).toEqual(expect.objectContaining({
nodeStatistics: expect.any(Object),
templateStatistics: {
error: 'Unable to fetch template statistics'
}
}));
});
it('should handle missing template service', async () => {
// Create server without template service
const serverWithoutTemplates = new TestableMCPServer();
(serverWithoutTemplates as any).templateService = undefined;
const result = await serverWithoutTemplates.testGetDatabaseStatistics();
expect(result).toEqual(expect.objectContaining({
nodeStatistics: expect.any(Object),
templateStatistics: {
error: 'Template service not available'
}
}));
});
});
describe('Error handling', () => {
it('should handle service errors in listTemplates', async () => {
mockTemplateService.listTemplates = vi.fn().mockRejectedValue(new Error('Database error'));
await expect(server.testListTemplates()).rejects.toThrow('Database error');
});
it('should handle service errors in getTemplate', async () => {
mockTemplateService.getTemplate = vi.fn().mockRejectedValue(new Error('Template fetch error'));
await expect(server.testGetTemplate(1)).rejects.toThrow('Template fetch error');
});
it('should validate template mode parameter', async () => {
const result = await server.testGetTemplate(1, 'invalid_mode' as any);
expect(mockTemplateService.getTemplate).toHaveBeenCalledWith(1, 'invalid_mode');
});
it('should handle invalid pagination parameters', async () => {
const mockResponse = {
items: [],
total: 0,
limit: 10,
offset: 0,
hasMore: false
};
mockTemplateService.listTemplates = vi.fn().mockResolvedValue(mockResponse);
// Should handle negative values gracefully
const result = await server.testListTemplates(-5, -10, 'views');
expect(mockTemplateService.listTemplates).toHaveBeenCalledWith(-5, -10, 'views');
expect(result.pagination.limit).toBe(10);
expect(result.pagination.offset).toBe(0);
});
});
describe('Integration with existing functionality', () => {
it('should maintain backward compatibility', async () => {
// Existing getTemplate method should work without mode parameter
const mockTemplate = { id: 1, name: 'Test' };
mockTemplateService.getTemplate = vi.fn().mockResolvedValue(mockTemplate);
const result = await server.testGetTemplate(1);
expect(mockTemplateService.getTemplate).toHaveBeenCalledWith(1, 'full');
});
it('should work with existing template tools', async () => {
// Test that existing functionality isn't broken
const mockResponse = {
items: [{ id: 1, name: 'Template' }],
total: 1,
limit: 10,
offset: 0,
hasMore: false
};
mockTemplateService.listNodeTemplates = vi.fn().mockResolvedValue(mockResponse);
const result = await server.testListNodeTemplates(['n8n-nodes-base.webhook']);
expect(mockTemplateService.listNodeTemplates).toHaveBeenCalledWith(['n8n-nodes-base.webhook'], 10, 0);
expect(result.templates).toBeDefined();
expect(result.pagination).toBeDefined();
});
});
});

View File

@@ -254,7 +254,7 @@ describe('n8nDocumentationToolsFinal', () => {
discovery: ['list_nodes', 'search_nodes', 'list_ai_tools'],
configuration: ['get_node_info', 'get_node_essentials', 'get_node_documentation'],
validation: ['validate_node_operation', 'validate_workflow', 'validate_node_minimal'],
templates: ['list_tasks', 'get_node_for_task', 'search_templates'],
templates: ['list_tasks', 'get_node_for_task', 'search_templates', 'list_templates', 'get_template', 'list_node_templates'],
documentation: ['tools_documentation']
};
@@ -317,4 +317,83 @@ describe('n8nDocumentationToolsFinal', () => {
});
});
});
describe('New Template Tools', () => {
describe('list_templates', () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'list_templates');
it('should exist and be properly defined', () => {
expect(tool).toBeDefined();
expect(tool?.description).toContain('minimal data');
});
it('should have correct parameters', () => {
expect(tool?.inputSchema.properties).toHaveProperty('limit');
expect(tool?.inputSchema.properties).toHaveProperty('offset');
expect(tool?.inputSchema.properties).toHaveProperty('sortBy');
const limitParam = tool?.inputSchema.properties.limit;
expect(limitParam.type).toBe('number');
expect(limitParam.minimum).toBe(1);
expect(limitParam.maximum).toBe(100);
const offsetParam = tool?.inputSchema.properties.offset;
expect(offsetParam.type).toBe('number');
expect(offsetParam.minimum).toBe(0);
const sortByParam = tool?.inputSchema.properties.sortBy;
expect(sortByParam.enum).toEqual(['views', 'created_at', 'name']);
});
it('should have no required parameters', () => {
expect(tool?.inputSchema.required).toBeUndefined();
});
});
describe('get_template (enhanced)', () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_template');
it('should exist and support mode parameter', () => {
expect(tool).toBeDefined();
expect(tool?.description).toContain('mode');
});
it('should have mode parameter with correct values', () => {
expect(tool?.inputSchema.properties).toHaveProperty('mode');
const modeParam = tool?.inputSchema.properties.mode;
expect(modeParam.enum).toEqual(['nodes_only', 'structure', 'full']);
expect(modeParam.default).toBe('full');
});
it('should require templateId parameter', () => {
expect(tool?.inputSchema.required).toContain('templateId');
});
});
describe('Enhanced pagination support', () => {
const paginatedTools = ['list_node_templates', 'search_templates', 'get_templates_for_task'];
paginatedTools.forEach(toolName => {
describe(toolName, () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
it('should support limit parameter', () => {
expect(tool?.inputSchema.properties).toHaveProperty('limit');
const limitParam = tool?.inputSchema.properties.limit;
expect(limitParam.type).toBe('number');
expect(limitParam.minimum).toBeGreaterThanOrEqual(1);
expect(limitParam.maximum).toBeGreaterThanOrEqual(50);
});
it('should support offset parameter', () => {
expect(tool?.inputSchema.properties).toHaveProperty('offset');
const offsetParam = tool?.inputSchema.properties.offset;
expect(offsetParam.type).toBe('number');
expect(offsetParam.minimum).toBe(0);
});
});
});
});
});
});