metadata-generator.test.ts•15.2 kB
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MetadataGenerator, TemplateMetadataSchema, MetadataRequest } from '../../../src/templates/metadata-generator';
// Mock OpenAI
vi.mock('openai', () => {
return {
default: vi.fn().mockImplementation(() => ({
chat: {
completions: {
create: vi.fn()
}
}
}))
};
});
describe('MetadataGenerator', () => {
let generator: MetadataGenerator;
beforeEach(() => {
generator = new MetadataGenerator('test-api-key', 'gpt-5-mini-2025-08-07');
});
describe('createBatchRequest', () => {
it('should create a valid batch request', () => {
const template: MetadataRequest = {
templateId: 123,
name: 'Test Workflow',
description: 'A test workflow',
nodes: ['n8n-nodes-base.webhook', 'n8n-nodes-base.httpRequest', 'n8n-nodes-base.slack']
};
const request = generator.createBatchRequest(template);
expect(request.custom_id).toBe('template-123');
expect(request.method).toBe('POST');
expect(request.url).toBe('/v1/chat/completions');
expect(request.body.model).toBe('gpt-5-mini-2025-08-07');
expect(request.body.response_format.type).toBe('json_schema');
expect(request.body.response_format.json_schema.strict).toBe(true);
expect(request.body.messages).toHaveLength(2);
});
it('should summarize nodes effectively', () => {
const template: MetadataRequest = {
templateId: 456,
name: 'Complex Workflow',
nodes: [
'n8n-nodes-base.webhook',
'n8n-nodes-base.httpRequest',
'n8n-nodes-base.httpRequest',
'n8n-nodes-base.postgres',
'n8n-nodes-base.slack',
'@n8n/n8n-nodes-langchain.agent'
]
};
const request = generator.createBatchRequest(template);
const userMessage = request.body.messages[1].content;
expect(userMessage).toContain('Complex Workflow');
expect(userMessage).toContain('Nodes Used (6)');
expect(userMessage).toContain('HTTP/Webhooks');
});
});
describe('parseResult', () => {
it('should parse a successful result', () => {
const mockResult = {
custom_id: 'template-789',
response: {
body: {
choices: [{
message: {
content: JSON.stringify({
categories: ['automation', 'integration'],
complexity: 'medium',
use_cases: ['API integration', 'Data sync'],
estimated_setup_minutes: 30,
required_services: ['Slack API'],
key_features: ['Webhook triggers', 'API calls'],
target_audience: ['developers']
})
},
finish_reason: 'stop'
}]
}
}
};
const result = generator.parseResult(mockResult);
expect(result.templateId).toBe(789);
expect(result.metadata.categories).toEqual(['automation', 'integration']);
expect(result.metadata.complexity).toBe('medium');
expect(result.error).toBeUndefined();
});
it('should handle error results', () => {
const mockResult = {
custom_id: 'template-999',
error: {
message: 'API error'
}
};
const result = generator.parseResult(mockResult);
expect(result.templateId).toBe(999);
expect(result.error).toBe('API error');
expect(result.metadata).toBeDefined();
expect(result.metadata.complexity).toBe('medium'); // Default metadata
});
it('should handle malformed responses', () => {
const mockResult = {
custom_id: 'template-111',
response: {
body: {
choices: [{
message: {
content: 'not valid json'
},
finish_reason: 'stop'
}]
}
}
};
const result = generator.parseResult(mockResult);
expect(result.templateId).toBe(111);
expect(result.error).toContain('Unexpected token');
expect(result.metadata).toBeDefined();
});
});
describe('TemplateMetadataSchema', () => {
it('should validate correct metadata', () => {
const validMetadata = {
categories: ['automation', 'integration'],
complexity: 'simple' as const,
use_cases: ['API calls', 'Data processing'],
estimated_setup_minutes: 15,
required_services: [],
key_features: ['Fast processing'],
target_audience: ['developers']
};
const result = TemplateMetadataSchema.safeParse(validMetadata);
expect(result.success).toBe(true);
});
it('should reject invalid complexity', () => {
const invalidMetadata = {
categories: ['automation'],
complexity: 'very-hard', // Invalid
use_cases: ['API calls'],
estimated_setup_minutes: 15,
required_services: [],
key_features: ['Fast'],
target_audience: ['developers']
};
const result = TemplateMetadataSchema.safeParse(invalidMetadata);
expect(result.success).toBe(false);
});
it('should enforce array limits', () => {
const tooManyCategories = {
categories: ['a', 'b', 'c', 'd', 'e', 'f'], // Max 5
complexity: 'simple' as const,
use_cases: ['API calls'],
estimated_setup_minutes: 15,
required_services: [],
key_features: ['Fast'],
target_audience: ['developers']
};
const result = TemplateMetadataSchema.safeParse(tooManyCategories);
expect(result.success).toBe(false);
});
it('should enforce time limits', () => {
const tooLongSetup = {
categories: ['automation'],
complexity: 'complex' as const,
use_cases: ['API calls'],
estimated_setup_minutes: 500, // Max 480
required_services: [],
key_features: ['Fast'],
target_audience: ['developers']
};
const result = TemplateMetadataSchema.safeParse(tooLongSetup);
expect(result.success).toBe(false);
});
});
describe('Input Sanitization and Security', () => {
it('should handle malicious template names safely', () => {
const maliciousTemplate: MetadataRequest = {
templateId: 123,
name: '<script>alert("xss")</script>',
description: 'javascript:alert(1)',
nodes: ['n8n-nodes-base.webhook']
};
const request = generator.createBatchRequest(maliciousTemplate);
const userMessage = request.body.messages[1].content;
// Should contain the malicious content as-is (OpenAI will handle it)
// but should not cause any injection in our code
expect(userMessage).toContain('<script>alert("xss")</script>');
expect(userMessage).toContain('javascript:alert(1)');
expect(request.body.model).toBe('gpt-5-mini-2025-08-07');
});
it('should handle extremely long template names', () => {
const longName = 'A'.repeat(10000); // Very long name
const template: MetadataRequest = {
templateId: 456,
name: longName,
nodes: ['n8n-nodes-base.webhook']
};
const request = generator.createBatchRequest(template);
expect(request.custom_id).toBe('template-456');
expect(request.body.messages[1].content).toContain(longName);
});
it('should handle special characters in node names', () => {
const template: MetadataRequest = {
templateId: 789,
name: 'Test Workflow',
nodes: [
'n8n-nodes-base.webhook',
'@n8n/custom-node.with.dots',
'custom-package/node-with-slashes',
'node_with_underscore',
'node-with-unicode-名前'
]
};
const request = generator.createBatchRequest(template);
const userMessage = request.body.messages[1].content;
expect(userMessage).toContain('HTTP/Webhooks');
expect(userMessage).toContain('custom-node.with.dots');
});
it('should handle empty or undefined descriptions safely', () => {
const template: MetadataRequest = {
templateId: 100,
name: 'Test',
description: undefined,
nodes: ['n8n-nodes-base.webhook']
};
const request = generator.createBatchRequest(template);
const userMessage = request.body.messages[1].content;
// Should not include undefined or null in the message
expect(userMessage).not.toContain('undefined');
expect(userMessage).not.toContain('null');
expect(userMessage).toContain('Test');
});
it('should limit context size for very large workflows', () => {
const manyNodes = Array.from({ length: 1000 }, (_, i) => `n8n-nodes-base.node${i}`);
const template: MetadataRequest = {
templateId: 200,
name: 'Huge Workflow',
nodes: manyNodes,
workflow: {
nodes: Array.from({ length: 500 }, (_, i) => ({ id: `node${i}` })),
connections: {}
}
};
const request = generator.createBatchRequest(template);
const userMessage = request.body.messages[1].content;
// Should handle large amounts of data gracefully
expect(userMessage.length).toBeLessThan(50000); // Reasonable limit
expect(userMessage).toContain('Huge Workflow');
});
});
describe('Error Handling and Edge Cases', () => {
it('should handle malformed OpenAI responses', () => {
const malformedResults = [
{
custom_id: 'template-111',
response: {
body: {
choices: [{
message: {
content: '{"invalid": json syntax}'
},
finish_reason: 'stop'
}]
}
}
},
{
custom_id: 'template-222',
response: {
body: {
choices: [{
message: {
content: null
},
finish_reason: 'stop'
}]
}
}
},
{
custom_id: 'template-333',
response: {
body: {
choices: []
}
}
}
];
malformedResults.forEach(result => {
const parsed = generator.parseResult(result);
expect(parsed.error).toBeDefined();
expect(parsed.metadata).toBeDefined();
expect(parsed.metadata.complexity).toBe('medium'); // Default metadata
});
});
it('should handle Zod validation failures', () => {
const invalidResponse = {
custom_id: 'template-444',
response: {
body: {
choices: [{
message: {
content: JSON.stringify({
categories: ['too', 'many', 'categories', 'here', 'way', 'too', 'many'],
complexity: 'invalid-complexity',
use_cases: [],
estimated_setup_minutes: -5, // Invalid negative time
required_services: 'not-an-array',
key_features: null,
target_audience: ['too', 'many', 'audiences', 'here']
})
},
finish_reason: 'stop'
}]
}
}
};
const result = generator.parseResult(invalidResponse);
expect(result.templateId).toBe(444);
expect(result.error).toBeDefined();
expect(result.metadata).toEqual(generator['getDefaultMetadata']());
});
it('should handle network timeouts gracefully in generateSingle', async () => {
// Create a new generator with mocked OpenAI client
const mockClient = {
chat: {
completions: {
create: vi.fn().mockRejectedValue(new Error('Request timed out'))
}
}
};
// Override the client property using Object.defineProperty
Object.defineProperty(generator, 'client', {
value: mockClient,
writable: true
});
const template: MetadataRequest = {
templateId: 555,
name: 'Timeout Test',
nodes: ['n8n-nodes-base.webhook']
};
const result = await generator.generateSingle(template);
// Should return default metadata instead of throwing
expect(result).toEqual(generator['getDefaultMetadata']());
});
});
describe('Node Summarization Logic', () => {
it('should group similar nodes correctly', () => {
const template: MetadataRequest = {
templateId: 666,
name: 'Complex Workflow',
nodes: [
'n8n-nodes-base.webhook',
'n8n-nodes-base.httpRequest',
'n8n-nodes-base.postgres',
'n8n-nodes-base.mysql',
'n8n-nodes-base.slack',
'n8n-nodes-base.gmail',
'@n8n/n8n-nodes-langchain.openAi',
'@n8n/n8n-nodes-langchain.agent',
'n8n-nodes-base.googleSheets',
'n8n-nodes-base.excel'
]
};
const request = generator.createBatchRequest(template);
const userMessage = request.body.messages[1].content;
expect(userMessage).toContain('HTTP/Webhooks (2)');
expect(userMessage).toContain('Database (2)');
expect(userMessage).toContain('Communication (2)');
expect(userMessage).toContain('AI/ML (2)');
expect(userMessage).toContain('Spreadsheets (2)');
});
it('should handle unknown node types gracefully', () => {
const template: MetadataRequest = {
templateId: 777,
name: 'Unknown Nodes',
nodes: [
'custom-package.unknownNode',
'another-package.weirdNodeType',
'someNodeTrigger',
'anotherNode'
]
};
const request = generator.createBatchRequest(template);
const userMessage = request.body.messages[1].content;
// Should handle unknown nodes without crashing
expect(userMessage).toContain('unknownNode');
expect(userMessage).toContain('weirdNodeType');
expect(userMessage).toContain('someNode'); // Trigger suffix removed
});
it('should limit node summary length', () => {
const manyNodes = Array.from({ length: 50 }, (_, i) =>
`n8n-nodes-base.customNode${i}`
);
const template: MetadataRequest = {
templateId: 888,
name: 'Many Nodes',
nodes: manyNodes
};
const request = generator.createBatchRequest(template);
const userMessage = request.body.messages[1].content;
// Should limit to top 10 groups
const summaryLine = userMessage.split('\n').find((line: string) =>
line.includes('Nodes Used (50)')
);
expect(summaryLine).toBeDefined();
const nodeGroups = summaryLine!.split(': ')[1].split(', ');
expect(nodeGroups.length).toBeLessThanOrEqual(10);
});
});
});