import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { DocumentationGenerator, DocumentationSummarySchema, DocumentationGeneratorConfig, DocumentationInput, DocumentationResult, createDocumentationGenerator, } from '../../../src/community/documentation-generator'; // Mock OpenAI vi.mock('openai', () => { return { default: vi.fn().mockImplementation(() => ({ chat: { completions: { create: vi.fn(), }, }, })), }; }); // Mock logger to prevent console output during tests vi.mock('../../../src/utils/logger', () => ({ logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn(), }, })); describe('DocumentationGenerator', () => { let generator: DocumentationGenerator; let mockCreate: ReturnType; const defaultConfig: DocumentationGeneratorConfig = { baseUrl: 'http://localhost:1234/v1', model: 'test-model', apiKey: 'test-key', timeout: 30000, maxTokens: 1000, }; const validSummary = { purpose: 'Sends messages to Slack channels', capabilities: ['Send messages', 'Create channels', 'Upload files'], authentication: 'OAuth2 or API Token', commonUseCases: ['Team notifications', 'Alert systems'], limitations: ['Rate limits apply'], relatedNodes: ['n8n-nodes-base.slack'], }; const sampleInput: DocumentationInput = { nodeType: 'n8n-nodes-community.slack', displayName: 'Slack Community', description: 'A community Slack integration', readme: '# Slack Community Node\n\nThis node allows you to send messages to Slack.', npmPackageName: '@community/n8n-nodes-slack', }; beforeEach(() => { generator = new DocumentationGenerator(defaultConfig); // Get the mocked create function mockCreate = vi.fn(); Object.defineProperty(generator, 'client', { value: { chat: { completions: { create: mockCreate, }, }, }, writable: true, }); }); afterEach(() => { vi.clearAllMocks(); }); describe('Constructor Configuration', () => { it('should use provided configuration values', () => { const config: DocumentationGeneratorConfig = { baseUrl: 'http://custom-server:8080/v1', model: 'custom-model', apiKey: 'custom-key', timeout: 45000, maxTokens: 2500, }; const customGenerator = new DocumentationGenerator(config); // Verify internal properties are set correctly expect(customGenerator['model']).toBe('custom-model'); expect(customGenerator['maxTokens']).toBe(2500); expect(customGenerator['timeout']).toBe(45000); }); it('should apply default values when optional config is omitted', () => { const minimalConfig: DocumentationGeneratorConfig = { baseUrl: 'http://localhost:1234/v1', }; const minimalGenerator = new DocumentationGenerator(minimalConfig); expect(minimalGenerator['model']).toBe('qwen3-4b-thinking-2507'); expect(minimalGenerator['maxTokens']).toBe(2000); expect(minimalGenerator['timeout']).toBe(60000); }); it('should partially override defaults', () => { const partialConfig: DocumentationGeneratorConfig = { baseUrl: 'http://localhost:1234/v1', model: 'custom-model', // apiKey, timeout, maxTokens should use defaults }; const partialGenerator = new DocumentationGenerator(partialConfig); expect(partialGenerator['model']).toBe('custom-model'); expect(partialGenerator['maxTokens']).toBe(2000); expect(partialGenerator['timeout']).toBe(60000); }); }); describe('generateSummary()', () => { describe('Successful generation', () => { it('should generate documentation summary from valid LLM response', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }); const result = await generator.generateSummary(sampleInput); expect(result.nodeType).toBe('n8n-nodes-community.slack'); expect(result.summary.purpose).toBe('Sends messages to Slack channels'); expect(result.summary.capabilities).toEqual(['Send messages', 'Create channels', 'Upload files']); expect(result.summary.authentication).toBe('OAuth2 or API Token'); expect(result.error).toBeUndefined(); }); it('should call OpenAI with correct parameters', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }); await generator.generateSummary(sampleInput); expect(mockCreate).toHaveBeenCalledWith({ model: 'test-model', max_tokens: 1000, temperature: 0.3, messages: expect.arrayContaining([ expect.objectContaining({ role: 'system' }), expect.objectContaining({ role: 'user' }), ]), }); }); it('should include node information in the prompt', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }); await generator.generateSummary(sampleInput); const userMessage = mockCreate.mock.calls[0][0].messages[1].content; expect(userMessage).toContain('Slack Community'); expect(userMessage).toContain('n8n-nodes-community.slack'); expect(userMessage).toContain('@community/n8n-nodes-slack'); expect(userMessage).toContain('A community Slack integration'); }); }); describe('JSON extraction from markdown code blocks', () => { it('should extract JSON from markdown json code block', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: '```json\n' + JSON.stringify(validSummary) + '\n```', }, }, ], }); const result = await generator.generateSummary(sampleInput); expect(result.error).toBeUndefined(); expect(result.summary.purpose).toBe('Sends messages to Slack channels'); }); it('should extract JSON from generic markdown code block', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: '```\n' + JSON.stringify(validSummary) + '\n```', }, }, ], }); const result = await generator.generateSummary(sampleInput); expect(result.error).toBeUndefined(); expect(result.summary.purpose).toBe('Sends messages to Slack channels'); }); it('should extract JSON object directly from response text', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: 'Here is the summary:\n' + JSON.stringify(validSummary) + '\n\nDone.', }, }, ], }); const result = await generator.generateSummary(sampleInput); expect(result.error).toBeUndefined(); expect(result.summary.purpose).toBe('Sends messages to Slack channels'); }); it('should handle JSON with extra whitespace in code block', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: '```json\n \n' + JSON.stringify(validSummary, null, 2) + '\n \n```', }, }, ], }); const result = await generator.generateSummary(sampleInput); expect(result.error).toBeUndefined(); expect(result.summary.purpose).toBe('Sends messages to Slack channels'); }); }); describe('Error handling', () => { it('should return default summary when LLM returns no content', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: null, }, }, ], }); const result = await generator.generateSummary(sampleInput); expect(result.error).toBe('No content in LLM response'); expect(result.summary.purpose).toBe('A community Slack integration'); expect(result.summary.limitations).toContain('Documentation could not be automatically generated'); }); it('should return default summary when LLM returns empty content', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: '', }, }, ], }); const result = await generator.generateSummary(sampleInput); expect(result.error).toBeDefined(); expect(result.summary.limitations).toContain('Documentation could not be automatically generated'); }); it('should return default summary when LLM returns invalid JSON', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: 'This is not valid JSON at all', }, }, ], }); const result = await generator.generateSummary(sampleInput); expect(result.error).toBeDefined(); expect(result.summary.purpose).toBe('A community Slack integration'); }); it('should return default summary when LLM returns malformed JSON', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: '{"purpose": "test", "capabilities": [}', }, }, ], }); const result = await generator.generateSummary(sampleInput); expect(result.error).toBeDefined(); expect(result.nodeType).toBe('n8n-nodes-community.slack'); }); it('should return default summary when choices array is empty', async () => { mockCreate.mockResolvedValue({ choices: [], }); const result = await generator.generateSummary(sampleInput); expect(result.error).toBe('No content in LLM response'); }); it('should return default summary on network error', async () => { mockCreate.mockRejectedValue(new Error('Connection refused')); const result = await generator.generateSummary(sampleInput); expect(result.error).toBe('Connection refused'); expect(result.summary.limitations).toContain('Documentation could not be automatically generated'); }); it('should return default summary on timeout', async () => { mockCreate.mockRejectedValue(new Error('Request timed out')); const result = await generator.generateSummary(sampleInput); expect(result.error).toBe('Request timed out'); }); it('should return default summary when Zod validation fails', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify({ purpose: 'Valid purpose', capabilities: 'not-an-array', // Should be array authentication: 'API key', commonUseCases: [], limitations: [], relatedNodes: [], }), }, }, ], }); const result = await generator.generateSummary(sampleInput); expect(result.error).toBeDefined(); expect(result.summary.limitations).toContain('Documentation could not be automatically generated'); }); it('should use node display name in default summary when description is missing', async () => { mockCreate.mockRejectedValue(new Error('API error')); const inputWithoutDescription: DocumentationInput = { nodeType: 'n8n-nodes-community.custom', displayName: 'Custom Node', readme: '# Custom Node', }; const result = await generator.generateSummary(inputWithoutDescription); expect(result.summary.purpose).toBe('Community node: Custom Node'); }); it('should handle non-Error exceptions', async () => { mockCreate.mockRejectedValue('String error'); const result = await generator.generateSummary(sampleInput); expect(result.error).toBe('Unknown error'); }); }); }); describe('generateBatch()', () => { const createInputs = (count: number): DocumentationInput[] => { return Array.from({ length: count }, (_, i) => ({ nodeType: `n8n-nodes-community.node${i}`, displayName: `Node ${i}`, description: `Description for node ${i}`, readme: `# Node ${i} README`, })); }; it('should process multiple nodes in parallel', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }); const inputs = createInputs(5); const results = await generator.generateBatch(inputs, 2); expect(results).toHaveLength(5); results.forEach((result, i) => { expect(result.nodeType).toBe(`n8n-nodes-community.node${i}`); }); }); it('should respect concurrency limit', async () => { const callOrder: number[] = []; let currentConcurrency = 0; let maxConcurrency = 0; mockCreate.mockImplementation(async () => { currentConcurrency++; maxConcurrency = Math.max(maxConcurrency, currentConcurrency); callOrder.push(currentConcurrency); await new Promise((resolve) => setTimeout(resolve, 50)); currentConcurrency--; return { choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }; }); const inputs = createInputs(6); await generator.generateBatch(inputs, 2); // Max concurrency should not exceed the limit expect(maxConcurrency).toBeLessThanOrEqual(2); }); it('should call progress callback with correct values', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }); const progressCalls: Array<{ message: string; current: number; total: number }> = []; const progressCallback = (message: string, current: number, total: number) => { progressCalls.push({ message, current, total }); }; const inputs = createInputs(5); await generator.generateBatch(inputs, 2, progressCallback); expect(progressCalls.length).toBeGreaterThan(0); expect(progressCalls[0].message).toBe('Generating documentation'); expect(progressCalls[progressCalls.length - 1].current).toBe(5); expect(progressCalls[progressCalls.length - 1].total).toBe(5); }); it('should handle mixed success and failure results', async () => { let callCount = 0; mockCreate.mockImplementation(async () => { callCount++; if (callCount % 2 === 0) { throw new Error('Simulated failure'); } return { choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }; }); const inputs = createInputs(4); const results = await generator.generateBatch(inputs, 2); expect(results).toHaveLength(4); const successCount = results.filter((r) => !r.error).length; const errorCount = results.filter((r) => r.error).length; expect(successCount).toBe(2); expect(errorCount).toBe(2); }); it('should handle empty input array', async () => { const results = await generator.generateBatch([], 3); expect(results).toHaveLength(0); expect(mockCreate).not.toHaveBeenCalled(); }); it('should handle single item input', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }); const inputs = createInputs(1); const results = await generator.generateBatch(inputs, 3); expect(results).toHaveLength(1); expect(mockCreate).toHaveBeenCalledTimes(1); }); it('should use default concurrency of 3', async () => { let maxConcurrency = 0; let currentConcurrency = 0; mockCreate.mockImplementation(async () => { currentConcurrency++; maxConcurrency = Math.max(maxConcurrency, currentConcurrency); await new Promise((resolve) => setTimeout(resolve, 10)); currentConcurrency--; return { choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }; }); const inputs = createInputs(9); await generator.generateBatch(inputs); expect(maxConcurrency).toBeLessThanOrEqual(3); }); }); describe('testConnection()', () => { it('should return success when LLM responds', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: 'Hello!', }, }, ], }); const result = await generator.testConnection(); expect(result.success).toBe(true); expect(result.message).toBe('Connected to test-model'); }); it('should return failure when LLM returns empty response', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: null, }, }, ], }); const result = await generator.testConnection(); expect(result.success).toBe(false); expect(result.message).toBe('No response from LLM'); }); it('should return failure when LLM returns empty string', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: '', }, }, ], }); const result = await generator.testConnection(); expect(result.success).toBe(false); expect(result.message).toBe('No response from LLM'); }); it('should return failure when choices array is empty', async () => { mockCreate.mockResolvedValue({ choices: [], }); const result = await generator.testConnection(); expect(result.success).toBe(false); expect(result.message).toBe('No response from LLM'); }); it('should return failure on connection error', async () => { mockCreate.mockRejectedValue(new Error('Connection refused')); const result = await generator.testConnection(); expect(result.success).toBe(false); expect(result.message).toBe('Connection failed: Connection refused'); }); it('should return failure on timeout', async () => { mockCreate.mockRejectedValue(new Error('Request timed out')); const result = await generator.testConnection(); expect(result.success).toBe(false); expect(result.message).toBe('Connection failed: Request timed out'); }); it('should handle non-Error exceptions', async () => { mockCreate.mockRejectedValue('Network failure'); const result = await generator.testConnection(); expect(result.success).toBe(false); expect(result.message).toBe('Connection failed: Unknown error'); }); it('should use minimal tokens for connection test', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: 'Hi', }, }, ], }); await generator.testConnection(); expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ max_tokens: 10, messages: [ { role: 'user', content: 'Hello', }, ], }) ); }); }); describe('DocumentationSummarySchema validation', () => { it('should validate correct documentation summary', () => { const result = DocumentationSummarySchema.safeParse(validSummary); expect(result.success).toBe(true); }); it('should reject missing required fields', () => { const incompleteSummary = { purpose: 'Test purpose', capabilities: ['test'], // Missing: authentication, commonUseCases, limitations, relatedNodes }; const result = DocumentationSummarySchema.safeParse(incompleteSummary); expect(result.success).toBe(false); }); it('should enforce capabilities array max length of 10', () => { const tooManyCapabilities = { ...validSummary, capabilities: Array.from({ length: 11 }, (_, i) => `capability${i}`), }; const result = DocumentationSummarySchema.safeParse(tooManyCapabilities); expect(result.success).toBe(false); }); it('should enforce commonUseCases array max length of 5', () => { const tooManyUseCases = { ...validSummary, commonUseCases: Array.from({ length: 6 }, (_, i) => `useCase${i}`), }; const result = DocumentationSummarySchema.safeParse(tooManyUseCases); expect(result.success).toBe(false); }); it('should accept empty arrays for optional fields', () => { const minimalSummary = { purpose: 'Minimal node', capabilities: [], authentication: 'None', commonUseCases: [], limitations: [], relatedNodes: [], }; const result = DocumentationSummarySchema.safeParse(minimalSummary); expect(result.success).toBe(true); }); it('should reject non-string values in arrays', () => { const invalidSummary = { ...validSummary, capabilities: [1, 2, 3], }; const result = DocumentationSummarySchema.safeParse(invalidSummary); expect(result.success).toBe(false); }); it('should reject non-string purpose', () => { const invalidSummary = { ...validSummary, purpose: { text: 'purpose' }, }; const result = DocumentationSummarySchema.safeParse(invalidSummary); expect(result.success).toBe(false); }); it('should reject null values', () => { const invalidSummary = { ...validSummary, authentication: null, }; const result = DocumentationSummarySchema.safeParse(invalidSummary); expect(result.success).toBe(false); }); }); describe('createDocumentationGenerator factory', () => { const originalEnv = process.env; beforeEach(() => { process.env = { ...originalEnv }; }); afterEach(() => { process.env = originalEnv; }); it('should use environment variables when set', () => { process.env.N8N_MCP_LLM_BASE_URL = 'http://custom:9999/v1'; process.env.N8N_MCP_LLM_MODEL = 'custom-model'; process.env.N8N_MCP_LLM_TIMEOUT = '90000'; const factoryGenerator = createDocumentationGenerator(); expect(factoryGenerator['model']).toBe('custom-model'); expect(factoryGenerator['timeout']).toBe(90000); }); it('should use default values when environment variables are not set', () => { delete process.env.N8N_MCP_LLM_BASE_URL; delete process.env.N8N_MCP_LLM_MODEL; delete process.env.N8N_MCP_LLM_TIMEOUT; const factoryGenerator = createDocumentationGenerator(); expect(factoryGenerator['model']).toBe('qwen3-4b-thinking-2507'); expect(factoryGenerator['timeout']).toBe(60000); }); }); describe('Private method behaviors', () => { describe('truncateReadme', () => { it('should not truncate short README', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }); const shortReadme = 'Short README content'; const input = { ...sampleInput, readme: shortReadme }; await generator.generateSummary(input); const userMessage = mockCreate.mock.calls[0][0].messages[1].content; expect(userMessage).toContain(shortReadme); expect(userMessage).not.toContain('[README truncated...]'); }); it('should truncate very long README', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }); const longReadme = 'A'.repeat(10000); const input = { ...sampleInput, readme: longReadme }; await generator.generateSummary(input); const userMessage = mockCreate.mock.calls[0][0].messages[1].content; expect(userMessage).toContain('[README truncated...]'); }); it('should try to truncate at paragraph boundary', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }); const readmeWithParagraphs = 'A'.repeat(5000) + '\n\n' + 'B'.repeat(2000); const input = { ...sampleInput, readme: readmeWithParagraphs }; await generator.generateSummary(input); const userMessage = mockCreate.mock.calls[0][0].messages[1].content; expect(userMessage).toContain('[README truncated...]'); }); }); describe('buildPrompt', () => { it('should handle missing optional fields', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }); const minimalInput: DocumentationInput = { nodeType: 'n8n-nodes-community.minimal', displayName: 'Minimal Node', readme: '# Minimal', }; await generator.generateSummary(minimalInput); const userMessage = mockCreate.mock.calls[0][0].messages[1].content; expect(userMessage).toContain('Package: unknown'); expect(userMessage).toContain('Description: No description provided'); }); }); }); describe('Edge cases and security', () => { it('should handle README with special characters', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }); const specialReadme = '# Node with "quotes" and `backticks` and tags'; const input = { ...sampleInput, readme: specialReadme }; const result = await generator.generateSummary(input); expect(result.error).toBeUndefined(); }); it('should handle Unicode content in README', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary), }, }, ], }); const unicodeReadme = '# Node with Unicode: emoji, Chinese: 中文, Arabic: العربية'; const input = { ...sampleInput, readme: unicodeReadme }; await generator.generateSummary(input); const userMessage = mockCreate.mock.calls[0][0].messages[1].content; expect(userMessage).toContain('中文'); expect(userMessage).toContain('العربية'); }); it('should handle LLM response with thinking tokens', async () => { mockCreate.mockResolvedValue({ choices: [ { message: { content: 'Let me analyze this...\n```json\n' + JSON.stringify(validSummary) + '\n```', }, }, ], }); const result = await generator.generateSummary(sampleInput); expect(result.error).toBeUndefined(); expect(result.summary.purpose).toBe('Sends messages to Slack channels'); }); it('should return error when response contains multiple JSON objects', async () => { // When the response contains multiple JSON objects concatenated, // JSON.parse will fail, and we should get a default summary with error mockCreate.mockResolvedValue({ choices: [ { message: { content: JSON.stringify(validSummary) + '\n\n' + JSON.stringify({ other: 'data' }), }, }, ], }); const result = await generator.generateSummary(sampleInput); // This should fail to parse and return default summary with error expect(result.error).toBeDefined(); expect(result.summary.limitations).toContain('Documentation could not be automatically generated'); }); }); describe('truncateArrayFields helper', () => { it('should truncate capabilities array to 10 items', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const input = { purpose: 'Test purpose', capabilities: Array.from({ length: 15 }, (_, i) => `capability${i}`), authentication: 'None', commonUseCases: [], limitations: [], relatedNodes: [], }; const result = truncateMethod(input); expect(result.capabilities).toHaveLength(10); expect(result.capabilities[0]).toBe('capability0'); expect(result.capabilities[9]).toBe('capability9'); }); it('should truncate commonUseCases array to 5 items', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const input = { purpose: 'Test purpose', capabilities: [], authentication: 'None', commonUseCases: Array.from({ length: 8 }, (_, i) => `useCase${i}`), limitations: [], relatedNodes: [], }; const result = truncateMethod(input); expect(result.commonUseCases).toHaveLength(5); expect(result.commonUseCases[0]).toBe('useCase0'); expect(result.commonUseCases[4]).toBe('useCase4'); }); it('should truncate limitations array to 5 items', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const input = { purpose: 'Test purpose', capabilities: [], authentication: 'None', commonUseCases: [], limitations: Array.from({ length: 10 }, (_, i) => `limitation${i}`), relatedNodes: [], }; const result = truncateMethod(input); expect(result.limitations).toHaveLength(5); expect(result.limitations[0]).toBe('limitation0'); expect(result.limitations[4]).toBe('limitation4'); }); it('should truncate relatedNodes array to 5 items', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const input = { purpose: 'Test purpose', capabilities: [], authentication: 'None', commonUseCases: [], limitations: [], relatedNodes: Array.from({ length: 7 }, (_, i) => `node${i}`), }; const result = truncateMethod(input); expect(result.relatedNodes).toHaveLength(5); expect(result.relatedNodes[0]).toBe('node0'); expect(result.relatedNodes[4]).toBe('node4'); }); it('should not modify arrays within limits', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const input = { purpose: 'Test purpose', capabilities: ['cap1', 'cap2', 'cap3'], authentication: 'None', commonUseCases: ['use1', 'use2'], limitations: ['lim1'], relatedNodes: [], }; const result = truncateMethod(input); expect(result.capabilities).toHaveLength(3); expect(result.commonUseCases).toHaveLength(2); expect(result.limitations).toHaveLength(1); expect(result.relatedNodes).toHaveLength(0); }); it('should not modify arrays at exact limits', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const input = { purpose: 'Test purpose', capabilities: Array.from({ length: 10 }, (_, i) => `cap${i}`), authentication: 'None', commonUseCases: Array.from({ length: 5 }, (_, i) => `use${i}`), limitations: Array.from({ length: 5 }, (_, i) => `lim${i}`), relatedNodes: Array.from({ length: 5 }, (_, i) => `node${i}`), }; const result = truncateMethod(input); expect(result.capabilities).toHaveLength(10); expect(result.commonUseCases).toHaveLength(5); expect(result.limitations).toHaveLength(5); expect(result.relatedNodes).toHaveLength(5); }); it('should handle empty arrays', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const input = { purpose: 'Test purpose', capabilities: [], authentication: 'None', commonUseCases: [], limitations: [], relatedNodes: [], }; const result = truncateMethod(input); expect(result.capabilities).toHaveLength(0); expect(result.commonUseCases).toHaveLength(0); expect(result.limitations).toHaveLength(0); expect(result.relatedNodes).toHaveLength(0); }); it('should preserve non-array fields unchanged', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const input = { purpose: 'This is a long purpose string', capabilities: Array.from({ length: 15 }, (_, i) => `cap${i}`), authentication: 'OAuth2 with refresh token', commonUseCases: [], limitations: [], relatedNodes: [], extraField: 'should be preserved', }; const result = truncateMethod(input); expect(result.purpose).toBe('This is a long purpose string'); expect(result.authentication).toBe('OAuth2 with refresh token'); expect(result.extraField).toBe('should be preserved'); }); it('should handle missing array fields gracefully', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const input = { purpose: 'Test purpose', authentication: 'None', // Missing: capabilities, commonUseCases, limitations, relatedNodes }; const result = truncateMethod(input); expect(result.purpose).toBe('Test purpose'); expect(result.authentication).toBe('None'); expect(result.capabilities).toBeUndefined(); }); it('should truncate multiple arrays simultaneously', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const input = { purpose: 'Test purpose', capabilities: Array.from({ length: 12 }, (_, i) => `cap${i}`), authentication: 'None', commonUseCases: Array.from({ length: 8 }, (_, i) => `use${i}`), limitations: Array.from({ length: 6 }, (_, i) => `lim${i}`), relatedNodes: Array.from({ length: 10 }, (_, i) => `node${i}`), }; const result = truncateMethod(input); expect(result.capabilities).toHaveLength(10); expect(result.commonUseCases).toHaveLength(5); expect(result.limitations).toHaveLength(5); expect(result.relatedNodes).toHaveLength(5); }); it('should preserve order of items when truncating', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const input = { purpose: 'Test purpose', capabilities: ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth', 'tenth', 'eleventh', 'twelfth'], authentication: 'None', commonUseCases: [], limitations: [], relatedNodes: [], }; const result = truncateMethod(input); expect(result.capabilities[0]).toBe('first'); expect(result.capabilities[9]).toBe('tenth'); expect(result.capabilities).not.toContain('eleventh'); expect(result.capabilities).not.toContain('twelfth'); }); it('should handle non-string array items', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const input = { purpose: 'Test purpose', capabilities: Array.from({ length: 12 }, (_, i) => ({ id: i, name: `cap${i}` })), authentication: 'None', commonUseCases: [], limitations: [], relatedNodes: [], }; const result = truncateMethod(input); expect(result.capabilities).toHaveLength(10); expect(result.capabilities[0]).toEqual({ id: 0, name: 'cap0' }); expect(result.capabilities[9]).toEqual({ id: 9, name: 'cap9' }); }); it('should create a new object and not mutate the original', () => { const truncateMethod = (generator as any).truncateArrayFields.bind(generator); const original = { purpose: 'Test purpose', capabilities: Array.from({ length: 15 }, (_, i) => `cap${i}`), authentication: 'None', commonUseCases: [], limitations: [], relatedNodes: [], }; const originalCapabilitiesLength = original.capabilities.length; const result = truncateMethod(original); // Original should not be mutated expect(original.capabilities).toHaveLength(originalCapabilitiesLength); // Result should be truncated expect(result.capabilities).toHaveLength(10); // They should not be the same reference expect(result).not.toBe(original); }); }); });