test: add comprehensive unit tests for database, parsers, loaders, and MCP tools

- Database layer tests (32 tests):
  - node-repository.ts: 100% coverage
  - template-repository.ts: 80.31% coverage
  - database-adapter.ts: interface compliance tests

- Parser tests (99 tests):
  - node-parser.ts: 93.10% coverage
  - property-extractor.ts: 95.18% coverage
  - simple-parser.ts: 91.26% coverage
  - Fixed parser bugs for version extraction

- Loader tests (22 tests):
  - node-loader.ts: comprehensive mocking tests

- MCP tools tests (85 tests):
  - tools.ts: 100% coverage
  - tools-documentation.ts: 100% coverage
  - docs-mapper.ts: 100% coverage

Total: 943 tests passing across 32 test files
Significant progress from 2.45% to ~30% overall coverage

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-07-28 20:16:38 +02:00
parent 48219fb860
commit d870d0ab71
16 changed files with 5077 additions and 21 deletions

View File

@@ -0,0 +1,377 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
getToolDocumentation,
getToolsOverview,
searchToolDocumentation,
getToolsByCategory,
getAllCategories
} from '@/mcp/tools-documentation';
// Mock the tool-docs import
vi.mock('@/mcp/tool-docs', () => ({
toolsDocumentation: {
search_nodes: {
name: 'search_nodes',
category: 'discovery',
essentials: {
description: 'Search nodes by keywords',
keyParameters: ['query', 'mode', 'limit'],
example: 'search_nodes({query: "slack"})',
performance: 'Instant (<10ms)',
tips: ['Use single words for precision', 'Try FUZZY mode for typos']
},
full: {
description: 'Full-text search across all n8n nodes with multiple matching modes',
parameters: {
query: {
type: 'string',
description: 'Search terms',
required: true
},
mode: {
type: 'string',
description: 'Search mode',
enum: ['OR', 'AND', 'FUZZY'],
default: 'OR'
},
limit: {
type: 'number',
description: 'Max results',
default: 20
}
},
returns: 'Array of matching nodes with metadata',
examples: [
'search_nodes({query: "webhook"})',
'search_nodes({query: "http request", mode: "AND"})'
],
useCases: ['Finding integration nodes', 'Discovering available triggers'],
performance: 'Instant - uses in-memory index',
bestPractices: ['Start with single words', 'Use FUZZY for uncertain names'],
pitfalls: ['Overly specific queries may return no results'],
relatedTools: ['list_nodes', 'get_node_info']
}
},
validate_workflow: {
name: 'validate_workflow',
category: 'validation',
essentials: {
description: 'Validate complete workflow structure',
keyParameters: ['workflow', 'options'],
example: 'validate_workflow(workflow)',
performance: 'Moderate (100-500ms)',
tips: ['Run before deployment', 'Check all validation types']
},
full: {
description: 'Comprehensive workflow validation',
parameters: {
workflow: {
type: 'object',
description: 'Workflow JSON',
required: true
},
options: {
type: 'object',
description: 'Validation options'
}
},
returns: 'Validation results with errors and warnings',
examples: ['validate_workflow(workflow)'],
useCases: ['Pre-deployment checks', 'CI/CD validation'],
performance: 'Depends on workflow complexity',
bestPractices: ['Validate before saving', 'Fix errors first'],
pitfalls: ['Large workflows may take time'],
relatedTools: ['validate_node_operation']
}
},
get_node_essentials: {
name: 'get_node_essentials',
category: 'configuration',
essentials: {
description: 'Get essential node properties only',
keyParameters: ['nodeType'],
example: 'get_node_essentials("nodes-base.slack")',
performance: 'Fast (<100ms)',
tips: ['Use this before get_node_info', 'Returns 95% smaller payload']
},
full: {
description: 'Returns 10-20 most important properties',
parameters: {
nodeType: {
type: 'string',
description: 'Full node type with prefix',
required: true
}
},
returns: 'Essential properties with examples',
examples: ['get_node_essentials("nodes-base.httpRequest")'],
useCases: ['Quick configuration', 'Property discovery'],
performance: 'Fast - pre-filtered data',
bestPractices: ['Always try essentials first'],
pitfalls: ['May not include all advanced options'],
relatedTools: ['get_node_info']
}
}
}
}));
// Mock package.json for version info
vi.mock('../../package.json', () => ({
default: {
dependencies: {
n8n: '^1.103.2'
}
}
}));
describe('tools-documentation', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getToolDocumentation', () => {
describe('essentials mode', () => {
it('should return essential documentation for existing tool', () => {
const doc = getToolDocumentation('search_nodes', 'essentials');
expect(doc).toContain('# search_nodes');
expect(doc).toContain('Search nodes by keywords');
expect(doc).toContain('**Example**: search_nodes({query: "slack"})');
expect(doc).toContain('**Key parameters**: query, mode, limit');
expect(doc).toContain('**Performance**: Instant (<10ms)');
expect(doc).toContain('- Use single words for precision');
expect(doc).toContain('- Try FUZZY mode for typos');
expect(doc).toContain('For full documentation, use: tools_documentation({topic: "search_nodes", depth: "full"})');
});
it('should return error message for unknown tool', () => {
const doc = getToolDocumentation('unknown_tool', 'essentials');
expect(doc).toBe("Tool 'unknown_tool' not found. Use tools_documentation() to see available tools.");
});
it('should use essentials as default depth', () => {
const docDefault = getToolDocumentation('search_nodes');
const docEssentials = getToolDocumentation('search_nodes', 'essentials');
expect(docDefault).toBe(docEssentials);
});
});
describe('full mode', () => {
it('should return complete documentation for existing tool', () => {
const doc = getToolDocumentation('search_nodes', 'full');
expect(doc).toContain('# search_nodes');
expect(doc).toContain('Full-text search across all n8n nodes');
expect(doc).toContain('## Parameters');
expect(doc).toContain('- **query** (string, required): Search terms');
expect(doc).toContain('- **mode** (string): Search mode');
expect(doc).toContain('- **limit** (number): Max results');
expect(doc).toContain('## Returns');
expect(doc).toContain('Array of matching nodes with metadata');
expect(doc).toContain('## Examples');
expect(doc).toContain('search_nodes({query: "webhook"})');
expect(doc).toContain('## Common Use Cases');
expect(doc).toContain('- Finding integration nodes');
expect(doc).toContain('## Performance');
expect(doc).toContain('Instant - uses in-memory index');
expect(doc).toContain('## Best Practices');
expect(doc).toContain('- Start with single words');
expect(doc).toContain('## Common Pitfalls');
expect(doc).toContain('- Overly specific queries');
expect(doc).toContain('## Related Tools');
expect(doc).toContain('- list_nodes');
});
});
describe('special documentation topics', () => {
it('should return JavaScript Code node guide for javascript_code_node_guide', () => {
const doc = getToolDocumentation('javascript_code_node_guide', 'essentials');
expect(doc).toContain('# JavaScript Code Node Guide');
expect(doc).toContain('$input.all()');
expect(doc).toContain('DateTime');
});
it('should return Python Code node guide for python_code_node_guide', () => {
const doc = getToolDocumentation('python_code_node_guide', 'essentials');
expect(doc).toContain('# Python Code Node Guide');
expect(doc).toContain('_input.all()');
expect(doc).toContain('_json');
});
it('should return full JavaScript guide when requested', () => {
const doc = getToolDocumentation('javascript_code_node_guide', 'full');
expect(doc).toContain('# JavaScript Code Node Complete Guide');
expect(doc).toContain('## Data Access Patterns');
expect(doc).toContain('## Available Built-in Functions');
expect(doc).toContain('$helpers.httpRequest');
});
it('should return full Python guide when requested', () => {
const doc = getToolDocumentation('python_code_node_guide', 'full');
expect(doc).toContain('# Python Code Node Complete Guide');
expect(doc).toContain('## Available Built-in Modules');
expect(doc).toContain('## Limitations & Workarounds');
expect(doc).toContain('import json');
});
});
});
describe('getToolsOverview', () => {
describe('essentials mode', () => {
it('should return essential overview with categories', () => {
const overview = getToolsOverview('essentials');
expect(overview).toContain('# n8n MCP Tools Reference');
expect(overview).toContain('## Important: Compatibility Notice');
expect(overview).toContain('n8n version 1.103.2');
expect(overview).toContain('## Code Node Configuration');
expect(overview).toContain('## Standard Workflow Pattern');
expect(overview).toContain('**Discovery Tools**');
expect(overview).toContain('**Configuration Tools**');
expect(overview).toContain('**Validation Tools**');
expect(overview).toContain('## Performance Characteristics');
expect(overview).toContain('- Instant (<10ms)');
expect(overview).toContain('tools_documentation({topic: "tool_name", depth: "full"})');
});
it('should use essentials as default', () => {
const overviewDefault = getToolsOverview();
const overviewEssentials = getToolsOverview('essentials');
expect(overviewDefault).toBe(overviewEssentials);
});
});
describe('full mode', () => {
it('should return complete overview with all tools', () => {
const overview = getToolsOverview('full');
expect(overview).toContain('# n8n MCP Tools - Complete Reference');
expect(overview).toContain('## All Available Tools by Category');
expect(overview).toContain('### Discovery');
expect(overview).toContain('- **search_nodes**: Search nodes by keywords');
expect(overview).toContain('### Validation');
expect(overview).toContain('- **validate_workflow**: Validate complete workflow structure');
expect(overview).toContain('## Usage Notes');
});
});
});
describe('searchToolDocumentation', () => {
it('should find tools matching keyword in name', () => {
const results = searchToolDocumentation('search');
expect(results).toContain('search_nodes');
});
it('should find tools matching keyword in description', () => {
const results = searchToolDocumentation('workflow');
expect(results).toContain('validate_workflow');
});
it('should be case insensitive', () => {
const resultsLower = searchToolDocumentation('search');
const resultsUpper = searchToolDocumentation('SEARCH');
expect(resultsLower).toEqual(resultsUpper);
});
it('should return empty array for no matches', () => {
const results = searchToolDocumentation('nonexistentxyz123');
expect(results).toEqual([]);
});
it('should search in both essentials and full descriptions', () => {
const results = searchToolDocumentation('validation');
expect(results.length).toBeGreaterThan(0);
});
});
describe('getToolsByCategory', () => {
it('should return tools for discovery category', () => {
const tools = getToolsByCategory('discovery');
expect(tools).toContain('search_nodes');
});
it('should return tools for validation category', () => {
const tools = getToolsByCategory('validation');
expect(tools).toContain('validate_workflow');
});
it('should return tools for configuration category', () => {
const tools = getToolsByCategory('configuration');
expect(tools).toContain('get_node_essentials');
});
it('should return empty array for unknown category', () => {
const tools = getToolsByCategory('unknown_category');
expect(tools).toEqual([]);
});
});
describe('getAllCategories', () => {
it('should return all unique categories', () => {
const categories = getAllCategories();
expect(categories).toContain('discovery');
expect(categories).toContain('validation');
expect(categories).toContain('configuration');
});
it('should not have duplicates', () => {
const categories = getAllCategories();
const uniqueCategories = new Set(categories);
expect(categories.length).toBe(uniqueCategories.size);
});
it('should return non-empty array', () => {
const categories = getAllCategories();
expect(categories.length).toBeGreaterThan(0);
});
});
describe('Error Handling', () => {
it('should handle missing tool gracefully', () => {
const doc = getToolDocumentation('missing_tool');
expect(doc).toContain("Tool 'missing_tool' not found");
expect(doc).toContain('Use tools_documentation()');
});
it('should handle empty search query', () => {
const results = searchToolDocumentation('');
// Should match all tools since empty string is in everything
expect(results.length).toBeGreaterThan(0);
});
});
describe('Documentation Quality', () => {
it('should format parameters correctly in full mode', () => {
const doc = getToolDocumentation('search_nodes', 'full');
// Check parameter formatting
expect(doc).toMatch(/- \*\*query\*\* \(string, required\): Search terms/);
expect(doc).toMatch(/- \*\*mode\*\* \(string\): Search mode/);
expect(doc).toMatch(/- \*\*limit\*\* \(number\): Max results/);
});
it('should include code blocks for examples', () => {
const doc = getToolDocumentation('search_nodes', 'full');
expect(doc).toContain('```javascript');
expect(doc).toContain('```');
});
it('should have consistent section headers', () => {
const doc = getToolDocumentation('search_nodes', 'full');
const expectedSections = [
'## Parameters',
'## Returns',
'## Examples',
'## Common Use Cases',
'## Performance',
'## Best Practices',
'## Common Pitfalls',
'## Related Tools'
];
expectedSections.forEach(section => {
expect(doc).toContain(section);
});
});
});
});

View File

@@ -0,0 +1,320 @@
import { describe, it, expect } from 'vitest';
import { n8nDocumentationToolsFinal } from '@/mcp/tools';
import { z } from 'zod';
describe('n8nDocumentationToolsFinal', () => {
describe('Tool Structure Validation', () => {
it('should have all required properties for each tool', () => {
n8nDocumentationToolsFinal.forEach(tool => {
// Check required properties exist
expect(tool).toHaveProperty('name');
expect(tool).toHaveProperty('description');
expect(tool).toHaveProperty('inputSchema');
// Check property types
expect(typeof tool.name).toBe('string');
expect(typeof tool.description).toBe('string');
expect(tool.inputSchema).toBeTypeOf('object');
// Name should be non-empty
expect(tool.name.length).toBeGreaterThan(0);
// Description should be meaningful
expect(tool.description.length).toBeGreaterThan(10);
});
});
it('should have unique tool names', () => {
const names = n8nDocumentationToolsFinal.map(tool => tool.name);
const uniqueNames = new Set(names);
expect(names.length).toBe(uniqueNames.size);
});
it('should have valid JSON Schema for all inputSchemas', () => {
// Define a minimal JSON Schema validator using Zod
const jsonSchemaValidator = z.object({
type: z.literal('object'),
properties: z.record(z.any()).optional(),
required: z.array(z.string()).optional(),
});
n8nDocumentationToolsFinal.forEach(tool => {
expect(() => {
jsonSchemaValidator.parse(tool.inputSchema);
}).not.toThrow();
});
});
});
describe('Individual Tool Validation', () => {
describe('tools_documentation', () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'tools_documentation');
it('should exist', () => {
expect(tool).toBeDefined();
});
it('should have correct schema', () => {
expect(tool?.inputSchema).toMatchObject({
type: 'object',
properties: {
topic: {
type: 'string',
description: expect.any(String)
},
depth: {
type: 'string',
enum: ['essentials', 'full'],
description: expect.any(String),
default: 'essentials'
}
}
});
});
it('should have helpful description', () => {
expect(tool?.description).toContain('documentation');
expect(tool?.description).toContain('MCP tools');
});
});
describe('list_nodes', () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'list_nodes');
it('should exist', () => {
expect(tool).toBeDefined();
});
it('should have correct schema properties', () => {
const properties = tool?.inputSchema.properties;
expect(properties).toHaveProperty('package');
expect(properties).toHaveProperty('category');
expect(properties).toHaveProperty('developmentStyle');
expect(properties).toHaveProperty('isAITool');
expect(properties).toHaveProperty('limit');
});
it('should have correct defaults', () => {
expect(tool?.inputSchema.properties.limit.default).toBe(50);
});
it('should have proper enum values', () => {
expect(tool?.inputSchema.properties.developmentStyle.enum).toEqual(['declarative', 'programmatic']);
});
});
describe('get_node_info', () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_node_info');
it('should exist', () => {
expect(tool).toBeDefined();
});
it('should have nodeType as required parameter', () => {
expect(tool?.inputSchema.required).toContain('nodeType');
});
it('should mention performance implications in description', () => {
expect(tool?.description).toMatch(/100KB\+|large|full/i);
});
});
describe('search_nodes', () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_nodes');
it('should exist', () => {
expect(tool).toBeDefined();
});
it('should have query as required parameter', () => {
expect(tool?.inputSchema.required).toContain('query');
});
it('should have mode enum with correct values', () => {
expect(tool?.inputSchema.properties.mode.enum).toEqual(['OR', 'AND', 'FUZZY']);
expect(tool?.inputSchema.properties.mode.default).toBe('OR');
});
it('should have limit with default value', () => {
expect(tool?.inputSchema.properties.limit.default).toBe(20);
});
});
describe('validate_workflow', () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'validate_workflow');
it('should exist', () => {
expect(tool).toBeDefined();
});
it('should have workflow as required parameter', () => {
expect(tool?.inputSchema.required).toContain('workflow');
});
it('should have options with correct validation settings', () => {
const options = tool?.inputSchema.properties.options.properties;
expect(options).toHaveProperty('validateNodes');
expect(options).toHaveProperty('validateConnections');
expect(options).toHaveProperty('validateExpressions');
expect(options).toHaveProperty('profile');
});
it('should have correct profile enum values', () => {
const profile = tool?.inputSchema.properties.options.properties.profile;
expect(profile.enum).toEqual(['minimal', 'runtime', 'ai-friendly', 'strict']);
expect(profile.default).toBe('runtime');
});
});
describe('get_templates_for_task', () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_templates_for_task');
it('should exist', () => {
expect(tool).toBeDefined();
});
it('should have task as required parameter', () => {
expect(tool?.inputSchema.required).toContain('task');
});
it('should have correct task enum values', () => {
const expectedTasks = [
'ai_automation',
'data_sync',
'webhook_processing',
'email_automation',
'slack_integration',
'data_transformation',
'file_processing',
'scheduling',
'api_integration',
'database_operations'
];
expect(tool?.inputSchema.properties.task.enum).toEqual(expectedTasks);
});
});
});
describe('Tool Description Quality', () => {
it('should have concise descriptions that fit in one line', () => {
n8nDocumentationToolsFinal.forEach(tool => {
// Descriptions should be informative but not overly long
expect(tool.description.length).toBeLessThan(300);
});
});
it('should include examples or key information in descriptions', () => {
const toolsWithExamples = [
'list_nodes',
'get_node_info',
'search_nodes',
'get_node_essentials',
'get_node_documentation'
];
toolsWithExamples.forEach(toolName => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
// Should include either example usage, format information, or "nodes-base"
expect(tool?.description).toMatch(/example|Example|format|Format|nodes-base|Common:/i);
});
});
});
describe('Schema Consistency', () => {
it('should use consistent parameter naming', () => {
const toolsWithNodeType = n8nDocumentationToolsFinal.filter(tool =>
tool.inputSchema.properties?.nodeType
);
toolsWithNodeType.forEach(tool => {
const nodeTypeParam = tool.inputSchema.properties.nodeType;
expect(nodeTypeParam.type).toBe('string');
// Should mention the prefix requirement
expect(nodeTypeParam.description).toMatch(/nodes-base|prefix/i);
});
});
it('should have consistent limit parameter defaults', () => {
const toolsWithLimit = n8nDocumentationToolsFinal.filter(tool =>
tool.inputSchema.properties?.limit
);
toolsWithLimit.forEach(tool => {
const limitParam = tool.inputSchema.properties.limit;
expect(limitParam.type).toBe('number');
expect(limitParam.default).toBeDefined();
expect(limitParam.default).toBeGreaterThan(0);
});
});
});
describe('Tool Categories Coverage', () => {
it('should have tools for all major categories', () => {
const categories = {
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'],
documentation: ['tools_documentation']
};
Object.entries(categories).forEach(([category, expectedTools]) => {
expectedTools.forEach(toolName => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
expect(tool).toBeDefined();
});
});
});
});
describe('Parameter Validation', () => {
it('should have proper type definitions for all parameters', () => {
const validTypes = ['string', 'number', 'boolean', 'object', 'array'];
n8nDocumentationToolsFinal.forEach(tool => {
if (tool.inputSchema.properties) {
Object.entries(tool.inputSchema.properties).forEach(([paramName, param]) => {
expect(validTypes).toContain(param.type);
expect(param.description).toBeDefined();
});
}
});
});
it('should mark required parameters correctly', () => {
const toolsWithRequired = n8nDocumentationToolsFinal.filter(tool =>
tool.inputSchema.required && tool.inputSchema.required.length > 0
);
toolsWithRequired.forEach(tool => {
tool.inputSchema.required.forEach(requiredParam => {
expect(tool.inputSchema.properties).toHaveProperty(requiredParam);
});
});
});
});
describe('Edge Cases', () => {
it('should handle tools with no parameters', () => {
const toolsWithNoParams = ['list_ai_tools', 'get_database_statistics'];
toolsWithNoParams.forEach(toolName => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
expect(tool).toBeDefined();
expect(Object.keys(tool?.inputSchema.properties || {}).length).toBe(0);
});
});
it('should have array parameters defined correctly', () => {
const toolsWithArrays = ['list_node_templates'];
toolsWithArrays.forEach(toolName => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
const arrayParam = tool?.inputSchema.properties.nodeTypes;
expect(arrayParam?.type).toBe('array');
expect(arrayParam?.items).toBeDefined();
expect(arrayParam?.items.type).toBe('string');
});
});
});
});