diff --git a/src/scripts/debug-http-search.ts b/src/scripts/debug-http-search.ts new file mode 100644 index 0000000..cc35ea1 --- /dev/null +++ b/src/scripts/debug-http-search.ts @@ -0,0 +1,77 @@ +#!/usr/bin/env npx tsx + +import { createDatabaseAdapter } from '../database/database-adapter'; +import { NodeRepository } from '../database/node-repository'; +import { NodeSimilarityService } from '../services/node-similarity-service'; +import path from 'path'; + +async function debugHttpSearch() { + const dbPath = path.join(process.cwd(), 'data/nodes.db'); + const db = await createDatabaseAdapter(dbPath); + const repository = new NodeRepository(db); + const service = new NodeSimilarityService(repository); + + console.log('Testing "http" search...\n'); + + // Check if httpRequest exists + const httpNode = repository.getNode('nodes-base.httpRequest'); + console.log('HTTP Request node exists:', httpNode ? 'Yes' : 'No'); + if (httpNode) { + console.log(' Display name:', httpNode.displayName); + } + + // Test the search with internal details + const suggestions = await service.findSimilarNodes('http', 5); + console.log('\nSuggestions for "http":', suggestions.length); + suggestions.forEach(s => { + console.log(` - ${s.nodeType} (${Math.round(s.confidence * 100)}%)`); + }); + + // Manually calculate score for httpRequest + console.log('\nManual score calculation for httpRequest:'); + const testNode = { + nodeType: 'nodes-base.httpRequest', + displayName: 'HTTP Request', + category: 'Core Nodes' + }; + + const cleanInvalid = 'http'; + const cleanValid = 'nodesbasehttprequest'; + const displayNameClean = 'httprequest'; + + // Check substring + const hasSubstring = cleanValid.includes(cleanInvalid) || displayNameClean.includes(cleanInvalid); + console.log(` Substring match: ${hasSubstring}`); + + // This should give us pattern match score + const patternScore = hasSubstring ? 35 : 0; // Using 35 for short searches + console.log(` Pattern score: ${patternScore}`); + + // Name similarity would be low + console.log(` Total score would need to be >= 50 to appear`); + + // Get all nodes and check which ones contain 'http' + const allNodes = repository.getAllNodes(); + const httpNodes = allNodes.filter(n => + n.nodeType.toLowerCase().includes('http') || + (n.displayName && n.displayName.toLowerCase().includes('http')) + ); + + console.log('\n\nNodes containing "http" in name:'); + httpNodes.slice(0, 5).forEach(n => { + console.log(` - ${n.nodeType} (${n.displayName})`); + + // Calculate score for this node + const normalizedSearch = 'http'; + const normalizedType = n.nodeType.toLowerCase().replace(/[^a-z0-9]/g, ''); + const normalizedDisplay = (n.displayName || '').toLowerCase().replace(/[^a-z0-9]/g, ''); + + const containsInType = normalizedType.includes(normalizedSearch); + const containsInDisplay = normalizedDisplay.includes(normalizedSearch); + + console.log(` Type check: "${normalizedType}" includes "${normalizedSearch}" = ${containsInType}`); + console.log(` Display check: "${normalizedDisplay}" includes "${normalizedSearch}" = ${containsInDisplay}`); + }); +} + +debugHttpSearch().catch(console.error); \ No newline at end of file diff --git a/src/scripts/test-summary.ts b/src/scripts/test-summary.ts new file mode 100644 index 0000000..c59ae8a --- /dev/null +++ b/src/scripts/test-summary.ts @@ -0,0 +1,81 @@ +#!/usr/bin/env npx tsx + +import { createDatabaseAdapter } from '../database/database-adapter'; +import { NodeRepository } from '../database/node-repository'; +import { NodeSimilarityService } from '../services/node-similarity-service'; +import path from 'path'; + +async function testSummary() { + const dbPath = path.join(process.cwd(), 'data/nodes.db'); + const db = await createDatabaseAdapter(dbPath); + const repository = new NodeRepository(db); + const similarityService = new NodeSimilarityService(repository); + + const testCases = [ + { invalid: 'HttpRequest', expected: 'nodes-base.httpRequest' }, + { invalid: 'HTTPRequest', expected: 'nodes-base.httpRequest' }, + { invalid: 'Webhook', expected: 'nodes-base.webhook' }, + { invalid: 'WebHook', expected: 'nodes-base.webhook' }, + { invalid: 'slack', expected: 'nodes-base.slack' }, + { invalid: 'googleSheets', expected: 'nodes-base.googleSheets' }, + { invalid: 'telegram', expected: 'nodes-base.telegram' }, + { invalid: 'htpRequest', expected: 'nodes-base.httpRequest' }, + { invalid: 'webook', expected: 'nodes-base.webhook' }, + { invalid: 'slak', expected: 'nodes-base.slack' }, + { invalid: 'http', expected: 'nodes-base.httpRequest' }, + { invalid: 'sheet', expected: 'nodes-base.googleSheets' }, + { invalid: 'nodes-base.openai', expected: 'nodes-langchain.openAi' }, + { invalid: 'n8n-nodes-base.httpRequest', expected: 'nodes-base.httpRequest' }, + { invalid: 'foobar', expected: null }, + { invalid: 'xyz123', expected: null }, + ]; + + let passed = 0; + let failed = 0; + + console.log('Test Results Summary:'); + console.log('='.repeat(60)); + + for (const testCase of testCases) { + const suggestions = await similarityService.findSimilarNodes(testCase.invalid, 3); + + let result = '❌'; + let status = 'FAILED'; + + if (testCase.expected === null) { + // Should have no suggestions + if (suggestions.length === 0) { + result = '✅'; + status = 'PASSED'; + passed++; + } else { + failed++; + } + } else { + // Should have the expected suggestion + const found = suggestions.some(s => s.nodeType === testCase.expected); + if (found) { + const suggestion = suggestions.find(s => s.nodeType === testCase.expected); + const isAutoFixable = suggestion && suggestion.confidence >= 0.9; + result = '✅'; + status = isAutoFixable ? 'PASSED (auto-fixable)' : 'PASSED'; + passed++; + } else { + failed++; + } + } + + console.log(`${result} "${testCase.invalid}" → ${testCase.expected || 'no suggestions'}: ${status}`); + } + + console.log('='.repeat(60)); + console.log(`\nTotal: ${passed}/${testCases.length} tests passed`); + + if (failed === 0) { + console.log('🎉 All tests passed!'); + } else { + console.log(`⚠️ ${failed} tests failed`); + } +} + +testSummary().catch(console.error); \ No newline at end of file diff --git a/src/services/node-similarity-service.ts b/src/services/node-similarity-service.ts index 29245e1..0cb1d35 100644 --- a/src/services/node-similarity-service.ts +++ b/src/services/node-similarity-service.ts @@ -62,8 +62,8 @@ export class NodeSimilarityService { // Old versions or deprecated names patterns.set('deprecated', [ - { pattern: /^n8n-nodes-base\./i, suggestion: '', confidence: 0.85, reason: 'Full package name used instead of short form' }, - { pattern: /^@n8n\/n8n-nodes-langchain\./i, suggestion: '', confidence: 0.85, reason: 'Full package name used instead of short form' }, + { pattern: /^n8n-nodes-base\./i, suggestion: '', confidence: 0.95, reason: 'Full package name used instead of short form' }, + { pattern: /^@n8n\/n8n-nodes-langchain\./i, suggestion: '', confidence: 0.95, reason: 'Full package name used instead of short form' }, ]); // Common typos @@ -78,6 +78,7 @@ export class NodeSimilarityService { // AI/LangChain specific patterns.set('ai_nodes', [ { pattern: /^openai$/i, suggestion: 'nodes-langchain.openAi', confidence: 0.85, reason: 'AI node - incorrect package' }, + { pattern: /^nodes-base\.openai$/i, suggestion: 'nodes-langchain.openAi', confidence: 0.9, reason: 'Wrong package - OpenAI is in LangChain package' }, { pattern: /^chatOpenAI$/i, suggestion: 'nodes-langchain.lmChatOpenAi', confidence: 0.85, reason: 'LangChain node naming convention' }, { pattern: /^vectorStore$/i, suggestion: 'nodes-langchain.vectorStoreInMemory', confidence: 0.7, reason: 'Generic vector store reference' }, ]); @@ -186,12 +187,20 @@ export class NodeSimilarityService { const cleanValid = this.normalizeNodeType(node.nodeType); const displayNameClean = this.normalizeNodeType(node.displayName); + // Special handling for very short search terms (e.g., "http", "sheet") + const isShortSearch = invalidType.length <= 5; + // Name similarity (40% weight) - const nameSimilarity = Math.max( + let nameSimilarity = Math.max( this.getStringSimilarity(cleanInvalid, cleanValid), this.getStringSimilarity(cleanInvalid, displayNameClean) ) * 40; + // For short searches that are substrings, give a small name similarity boost + if (isShortSearch && (cleanValid.includes(cleanInvalid) || displayNameClean.includes(cleanInvalid))) { + nameSimilarity = Math.max(nameSimilarity, 10); + } + // Category match (20% weight) let categoryMatch = 0; if (node.category) { @@ -215,7 +224,9 @@ export class NodeSimilarityService { // Check if it's a substring match if (cleanValid.includes(cleanInvalid) || displayNameClean.includes(cleanInvalid)) { - patternMatch = 25; + // Boost score significantly for short searches that are exact substring matches + // Short searches need more boost to reach the 50 threshold + patternMatch = isShortSearch ? 45 : 25; } else if (this.getEditDistance(cleanInvalid, cleanValid) <= 2) { // Small edit distance indicates likely typo patternMatch = 20; @@ -223,6 +234,11 @@ export class NodeSimilarityService { patternMatch = 18; } + // For very short searches, also check if the search term appears at the start + if (isShortSearch && (cleanValid.startsWith(cleanInvalid) || displayNameClean.startsWith(cleanInvalid))) { + patternMatch = Math.max(patternMatch, 40); + } + const totalScore = nameSimilarity + categoryMatch + packageMatch + patternMatch; return {