simple-parser.test.tsโข21.5 kB
import { describe, it, expect, beforeEach } from 'vitest';
import { SimpleParser } from '@/parsers/simple-parser';
import {
programmaticNodeFactory,
declarativeNodeFactory,
triggerNodeFactory,
webhookNodeFactory,
aiToolNodeFactory,
versionedNodeClassFactory,
versionedNodeTypeClassFactory,
malformedNodeFactory,
nodeClassFactory,
propertyFactory,
stringPropertyFactory,
resourcePropertyFactory,
operationPropertyFactory
} from '@tests/fixtures/factories/parser-node.factory';
describe('SimpleParser', () => {
let parser: SimpleParser;
beforeEach(() => {
parser = new SimpleParser();
});
describe('parse method', () => {
it('should parse a basic programmatic node', () => {
const nodeDefinition = programmaticNodeFactory.build();
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result).toMatchObject({
style: 'programmatic',
nodeType: nodeDefinition.name,
displayName: nodeDefinition.displayName,
description: nodeDefinition.description,
category: nodeDefinition.group?.[0],
properties: nodeDefinition.properties,
credentials: nodeDefinition.credentials || [],
isAITool: false,
isWebhook: false,
version: nodeDefinition.version?.toString() || '1',
isVersioned: false,
isTrigger: false,
operations: expect.any(Array)
});
});
it('should parse a declarative node', () => {
const nodeDefinition = declarativeNodeFactory.build();
// Fix the routing structure for simple parser - it expects operation.options to be an array
nodeDefinition.routing.request!.operation = {
options: [
{ name: 'Create User', value: 'createUser' },
{ name: 'Get User', value: 'getUser' }
]
} as any;
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result.style).toBe('declarative');
expect(result.operations.length).toBeGreaterThan(0);
});
it('should detect trigger nodes', () => {
const nodeDefinition = triggerNodeFactory.build();
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result.isTrigger).toBe(true);
});
it('should detect webhook nodes', () => {
const nodeDefinition = webhookNodeFactory.build();
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result.isWebhook).toBe(true);
});
it('should detect AI tool nodes', () => {
const nodeDefinition = aiToolNodeFactory.build();
// Fix the routing structure for simple parser
nodeDefinition.routing.request!.operation = {
options: [
{ name: 'Create', value: 'create' }
]
} as any;
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result.isAITool).toBe(true);
});
it('should parse VersionedNodeType class', () => {
const versionedDef = versionedNodeClassFactory.build();
const VersionedNodeClass = class VersionedNodeType {
baseDescription = versionedDef.baseDescription;
nodeVersions = versionedDef.nodeVersions;
currentVersion = versionedDef.baseDescription!.defaultVersion;
constructor() {
Object.defineProperty(this.constructor, 'name', {
value: 'VersionedNodeType',
configurable: true
});
}
};
const result = parser.parse(VersionedNodeClass as any);
expect(result.isVersioned).toBe(true);
expect(result.nodeType).toBe(versionedDef.baseDescription!.name);
expect(result.displayName).toBe(versionedDef.baseDescription!.displayName);
expect(result.version).toBe(versionedDef.baseDescription!.defaultVersion.toString());
});
it('should merge baseDescription with version-specific description', () => {
const VersionedNodeClass = class VersionedNodeType {
baseDescription = {
name: 'mergedNode',
displayName: 'Base Display Name',
description: 'Base description'
};
nodeVersions = {
1: {
description: {
displayName: 'Version 1 Display Name',
properties: [propertyFactory.build()]
}
}
};
currentVersion = 1;
constructor() {
Object.defineProperty(this.constructor, 'name', {
value: 'VersionedNodeType',
configurable: true
});
}
};
const result = parser.parse(VersionedNodeClass as any);
// Should merge baseDescription with version description
expect(result.nodeType).toBe('mergedNode'); // From base
expect(result.displayName).toBe('Version 1 Display Name'); // From version (overrides base)
expect(result.description).toBe('Base description'); // From base
});
it('should throw error for nodes without name', () => {
const nodeDefinition = malformedNodeFactory.build();
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
expect(() => parser.parse(NodeClass as any)).toThrow('Node is missing name property');
});
it('should handle nodes that fail to instantiate', () => {
const NodeClass = class {
constructor() {
throw new Error('Cannot instantiate');
}
};
expect(() => parser.parse(NodeClass as any)).toThrow('Node is missing name property');
});
it('should handle static description property', () => {
const nodeDefinition = programmaticNodeFactory.build();
const NodeClass = class {
static description = nodeDefinition;
};
// Since it can't instantiate and has no static description accessible,
// it should throw for missing name
expect(() => parser.parse(NodeClass as any)).toThrow();
});
it('should handle instance-based nodes', () => {
const nodeDefinition = programmaticNodeFactory.build();
const nodeInstance = {
description: nodeDefinition
};
const result = parser.parse(nodeInstance as any);
expect(result.displayName).toBe(nodeDefinition.displayName);
});
it('should use displayName fallback to name if not provided', () => {
const nodeDefinition = programmaticNodeFactory.build();
delete (nodeDefinition as any).displayName;
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result.displayName).toBe(nodeDefinition.name);
});
it('should handle category extraction from different fields', () => {
const testCases = [
{
description: { group: ['transform'], categories: ['output'] },
expected: 'transform' // group takes precedence
},
{
description: { categories: ['output'] },
expected: 'output'
},
{
description: {},
expected: undefined
}
];
testCases.forEach(({ description, expected }) => {
const baseDefinition = programmaticNodeFactory.build();
// Remove any existing group/categories from base definition to avoid conflicts
delete baseDefinition.group;
delete baseDefinition.categories;
const nodeDefinition = {
...baseDefinition,
...description,
name: baseDefinition.name // Ensure name is preserved
};
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result.category).toBe(expected);
});
});
});
describe('trigger detection', () => {
it('should detect triggers by group', () => {
const nodeDefinition = programmaticNodeFactory.build({
group: ['trigger']
});
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result.isTrigger).toBe(true);
});
it('should detect polling triggers', () => {
const nodeDefinition = programmaticNodeFactory.build({
polling: true
});
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result.isTrigger).toBe(true);
});
it('should detect trigger property', () => {
const nodeDefinition = programmaticNodeFactory.build({
trigger: true
});
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result.isTrigger).toBe(true);
});
it('should detect event triggers', () => {
const nodeDefinition = programmaticNodeFactory.build({
eventTrigger: true
});
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result.isTrigger).toBe(true);
});
it('should detect triggers by name', () => {
const nodeDefinition = programmaticNodeFactory.build({
name: 'customTrigger'
});
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result.isTrigger).toBe(true);
});
});
describe('operations extraction', () => {
it('should extract declarative operations from routing.request', () => {
const nodeDefinition = declarativeNodeFactory.build();
// Fix the routing structure for simple parser
nodeDefinition.routing.request!.operation = {
options: [
{ name: 'Create', value: 'create' },
{ name: 'Get', value: 'get' }
] as any
};
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
// Should have resource operations
const resourceOps = result.operations.filter(op => op.resource);
expect(resourceOps.length).toBeGreaterThan(0);
// Should have operation entries
const operationOps = result.operations.filter(op => op.operation && !op.resource);
expect(operationOps.length).toBeGreaterThan(0);
});
it('should extract declarative operations from routing.operations', () => {
const NodeClass = nodeClassFactory.build({
description: {
name: 'test',
routing: {
operations: {
create: { displayName: 'Create Item' },
read: { displayName: 'Read Item' },
update: { displayName: 'Update Item' },
delete: { displayName: 'Delete Item' }
}
}
}
});
const result = parser.parse(NodeClass as any);
expect(result.operations).toHaveLength(4);
expect(result.operations).toEqual(expect.arrayContaining([
{ operation: 'create', name: 'Create Item' },
{ operation: 'read', name: 'Read Item' },
{ operation: 'update', name: 'Update Item' },
{ operation: 'delete', name: 'Delete Item' }
]));
});
it('should extract programmatic operations from resource property', () => {
const resourceProp = resourcePropertyFactory.build();
const NodeClass = nodeClassFactory.build({
description: {
name: 'test',
properties: [resourceProp]
}
});
const result = parser.parse(NodeClass as any);
const resourceOps = result.operations.filter(op => op.type === 'resource');
expect(resourceOps).toHaveLength(resourceProp.options!.length);
resourceOps.forEach((op, idx) => {
expect(op).toMatchObject({
type: 'resource',
resource: resourceProp.options![idx].value,
name: resourceProp.options![idx].name
});
});
});
it('should extract programmatic operations with resource context', () => {
const operationProp = operationPropertyFactory.build();
const NodeClass = nodeClassFactory.build({
description: {
name: 'test',
properties: [operationProp]
}
});
const result = parser.parse(NodeClass as any);
const operationOps = result.operations.filter(op => op.type === 'operation');
expect(operationOps).toHaveLength(operationProp.options!.length);
// Should extract resource context from displayOptions
expect(operationOps[0].resources).toEqual(['user']);
});
it('should handle operations with multiple resource conditions', () => {
const operationProp = {
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: ['user', 'post', 'comment']
}
},
options: [
{ name: 'Create', value: 'create', action: 'Create item' }
]
};
const NodeClass = nodeClassFactory.build({
description: {
name: 'test',
properties: [operationProp]
}
});
const result = parser.parse(NodeClass as any);
const operationOps = result.operations.filter(op => op.type === 'operation');
expect(operationOps[0].resources).toEqual(['user', 'post', 'comment']);
});
it('should handle single resource condition as array', () => {
const operationProp = {
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: 'user' // Single value, not array
}
},
options: [
{ name: 'Get', value: 'get' }
]
};
const NodeClass = nodeClassFactory.build({
description: {
name: 'test',
properties: [operationProp]
}
});
const result = parser.parse(NodeClass as any);
const operationOps = result.operations.filter(op => op.type === 'operation');
expect(operationOps[0].resources).toEqual(['user']);
});
});
describe('version extraction', () => {
it('should prioritize currentVersion over description.defaultVersion', () => {
const NodeClass = class {
currentVersion = 2.2; // Should be returned
description = {
name: 'test',
displayName: 'Test',
defaultVersion: 3 // Should be ignored when currentVersion exists
};
};
const result = parser.parse(NodeClass as any);
expect(result.version).toBe('2.2');
});
it('should extract version from description.defaultVersion', () => {
const NodeClass = class {
description = {
name: 'test',
displayName: 'Test',
defaultVersion: 3
};
};
const result = parser.parse(NodeClass as any);
expect(result.version).toBe('3');
});
it('should NOT extract version from non-existent baseDescription (legacy bug)', () => {
// This test verifies the bug fix from v2.17.4
// baseDescription.defaultVersion doesn't exist on VersionedNodeType instances
const NodeClass = class {
baseDescription = { // This property doesn't exist on VersionedNodeType!
name: 'test',
displayName: 'Test',
defaultVersion: 3
};
// Constructor name trick to detect as VersionedNodeType
constructor() {
Object.defineProperty(this.constructor, 'name', {
value: 'VersionedNodeType',
configurable: true
});
}
};
const result = parser.parse(NodeClass as any);
// Should fallback to default version '1' since baseDescription.defaultVersion doesn't exist
expect(result.version).toBe('1');
});
it('should extract version from description.version', () => {
// For this test, the version needs to be in the instantiated description
const NodeClass = class {
description = {
name: 'test',
version: 2
};
};
const result = parser.parse(NodeClass as any);
expect(result.version).toBe('2');
});
it('should default to version 1', () => {
const NodeClass = nodeClassFactory.build({
description: {
name: 'test'
}
});
const result = parser.parse(NodeClass as any);
expect(result.version).toBe('1');
});
});
describe('versioned node detection', () => {
it('should detect nodes with baseDescription and nodeVersions', () => {
// For simple parser, need to create a proper class structure
const NodeClass = class {
baseDescription = {
name: 'test',
displayName: 'Test'
};
nodeVersions = { 1: {}, 2: {} };
constructor() {
Object.defineProperty(this.constructor, 'name', {
value: 'VersionedNodeType',
configurable: true
});
}
};
const result = parser.parse(NodeClass as any);
expect(result.isVersioned).toBe(true);
});
it('should detect nodes with version array', () => {
const NodeClass = nodeClassFactory.build({
description: {
name: 'test',
version: [1, 1.1, 2]
}
});
const result = parser.parse(NodeClass as any);
expect(result.isVersioned).toBe(true);
});
it('should detect nodes with defaultVersion', () => {
const NodeClass = nodeClassFactory.build({
description: {
name: 'test',
defaultVersion: 2
}
});
const result = parser.parse(NodeClass as any);
expect(result.isVersioned).toBe(true);
});
it('should handle instance-level version detection', () => {
const NodeClass = class {
description = {
name: 'test',
version: [1, 2, 3]
};
};
const result = parser.parse(NodeClass as any);
expect(result.isVersioned).toBe(true);
});
});
describe('edge cases', () => {
it('should handle empty routing object', () => {
const NodeClass = nodeClassFactory.build({
description: {
name: 'test',
routing: {}
}
});
const result = parser.parse(NodeClass as any);
expect(result.style).toBe('declarative');
expect(result.operations).toEqual([]);
});
it('should handle missing properties array', () => {
const NodeClass = nodeClassFactory.build({
description: {
name: 'test'
}
});
const result = parser.parse(NodeClass as any);
expect(result.properties).toEqual([]);
});
it('should handle missing credentials', () => {
const nodeDefinition = programmaticNodeFactory.build();
delete (nodeDefinition as any).credentials;
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
const result = parser.parse(NodeClass as any);
expect(result.credentials).toEqual([]);
});
it('should handle nodes with baseDescription but no name in main description', () => {
const NodeClass = class {
description = {};
baseDescription = {
name: 'baseNode',
displayName: 'Base Node'
};
};
const result = parser.parse(NodeClass as any);
expect(result.nodeType).toBe('baseNode');
expect(result.displayName).toBe('Base Node');
});
it('should handle complex nested routing structures', () => {
const NodeClass = nodeClassFactory.build({
description: {
name: 'test',
routing: {
request: {
resource: {
options: []
},
operation: {
options: [] // Should be array, not object
}
},
operations: {}
}
}
});
const result = parser.parse(NodeClass as any);
expect(result.operations).toEqual([]);
});
it('should handle operations without displayName', () => {
const NodeClass = nodeClassFactory.build({
description: {
name: 'test',
properties: [
{
name: 'operation',
type: 'options',
displayOptions: {
show: {}
},
options: [
{ value: 'create' }, // No name field
{ value: 'update', name: 'Update' }
]
}
]
}
});
const result = parser.parse(NodeClass as any);
// Should handle missing names gracefully
expect(result.operations).toHaveLength(2);
});
});
});