Files
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

174 lines
5.0 KiB
TypeScript

import { readFileSync, existsSync } from 'fs';
import type { INodeTypeDescription } from 'n8n-workflow';
// We need to mock the fs module before importing the module under test
jest.mock('fs', () => ({
readFileSync: jest.fn(),
existsSync: jest.fn(),
}));
const mockedReadFileSync = readFileSync as jest.MockedFunction<typeof readFileSync>;
const mockedExistsSync = existsSync as jest.MockedFunction<typeof existsSync>;
// Import after mocking
import { loadNodesFromFile } from './load-nodes';
describe('loadNodesFromFile', () => {
beforeEach(() => {
jest.clearAllMocks();
// Default: legacy path exists
mockedExistsSync.mockImplementation((path) => {
return String(path).endsWith('nodes.json');
});
});
describe('version handling', () => {
it('should keep all version entries for a node type so older versions can be validated', () => {
// This is the real-world scenario: removeDuplicates has two entries
// - Entry 1: version [1, 1.1], defaultVersion 2
// - Entry 2: version [2], defaultVersion 2
// A workflow using typeVersion 1.1 should still be able to validate
const nodesData: Array<Partial<INodeTypeDescription>> = [
{
name: 'n8n-nodes-base.removeDuplicates',
displayName: 'Remove Duplicates',
version: [1, 1.1],
defaultVersion: 2,
inputs: ['main'],
outputs: ['main'],
},
{
name: 'n8n-nodes-base.removeDuplicates',
displayName: 'Remove Duplicates',
version: [2],
defaultVersion: 2,
inputs: ['main'],
outputs: ['main'],
},
];
mockedReadFileSync.mockReturnValue(JSON.stringify(nodesData));
const result = loadNodesFromFile();
// The result should contain entries that cover ALL versions (1, 1.1, and 2)
// so that workflows using any of these versions can be validated
const removeDuplicatesEntries = result.filter(
(n) => n.name === 'n8n-nodes-base.removeDuplicates',
);
// We need at least the versions to be available for lookup
// Either keep both entries, or merge the versions
const allVersions = removeDuplicatesEntries.flatMap((entry) =>
Array.isArray(entry.version) ? entry.version : [entry.version],
);
expect(allVersions).toContain(1);
expect(allVersions).toContain(1.1);
expect(allVersions).toContain(2);
});
it('should handle single-version nodes correctly', () => {
const nodesData: Array<Partial<INodeTypeDescription>> = [
{
name: 'n8n-nodes-base.code',
displayName: 'Code',
version: 1,
inputs: ['main'],
outputs: ['main'],
},
];
mockedReadFileSync.mockReturnValue(JSON.stringify(nodesData));
const result = loadNodesFromFile();
const codeNode = result.find((n) => n.name === 'n8n-nodes-base.code');
expect(codeNode).toBeDefined();
expect(codeNode?.version).toBe(1);
});
it('should handle nodes with version array but no defaultVersion', () => {
const nodesData: Array<Partial<INodeTypeDescription>> = [
{
name: 'n8n-nodes-base.httpRequest',
displayName: 'HTTP Request',
version: [1, 2, 3],
inputs: ['main'],
outputs: ['main'],
},
];
mockedReadFileSync.mockReturnValue(JSON.stringify(nodesData));
const result = loadNodesFromFile();
const httpNode = result.find((n) => n.name === 'n8n-nodes-base.httpRequest');
expect(httpNode).toBeDefined();
// All versions should be available
const versions = Array.isArray(httpNode?.version) ? httpNode.version : [httpNode?.version];
expect(versions).toContain(1);
expect(versions).toContain(2);
expect(versions).toContain(3);
});
});
describe('filtering', () => {
it('should filter out ignored node types', () => {
const nodesData: Array<Partial<INodeTypeDescription>> = [
{
name: '@n8n/n8n-nodes-langchain.toolVectorStore',
displayName: 'Vector Store Tool',
version: 1,
inputs: [],
outputs: ['ai_tool'],
},
{
name: 'n8n-nodes-base.code',
displayName: 'Code',
version: 1,
inputs: ['main'],
outputs: ['main'],
},
];
mockedReadFileSync.mockReturnValue(JSON.stringify(nodesData));
const result = loadNodesFromFile();
expect(
result.find((n) => n.name === '@n8n/n8n-nodes-langchain.toolVectorStore'),
).toBeUndefined();
expect(result.find((n) => n.name === 'n8n-nodes-base.code')).toBeDefined();
});
it('should filter out hidden nodes except dataTable', () => {
const nodesData: Array<Partial<INodeTypeDescription>> = [
{
name: 'n8n-nodes-base.hiddenNode',
displayName: 'Hidden Node',
version: 1,
hidden: true,
inputs: ['main'],
outputs: ['main'],
},
{
name: 'n8n-nodes-base.dataTable',
displayName: 'Data Table',
version: 1,
hidden: true,
inputs: ['main'],
outputs: ['main'],
},
];
mockedReadFileSync.mockReturnValue(JSON.stringify(nodesData));
const result = loadNodesFromFile();
expect(result.find((n) => n.name === 'n8n-nodes-base.hiddenNode')).toBeUndefined();
expect(result.find((n) => n.name === 'n8n-nodes-base.dataTable')).toBeDefined();
});
});
});