fix: correct misleading Code node documentation based on real-world testing
Critical fixes based on Claude Desktop feedback:
1. Fixed crypto documentation: require('crypto') IS available despite editor warnings
- Added clear examples of crypto usage
- Updated validation to guide correct require() usage
2. Clarified $helpers vs standalone functions
- $getWorkflowStaticData() is standalone, NOT $helpers.getWorkflowStaticData()
- Added validation to catch incorrect usage (prevents '$helpers is not defined' errors)
- Enhanced examples showing proper $helpers availability checks
3. Fixed JMESPath numeric literal documentation
- n8n requires backticks around numbers in filters: [?age >= `18`]
- Added multiple examples and validation to detect missing backticks
- Prevents 'JMESPath syntax error' that Claude Desktop encountered
4. Fixed webhook data access gotcha
- Webhook payload is at items[0].json.body, NOT items[0].json
- Added dedicated 'Webhook Data Access' section with clear examples
- Created process_webhook_data task template
- Added validation to detect incorrect webhook data access patterns
All fixes based on production workflows TaNqYoZNNeHC4Hne and JZ9urD7PNClDZ1bm
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
203
scripts/test-code-node-enhancements.ts
Executable file
203
scripts/test-code-node-enhancements.ts
Executable file
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Test script for Code node enhancements
|
||||
* Tests:
|
||||
* 1. Code node documentation in tools_documentation
|
||||
* 2. Enhanced validation for Code nodes
|
||||
* 3. Code node examples
|
||||
* 4. Code node task templates
|
||||
*/
|
||||
|
||||
import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js';
|
||||
import { ExampleGenerator } from '../src/services/example-generator.js';
|
||||
import { TaskTemplates } from '../src/services/task-templates.js';
|
||||
import { getToolDocumentation } from '../src/mcp/tools-documentation.js';
|
||||
|
||||
console.log('🧪 Testing Code Node Enhancements\n');
|
||||
|
||||
// Test 1: Code node documentation
|
||||
console.log('1️⃣ Testing Code Node Documentation');
|
||||
console.log('=====================================');
|
||||
const codeNodeDocs = getToolDocumentation('code_node_guide', 'essentials');
|
||||
console.log('✅ Code node documentation available');
|
||||
console.log('First 500 chars:', codeNodeDocs.substring(0, 500) + '...\n');
|
||||
|
||||
// Test 2: Code node validation
|
||||
console.log('2️⃣ Testing Code Node Validation');
|
||||
console.log('=====================================');
|
||||
|
||||
// Test cases
|
||||
const validationTests = [
|
||||
{
|
||||
name: 'Empty code',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'No return statement',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: 'const data = items;'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Invalid return format',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: 'return "hello";'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Valid code',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: 'return [{json: {result: "success"}}];'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Python with external library',
|
||||
config: {
|
||||
language: 'python',
|
||||
pythonCode: 'import pandas as pd\nreturn [{"json": {"result": "fail"}}]'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Code with $json in wrong mode',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: 'const value = $json.field;\nreturn [{json: {value}}];'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Code with security issue',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: 'const result = eval(item.json.code);\nreturn [{json: {result}}];'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of validationTests) {
|
||||
console.log(`\nTest: ${test.name}`);
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
'nodes-base.code',
|
||||
test.config,
|
||||
[
|
||||
{ name: 'language', type: 'options', options: ['javaScript', 'python'] },
|
||||
{ name: 'jsCode', type: 'string' },
|
||||
{ name: 'pythonCode', type: 'string' },
|
||||
{ name: 'mode', type: 'options', options: ['runOnceForAllItems', 'runOnceForEachItem'] }
|
||||
],
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
console.log(` Valid: ${result.valid}`);
|
||||
if (result.errors.length > 0) {
|
||||
console.log(` Errors: ${result.errors.map(e => e.message).join(', ')}`);
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
console.log(` Warnings: ${result.warnings.map(w => w.message).join(', ')}`);
|
||||
}
|
||||
if (result.suggestions.length > 0) {
|
||||
console.log(` Suggestions: ${result.suggestions.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: Code node examples
|
||||
console.log('\n\n3️⃣ Testing Code Node Examples');
|
||||
console.log('=====================================');
|
||||
|
||||
const codeExamples = ExampleGenerator.getExamples('nodes-base.code');
|
||||
console.log('Available examples:', Object.keys(codeExamples));
|
||||
console.log('\nMinimal example:');
|
||||
console.log(JSON.stringify(codeExamples.minimal, null, 2));
|
||||
console.log('\nCommon example preview:');
|
||||
console.log(codeExamples.common?.jsCode?.substring(0, 200) + '...');
|
||||
|
||||
// Test 4: Code node task templates
|
||||
console.log('\n\n4️⃣ Testing Code Node Task Templates');
|
||||
console.log('=====================================');
|
||||
|
||||
const codeNodeTasks = [
|
||||
'transform_data',
|
||||
'custom_ai_tool',
|
||||
'aggregate_data',
|
||||
'batch_process_with_api',
|
||||
'error_safe_transform',
|
||||
'async_data_processing',
|
||||
'python_data_analysis'
|
||||
];
|
||||
|
||||
for (const taskName of codeNodeTasks) {
|
||||
const template = TaskTemplates.getTemplate(taskName);
|
||||
if (template) {
|
||||
console.log(`\n✅ ${taskName}:`);
|
||||
console.log(` Description: ${template.description}`);
|
||||
console.log(` Language: ${template.configuration.language || 'javaScript'}`);
|
||||
console.log(` Code preview: ${template.configuration.jsCode?.substring(0, 100) || template.configuration.pythonCode?.substring(0, 100)}...`);
|
||||
} else {
|
||||
console.log(`\n❌ ${taskName}: Template not found`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 5: Validate a complex Code node configuration
|
||||
console.log('\n\n5️⃣ Testing Complex Code Node Validation');
|
||||
console.log('==========================================');
|
||||
|
||||
const complexCode = {
|
||||
language: 'javaScript',
|
||||
mode: 'runOnceForEachItem',
|
||||
jsCode: `// Complex validation test
|
||||
try {
|
||||
const email = $json.email;
|
||||
const response = await $helpers.httpRequest({
|
||||
method: 'POST',
|
||||
url: 'https://api.example.com/validate',
|
||||
body: { email }
|
||||
});
|
||||
|
||||
return [{
|
||||
json: {
|
||||
...response,
|
||||
validated: true
|
||||
}
|
||||
}];
|
||||
} catch (error) {
|
||||
return [{
|
||||
json: {
|
||||
error: error.message,
|
||||
validated: false
|
||||
}
|
||||
}];
|
||||
}`,
|
||||
onError: 'continueRegularOutput',
|
||||
retryOnFail: true,
|
||||
maxTries: 3
|
||||
};
|
||||
|
||||
const complexResult = EnhancedConfigValidator.validateWithMode(
|
||||
'nodes-base.code',
|
||||
complexCode,
|
||||
[
|
||||
{ name: 'language', type: 'options', options: ['javaScript', 'python'] },
|
||||
{ name: 'jsCode', type: 'string' },
|
||||
{ name: 'mode', type: 'options', options: ['runOnceForAllItems', 'runOnceForEachItem'] },
|
||||
{ name: 'onError', type: 'options' },
|
||||
{ name: 'retryOnFail', type: 'boolean' },
|
||||
{ name: 'maxTries', type: 'number' }
|
||||
],
|
||||
'operation',
|
||||
'strict'
|
||||
);
|
||||
|
||||
console.log('Complex code validation:');
|
||||
console.log(` Valid: ${complexResult.valid}`);
|
||||
console.log(` Errors: ${complexResult.errors.length}`);
|
||||
console.log(` Warnings: ${complexResult.warnings.length}`);
|
||||
console.log(` Suggestions: ${complexResult.suggestions.length}`);
|
||||
|
||||
console.log('\n✅ All Code node enhancement tests completed!');
|
||||
133
scripts/test-code-node-fixes.ts
Executable file
133
scripts/test-code-node-fixes.ts
Executable file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env ts-node
|
||||
|
||||
/**
|
||||
* Test script to verify Code node documentation fixes
|
||||
*/
|
||||
|
||||
import { createDatabaseAdapter } from '../src/database/database-adapter';
|
||||
import { NodeDocumentationService } from '../src/services/node-documentation-service';
|
||||
import { getToolDocumentation } from '../src/mcp/tools-documentation';
|
||||
import { ExampleGenerator } from '../src/services/example-generator';
|
||||
import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator';
|
||||
|
||||
const dbPath = process.env.NODE_DB_PATH || './nodes.db';
|
||||
|
||||
async function main() {
|
||||
console.log('🧪 Testing Code Node Documentation Fixes\n');
|
||||
|
||||
const db = await createDatabaseAdapter(dbPath);
|
||||
const service = new NodeDocumentationService(dbPath);
|
||||
|
||||
// Test 1: Check JMESPath documentation
|
||||
console.log('1️⃣ Testing JMESPath Documentation Fix');
|
||||
console.log('=====================================');
|
||||
const codeNodeGuide = getToolDocumentation('code_node_guide', 'full');
|
||||
|
||||
// Check for correct JMESPath syntax
|
||||
if (codeNodeGuide.includes('$jmespath(') && !codeNodeGuide.includes('jmespath.search(')) {
|
||||
console.log('✅ JMESPath documentation correctly shows $jmespath() syntax');
|
||||
} else {
|
||||
console.log('❌ JMESPath documentation still shows incorrect syntax');
|
||||
}
|
||||
|
||||
// Check for Python JMESPath
|
||||
if (codeNodeGuide.includes('_jmespath(')) {
|
||||
console.log('✅ Python JMESPath with underscore prefix documented');
|
||||
} else {
|
||||
console.log('❌ Python JMESPath not properly documented');
|
||||
}
|
||||
|
||||
// Test 2: Check $node documentation
|
||||
console.log('\n2️⃣ Testing $node Documentation Fix');
|
||||
console.log('===================================');
|
||||
|
||||
if (codeNodeGuide.includes("$('Previous Node')") && !codeNodeGuide.includes('$node.name')) {
|
||||
console.log('✅ Node access correctly shows $("Node Name") syntax');
|
||||
} else {
|
||||
console.log('❌ Node access documentation still incorrect');
|
||||
}
|
||||
|
||||
// Test 3: Check Python item.json documentation
|
||||
console.log('\n3️⃣ Testing Python item.json Documentation Fix');
|
||||
console.log('==============================================');
|
||||
|
||||
if (codeNodeGuide.includes('item.json.to_py()') && codeNodeGuide.includes('JsProxy')) {
|
||||
console.log('✅ Python item.json correctly documented with to_py() method');
|
||||
} else {
|
||||
console.log('❌ Python item.json documentation incomplete');
|
||||
}
|
||||
|
||||
// Test 4: Check Python examples
|
||||
console.log('\n4️⃣ Testing Python Examples');
|
||||
console.log('===========================');
|
||||
|
||||
const pythonExample = ExampleGenerator.getExamples('nodes-base.code.pythonExample');
|
||||
if (pythonExample?.minimal?.pythonCode?.includes('_input.all()') &&
|
||||
pythonExample?.minimal?.pythonCode?.includes('to_py()')) {
|
||||
console.log('✅ Python examples use correct _input.all() and to_py()');
|
||||
} else {
|
||||
console.log('❌ Python examples not updated correctly');
|
||||
}
|
||||
|
||||
// Test 5: Validate Code node without visibility warnings
|
||||
console.log('\n5️⃣ Testing Code Node Validation (No Visibility Warnings)');
|
||||
console.log('=========================================================');
|
||||
|
||||
const codeNodeInfo = await service.getNodeInfo('n8n-nodes-base.code');
|
||||
if (!codeNodeInfo) {
|
||||
console.log('❌ Could not find Code node info');
|
||||
return;
|
||||
}
|
||||
|
||||
const testConfig = {
|
||||
language: 'javaScript',
|
||||
jsCode: 'return items.map(item => ({json: {...item.json, processed: true}}))',
|
||||
mode: 'runOnceForAllItems',
|
||||
onError: 'continueRegularOutput'
|
||||
};
|
||||
|
||||
const nodeProperties = (codeNodeInfo as any).properties || [];
|
||||
const validationResult = EnhancedConfigValidator.validateWithMode(
|
||||
'nodes-base.code',
|
||||
testConfig,
|
||||
nodeProperties,
|
||||
'full',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
// Check if there are any visibility warnings
|
||||
const visibilityWarnings = validationResult.warnings.filter(w =>
|
||||
w.message.includes("won't be used due to current settings")
|
||||
);
|
||||
|
||||
if (visibilityWarnings.length === 0) {
|
||||
console.log('✅ No false positive visibility warnings for Code node');
|
||||
} else {
|
||||
console.log(`❌ Still getting ${visibilityWarnings.length} visibility warnings:`);
|
||||
visibilityWarnings.forEach(w => console.log(` - ${w.property}: ${w.message}`));
|
||||
}
|
||||
|
||||
// Test 6: Check Python underscore variables in documentation
|
||||
console.log('\n6️⃣ Testing Python Underscore Variables');
|
||||
console.log('========================================');
|
||||
|
||||
const pythonVarsDocumented = codeNodeGuide.includes('Variables use underscore prefix') &&
|
||||
codeNodeGuide.includes('_input') &&
|
||||
codeNodeGuide.includes('_json') &&
|
||||
codeNodeGuide.includes('_jmespath');
|
||||
|
||||
if (pythonVarsDocumented) {
|
||||
console.log('✅ Python underscore variables properly documented');
|
||||
} else {
|
||||
console.log('❌ Python underscore variables not fully documented');
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n📊 Test Summary');
|
||||
console.log('===============');
|
||||
console.log('All critical documentation fixes have been verified!');
|
||||
|
||||
db.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
138
scripts/test-expression-code-validation.ts
Executable file
138
scripts/test-expression-code-validation.ts
Executable file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Test script for Expression vs Code Node validation
|
||||
* Tests that we properly detect and warn about expression syntax in Code nodes
|
||||
*/
|
||||
|
||||
import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js';
|
||||
|
||||
console.log('🧪 Testing Expression vs Code Node Validation\n');
|
||||
|
||||
// Test cases with expression syntax that shouldn't work in Code nodes
|
||||
const testCases = [
|
||||
{
|
||||
name: 'Expression syntax in Code node',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `// Using expression syntax
|
||||
const value = {{$json.field}};
|
||||
return [{json: {value}}];`
|
||||
},
|
||||
expectedError: 'Expression syntax {{...}} is not valid in Code nodes'
|
||||
},
|
||||
{
|
||||
name: 'Wrong $node syntax',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `// Using expression $node syntax
|
||||
const data = $node['Previous Node'].json;
|
||||
return [{json: data}];`
|
||||
},
|
||||
expectedWarning: 'Use $(\'Node Name\') instead of $node[\'Node Name\'] in Code nodes'
|
||||
},
|
||||
{
|
||||
name: 'Expression-only functions',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `// Using expression functions
|
||||
const now = $now();
|
||||
const unique = items.unique();
|
||||
return [{json: {now, unique}}];`
|
||||
},
|
||||
expectedWarning: '$now() is an expression-only function'
|
||||
},
|
||||
{
|
||||
name: 'Wrong JMESPath parameter order',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `// Wrong parameter order
|
||||
const result = $jmespath("users[*].name", data);
|
||||
return [{json: {result}}];`
|
||||
},
|
||||
expectedWarning: 'Code node $jmespath has reversed parameter order'
|
||||
},
|
||||
{
|
||||
name: 'Correct Code node syntax',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `// Correct syntax
|
||||
const prevData = $('Previous Node').first();
|
||||
const now = DateTime.now();
|
||||
const result = $jmespath(data, "users[*].name");
|
||||
return [{json: {prevData, now, result}}];`
|
||||
},
|
||||
shouldBeValid: true
|
||||
}
|
||||
];
|
||||
|
||||
// Basic node properties for Code node
|
||||
const codeNodeProperties = [
|
||||
{ name: 'language', type: 'options', options: ['javaScript', 'python'] },
|
||||
{ name: 'jsCode', type: 'string' },
|
||||
{ name: 'pythonCode', type: 'string' },
|
||||
{ name: 'mode', type: 'options', options: ['runOnceForAllItems', 'runOnceForEachItem'] }
|
||||
];
|
||||
|
||||
console.log('Running validation tests...\n');
|
||||
|
||||
testCases.forEach((test, index) => {
|
||||
console.log(`Test ${index + 1}: ${test.name}`);
|
||||
console.log('─'.repeat(50));
|
||||
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
'nodes-base.code',
|
||||
test.config,
|
||||
codeNodeProperties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
console.log(`Valid: ${result.valid}`);
|
||||
console.log(`Errors: ${result.errors.length}`);
|
||||
console.log(`Warnings: ${result.warnings.length}`);
|
||||
|
||||
if (test.expectedError) {
|
||||
const hasExpectedError = result.errors.some(e =>
|
||||
e.message.includes(test.expectedError)
|
||||
);
|
||||
console.log(`✅ Expected error found: ${hasExpectedError}`);
|
||||
if (!hasExpectedError) {
|
||||
console.log('❌ Missing expected error:', test.expectedError);
|
||||
console.log('Actual errors:', result.errors.map(e => e.message));
|
||||
}
|
||||
}
|
||||
|
||||
if (test.expectedWarning) {
|
||||
const hasExpectedWarning = result.warnings.some(w =>
|
||||
w.message.includes(test.expectedWarning)
|
||||
);
|
||||
console.log(`✅ Expected warning found: ${hasExpectedWarning}`);
|
||||
if (!hasExpectedWarning) {
|
||||
console.log('❌ Missing expected warning:', test.expectedWarning);
|
||||
console.log('Actual warnings:', result.warnings.map(w => w.message));
|
||||
}
|
||||
}
|
||||
|
||||
if (test.shouldBeValid) {
|
||||
console.log(`✅ Should be valid: ${result.valid && result.errors.length === 0}`);
|
||||
if (!result.valid || result.errors.length > 0) {
|
||||
console.log('❌ Unexpected errors:', result.errors);
|
||||
}
|
||||
}
|
||||
|
||||
// Show actual messages
|
||||
if (result.errors.length > 0) {
|
||||
console.log('\nErrors:');
|
||||
result.errors.forEach(e => console.log(` - ${e.message}`));
|
||||
}
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
console.log('\nWarnings:');
|
||||
result.warnings.forEach(w => console.log(` - ${w.message}`));
|
||||
}
|
||||
|
||||
console.log('\n');
|
||||
});
|
||||
|
||||
console.log('✅ Expression vs Code Node validation tests completed!');
|
||||
93
scripts/test-helpers-validation.ts
Normal file
93
scripts/test-helpers-validation.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js';
|
||||
|
||||
console.log('🧪 Testing $helpers Validation\n');
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: 'Incorrect $helpers.getWorkflowStaticData',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `const data = $helpers.getWorkflowStaticData('global');
|
||||
data.counter = 1;
|
||||
return [{json: {counter: data.counter}}];`
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Correct $getWorkflowStaticData',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `const data = $getWorkflowStaticData('global');
|
||||
data.counter = 1;
|
||||
return [{json: {counter: data.counter}}];`
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '$helpers without check',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `const response = await $helpers.httpRequest({
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com'
|
||||
});
|
||||
return [{json: response}];`
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '$helpers with proper check',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `if (typeof $helpers !== 'undefined' && $helpers.httpRequest) {
|
||||
const response = await $helpers.httpRequest({
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com'
|
||||
});
|
||||
return [{json: response}];
|
||||
}
|
||||
return [{json: {error: 'HTTP not available'}}];`
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Crypto without require',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `const token = crypto.randomBytes(32).toString('hex');
|
||||
return [{json: {token}}];`
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Crypto with require',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `const crypto = require('crypto');
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
return [{json: {token}}];`
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of testCases) {
|
||||
console.log(`Test: ${test.name}`);
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
'nodes-base.code',
|
||||
test.config,
|
||||
[
|
||||
{ name: 'language', type: 'options', options: ['javaScript', 'python'] },
|
||||
{ name: 'jsCode', type: 'string' }
|
||||
],
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
console.log(` Valid: ${result.valid}`);
|
||||
if (result.errors.length > 0) {
|
||||
console.log(` Errors: ${result.errors.map(e => e.message).join(', ')}`);
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
console.log(` Warnings: ${result.warnings.map(w => w.message).join(', ')}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log('✅ $helpers validation tests completed!');
|
||||
114
scripts/test-jmespath-validation.ts
Normal file
114
scripts/test-jmespath-validation.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js';
|
||||
|
||||
console.log('🧪 Testing JMESPath Validation\n');
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: 'JMESPath with unquoted numeric literal',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `const data = { users: [{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }] };
|
||||
const adults = $jmespath(data, 'users[?age >= 18]');
|
||||
return [{json: {adults}}];`
|
||||
},
|
||||
expectError: true
|
||||
},
|
||||
{
|
||||
name: 'JMESPath with properly quoted numeric literal',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `const data = { users: [{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }] };
|
||||
const adults = $jmespath(data, 'users[?age >= \`18\`]');
|
||||
return [{json: {adults}}];`
|
||||
},
|
||||
expectError: false
|
||||
},
|
||||
{
|
||||
name: 'Multiple JMESPath filters with unquoted numbers',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `const products = items.map(item => item.json);
|
||||
const expensive = $jmespath(products, '[?price > 100]');
|
||||
const lowStock = $jmespath(products, '[?quantity < 10]');
|
||||
const highPriority = $jmespath(products, '[?priority == 1]');
|
||||
return [{json: {expensive, lowStock, highPriority}}];`
|
||||
},
|
||||
expectError: true
|
||||
},
|
||||
{
|
||||
name: 'JMESPath with string comparison (no backticks needed)',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `const data = { users: [{ name: 'John', status: 'active' }, { name: 'Jane', status: 'inactive' }] };
|
||||
const activeUsers = $jmespath(data, 'users[?status == "active"]');
|
||||
return [{json: {activeUsers}}];`
|
||||
},
|
||||
expectError: false
|
||||
},
|
||||
{
|
||||
name: 'Python JMESPath with unquoted numeric literal',
|
||||
config: {
|
||||
language: 'python',
|
||||
pythonCode: `data = { 'users': [{ 'name': 'John', 'age': 30 }, { 'name': 'Jane', 'age': 25 }] }
|
||||
adults = _jmespath(data, 'users[?age >= 18]')
|
||||
return [{'json': {'adults': adults}}]`
|
||||
},
|
||||
expectError: true
|
||||
},
|
||||
{
|
||||
name: 'Complex filter with decimal numbers',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `const items = [{ price: 99.99 }, { price: 150.50 }, { price: 200 }];
|
||||
const expensive = $jmespath(items, '[?price >= 99.95]');
|
||||
return [{json: {expensive}}];`
|
||||
},
|
||||
expectError: true
|
||||
}
|
||||
];
|
||||
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const test of testCases) {
|
||||
console.log(`Test: ${test.name}`);
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
'nodes-base.code',
|
||||
test.config,
|
||||
[
|
||||
{ name: 'language', type: 'options', options: ['javaScript', 'python'] },
|
||||
{ name: 'jsCode', type: 'string' },
|
||||
{ name: 'pythonCode', type: 'string' }
|
||||
],
|
||||
'operation',
|
||||
'strict'
|
||||
);
|
||||
|
||||
const hasJMESPathError = result.errors.some(e =>
|
||||
e.message.includes('JMESPath numeric literal') ||
|
||||
e.message.includes('must be wrapped in backticks')
|
||||
);
|
||||
|
||||
const passed = hasJMESPathError === test.expectError;
|
||||
|
||||
console.log(` Expected error: ${test.expectError}`);
|
||||
console.log(` Has JMESPath error: ${hasJMESPathError}`);
|
||||
console.log(` Result: ${passed ? '✅ PASS' : '❌ FAIL'}`);
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
console.log(` Errors: ${result.errors.map(e => e.message).join(', ')}`);
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
console.log(` Warnings: ${result.warnings.slice(0, 2).map(w => w.message).join(', ')}`);
|
||||
}
|
||||
|
||||
if (passed) passCount++;
|
||||
else failCount++;
|
||||
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(`\n📊 Results: ${passCount} passed, ${failCount} failed`);
|
||||
console.log(failCount === 0 ? '✅ All JMESPath validation tests passed!' : '❌ Some tests failed');
|
||||
111
scripts/test-webhook-validation.ts
Normal file
111
scripts/test-webhook-validation.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js';
|
||||
|
||||
console.log('🧪 Testing Webhook Data Access Validation\n');
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: 'Direct webhook data access (incorrect)',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `// Processing data from Webhook node
|
||||
const prevWebhook = $('Webhook').first();
|
||||
const command = items[0].json.testCommand;
|
||||
const data = items[0].json.payload;
|
||||
return [{json: {command, data}}];`
|
||||
},
|
||||
expectWarning: true
|
||||
},
|
||||
{
|
||||
name: 'Correct webhook data access through body',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `// Processing data from Webhook node
|
||||
const webhookData = items[0].json.body;
|
||||
const command = webhookData.testCommand;
|
||||
const data = webhookData.payload;
|
||||
return [{json: {command, data}}];`
|
||||
},
|
||||
expectWarning: false
|
||||
},
|
||||
{
|
||||
name: 'Common webhook field names without body',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `// Processing webhook
|
||||
const command = items[0].json.command;
|
||||
const action = items[0].json.action;
|
||||
const payload = items[0].json.payload;
|
||||
return [{json: {command, action, payload}}];`
|
||||
},
|
||||
expectWarning: true
|
||||
},
|
||||
{
|
||||
name: 'Non-webhook data access (should not warn)',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `// Processing data from HTTP Request node
|
||||
const data = items[0].json.results;
|
||||
const status = items[0].json.status;
|
||||
return [{json: {data, status}}];`
|
||||
},
|
||||
expectWarning: false
|
||||
},
|
||||
{
|
||||
name: 'Mixed correct and incorrect access',
|
||||
config: {
|
||||
language: 'javaScript',
|
||||
jsCode: `// Mixed access patterns
|
||||
const webhookBody = items[0].json.body; // Correct
|
||||
const directAccess = items[0].json.command; // Incorrect if webhook
|
||||
return [{json: {webhookBody, directAccess}}];`
|
||||
},
|
||||
expectWarning: false // If user already uses .body, we assume they know the pattern
|
||||
}
|
||||
];
|
||||
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const test of testCases) {
|
||||
console.log(`Test: ${test.name}`);
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
'nodes-base.code',
|
||||
test.config,
|
||||
[
|
||||
{ name: 'language', type: 'options', options: ['javaScript', 'python'] },
|
||||
{ name: 'jsCode', type: 'string' }
|
||||
],
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
const hasWebhookWarning = result.warnings.some(w =>
|
||||
w.message.includes('Webhook data is nested under .body') ||
|
||||
w.message.includes('webhook data, remember it\'s nested under .body')
|
||||
);
|
||||
|
||||
const passed = hasWebhookWarning === test.expectWarning;
|
||||
|
||||
console.log(` Expected warning: ${test.expectWarning}`);
|
||||
console.log(` Has webhook warning: ${hasWebhookWarning}`);
|
||||
console.log(` Result: ${passed ? '✅ PASS' : '❌ FAIL'}`);
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
const relevantWarnings = result.warnings
|
||||
.filter(w => w.message.includes('webhook') || w.message.includes('Webhook'))
|
||||
.map(w => w.message);
|
||||
if (relevantWarnings.length > 0) {
|
||||
console.log(` Webhook warnings: ${relevantWarnings.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (passed) passCount++;
|
||||
else failCount++;
|
||||
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(`\n📊 Results: ${passCount} passed, ${failCount} failed`);
|
||||
console.log(failCount === 0 ? '✅ All webhook validation tests passed!' : '❌ Some tests failed');
|
||||
Reference in New Issue
Block a user