feat(editor): Add ability to extract sub-workflows to canvas context menu (#15538)

This commit is contained in:
Charlie Kolb
2025-06-02 12:17:27 +02:00
committed by GitHub
parent 096806af15
commit 5985df6e51
23 changed files with 2070 additions and 373 deletions

View File

@@ -1,323 +0,0 @@
import {
getInputEdges,
getOutputEdges,
getRootNodes,
getLeafNodes,
parseExtractableSubgraphSelection,
hasPath,
} from '@/graph/graph-utils';
describe('graphUtils', () => {
describe('getInputEdges', () => {
it('should return edges leading into the graph', () => {
const graphIds = new Set(['B', 'C']);
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B'])],
['B', new Set(['C'])],
]);
const result = getInputEdges(graphIds, adjacencyList);
expect(result).toEqual([['A', 'B']]);
});
it('should return an empty array if there are no input edges', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B'])],
['B', new Set()],
]);
const result = getInputEdges(graphIds, adjacencyList);
expect(result).toEqual([]);
});
});
describe('getOutputEdges', () => {
it('should return edges leading out of the graph', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B'])],
['B', new Set(['C'])],
['C', new Set()],
]);
const result = getOutputEdges(graphIds, adjacencyList);
expect(result).toEqual([['B', 'C']]);
});
it('should return an empty array if there are no output edges', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<string>>([['A', new Set(['B'])]]);
const result = getOutputEdges(graphIds, adjacencyList);
expect(result).toEqual([]);
});
});
describe('getRootNodes', () => {
it('should return root nodes of the graph', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<string>>([['A', new Set(['B'])]]);
const result = getRootNodes(graphIds, adjacencyList);
expect(result).toEqual(new Set(['A', 'C']));
});
it('should return all nodes if there are no incoming edges', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<string>>();
const result = getRootNodes(graphIds, adjacencyList);
expect(result).toEqual(new Set(['A', 'B']));
});
});
describe('getLeafNodes', () => {
it('should return leaf nodes of the graph', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B'])],
['B', new Set(['C'])],
['C', new Set()],
]);
const result = getLeafNodes(graphIds, adjacencyList);
expect(result).toEqual(new Set(['C']));
});
it('should return all nodes if there are no outgoing edges', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<string>>([
['A', new Set()],
['B', new Set()],
]);
const result = getLeafNodes(graphIds, adjacencyList);
expect(result).toEqual(new Set(['A', 'B']));
});
});
describe('parseExtractableSubgraphSelection', () => {
it('should return successfully for a valid extractable subgraph', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<string>>([
['C', new Set(['A'])],
['A', new Set(['B'])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toMatchObject({ start: 'A' });
});
it('should return successfully for multiple edges into single input node', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<string>>([
['X', new Set(['A'])],
['Y', new Set(['A'])],
['A', new Set(['B'])],
['B', new Set()],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toMatchObject({ start: 'A' });
});
it('should return successfully for multiple edges from single output nodes', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B'])],
['B', new Set(['X', 'Y'])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toMatchObject({});
});
it('should return errors for input edge to non-root node', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<string>>([
['X', new Set(['B'])],
['A', new Set(['B'])],
['B', new Set()],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual([
{
errorCode: 'Input Edge To Non-Root Node',
node: 'B',
},
]);
});
it('should return errors for output edge from non-leaf node', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<string>>([['A', new Set(['B', 'X'])]]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual([
{
errorCode: 'Output Edge From Non-Leaf Node',
node: 'A',
},
]);
});
it('should return successfully for multiple root nodes with 1 input', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['C'])],
['B', new Set(['C'])],
['X', new Set(['A'])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toMatchObject({});
});
it('should return an error for multiple root nodes with inputs', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['C'])],
['B', new Set(['C'])],
['X', new Set(['A'])],
['Y', new Set(['B'])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual([
{
errorCode: 'Multiple Input Nodes',
nodes: new Set(['A', 'B']),
},
]);
});
it('should return successfully for multiple leaf nodes with 1 output', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B', 'C'])],
['C', new Set(['X'])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toMatchObject({});
});
it('should return an error for multiple leaf nodes with outputs', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B', 'C'])],
['B', new Set(['X'])],
['C', new Set(['X'])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual([
{
errorCode: 'Multiple Output Nodes',
nodes: new Set(['B', 'C']),
},
]);
});
it('should return an error for a non-continuous selection', () => {
const graphIds = new Set(['A', 'D']);
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B'])],
['B', new Set(['C'])],
['C', new Set(['D'])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual([
{
errorCode: 'No Continuous Path From Root To Leaf In Selection',
start: 'D',
end: 'A',
},
]);
});
});
describe('hasPath', () => {
it('should return true for a direct path between start and end', () => {
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B'])],
['B', new Set(['C'])],
]);
const result = hasPath('A', 'C', adjacencyList);
expect(result).toBe(true);
});
it('should return false if there is no path between start and end', () => {
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B'])],
['C', new Set(['D'])],
]);
const result = hasPath('A', 'D', adjacencyList);
expect(result).toBe(false);
});
it('should return true for a path with multiple intermediate nodes', () => {
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B'])],
['B', new Set(['C'])],
['C', new Set(['D'])],
]);
const result = hasPath('A', 'D', adjacencyList);
expect(result).toBe(true);
});
it('should return false if the start node is not in the adjacency list', () => {
const adjacencyList = new Map<string, Set<string>>([
['B', new Set(['C'])],
['C', new Set(['D'])],
]);
const result = hasPath('A', 'D', adjacencyList);
expect(result).toBe(false);
});
it('should return false if the end node is not in the adjacency list', () => {
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B'])],
['B', new Set(['C'])],
]);
const result = hasPath('A', 'D', adjacencyList);
expect(result).toBe(false);
});
it('should return true for a cyclic graph where a path exists', () => {
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B'])],
['B', new Set(['C'])],
['C', new Set(['A'])],
]);
const result = hasPath('A', 'C', adjacencyList);
expect(result).toBe(true);
});
it('should return false for a cyclic graph where no path exists', () => {
const adjacencyList = new Map<string, Set<string>>([
['A', new Set(['B'])],
['B', new Set(['A'])],
['C', new Set(['D'])],
]);
const result = hasPath('A', 'D', adjacencyList);
expect(result).toBe(false);
});
it('should return true for a self-loop', () => {
const adjacencyList = new Map<string, Set<string>>([['A', new Set(['A'])]]);
const result = hasPath('A', 'A', adjacencyList);
expect(result).toBe(true);
});
});
});

View File

@@ -0,0 +1,484 @@
import {
getInputEdges,
getOutputEdges,
getRootNodes,
getLeafNodes,
parseExtractableSubgraphSelection,
hasPath,
buildAdjacencyList,
} from '@/graph/graph-utils';
import type { IConnection, IConnections, NodeConnectionType } from '@/index';
function makeConnection(
node: string,
index: number = 0,
type: NodeConnectionType = 'main',
): IConnection {
return {
node,
index,
type,
};
}
describe('graphUtils', () => {
describe('getInputEdges', () => {
it('should return edges leading into the graph', () => {
const graphIds = new Set(['B', 'C']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('C')])],
]);
const result = getInputEdges(graphIds, adjacencyList);
expect(result).toEqual([['A', makeConnection('B')]]);
});
it('should return an empty array if there are no input edges', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set()],
]);
const result = getInputEdges(graphIds, adjacencyList);
expect(result).toEqual([]);
});
});
describe('getOutputEdges', () => {
it('should return edges leading out of the graph', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('C')])],
['C', new Set()],
]);
const result = getOutputEdges(graphIds, adjacencyList);
expect(result).toEqual([['B', makeConnection('C')]]);
});
it('should return an empty array if there are no output edges', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
]);
const result = getOutputEdges(graphIds, adjacencyList);
expect(result).toEqual([]);
});
});
describe('getRootNodes', () => {
it('should return root nodes of the graph', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
]);
const result = getRootNodes(graphIds, adjacencyList);
expect(result).toEqual(new Set(['A', 'C']));
});
it('should return all nodes if there are no incoming edges', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<IConnection>>();
const result = getRootNodes(graphIds, adjacencyList);
expect(result).toEqual(new Set(['A', 'B']));
});
});
describe('getLeafNodes', () => {
it('should return leaf nodes of the graph', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('C')])],
['C', new Set()],
]);
const result = getLeafNodes(graphIds, adjacencyList);
expect(result).toEqual(new Set(['C']));
});
it('should return all nodes if there are no outgoing edges', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set()],
['B', new Set()],
]);
const result = getLeafNodes(graphIds, adjacencyList);
expect(result).toEqual(new Set(['A', 'B']));
});
});
describe('parseExtractableSubgraphSelection', () => {
it('should return successfully for a valid extractable subgraph', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<IConnection>>([
['C', new Set([makeConnection('A')])],
['A', new Set([makeConnection('B')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual({ start: 'A', end: undefined });
});
it('should return successfully for multiple edges into single input node', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<IConnection>>([
['X', new Set([makeConnection('A')])],
['Y', new Set([makeConnection('A')])],
['A', new Set([makeConnection('B')])],
['B', new Set()],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual({ start: 'A', end: undefined });
});
it('should return successfully for multiple edges from single output nodes', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('X'), makeConnection('Y')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual({ start: undefined, end: 'B' });
});
it('should return errors for input edge to non-root node', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<IConnection>>([
['X', new Set([makeConnection('B')])],
['A', new Set([makeConnection('B')])],
['B', new Set()],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual([
{
errorCode: 'Input Edge To Non-Root Node',
node: 'B',
},
]);
});
it('should return errors for output edge from non-leaf node', () => {
const graphIds = new Set(['A', 'B']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B'), makeConnection('X')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual([
{
errorCode: 'Output Edge From Non-Leaf Node',
node: 'A',
},
]);
});
it('should return successfully for multiple root nodes with 1 input', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('C')])],
['B', new Set([makeConnection('C')])],
['X', new Set([makeConnection('A')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual({ start: 'A', end: undefined });
});
it('should return an error for multiple root nodes with inputs', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('C')])],
['B', new Set([makeConnection('C')])],
['X', new Set([makeConnection('A')])],
['Y', new Set([makeConnection('B')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual([
{
errorCode: 'Multiple Input Nodes',
nodes: new Set(['A', 'B']),
},
]);
});
it('should return successfully for multiple leaf nodes with 1 output', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B'), makeConnection('C')])],
['C', new Set([makeConnection('X')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual({ start: undefined, end: 'C' });
});
it('should return an error for multiple leaf nodes with outputs', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B'), makeConnection('C')])],
['B', new Set([makeConnection('X')])],
['C', new Set([makeConnection('X')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual([
{
errorCode: 'Multiple Output Nodes',
nodes: new Set(['B', 'C']),
},
]);
});
it('should return an error for a non-continuous selection', () => {
const graphIds = new Set(['A', 'D']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('C')])],
['C', new Set([makeConnection('D')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual([
{
errorCode: 'No Continuous Path From Root To Leaf In Selection',
start: 'D',
end: 'A',
},
]);
});
it('should allow loop with node itself', () => {
const graphIds = new Set(['A']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('A')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual({ start: undefined, end: undefined });
});
it('should allow loop with node itself with input and output', () => {
const graphIds = new Set(['B']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('B'), makeConnection('C')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual({ start: 'B', end: 'B' });
});
it('should allow loop within selection', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('C')])],
['C', new Set([makeConnection('A')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual({ start: undefined, end: undefined });
});
it('should allow loop within selection with input', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('C')])],
['C', new Set([makeConnection('A')])],
['D', new Set([makeConnection('B')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual({ start: 'B', end: undefined });
});
it('should allow loop within selection with two inputs', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('C')])],
['C', new Set([makeConnection('A')])],
['D', new Set([makeConnection('B')])],
['E', new Set([makeConnection('B')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual({ start: 'B', end: undefined });
});
it('should not allow loop within selection with inputs to different nodes', () => {
const graphIds = new Set(['A', 'B', 'C']);
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('C')])],
['C', new Set([makeConnection('A')])],
['D', new Set([makeConnection('B')])],
['E', new Set([makeConnection('C')])],
]);
const result = parseExtractableSubgraphSelection(graphIds, adjacencyList);
expect(result).toEqual([
{ errorCode: 'Input Edge To Non-Root Node', node: 'B' },
{ errorCode: 'Input Edge To Non-Root Node', node: 'C' },
]);
});
});
describe('hasPath', () => {
it('should return true for a direct path between start and end', () => {
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('C')])],
]);
const result = hasPath('A', 'C', adjacencyList);
expect(result).toBe(true);
});
it('should return false if there is no path between start and end', () => {
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['C', new Set([makeConnection('D')])],
]);
const result = hasPath('A', 'D', adjacencyList);
expect(result).toBe(false);
});
it('should return true for a path with multiple intermediate nodes', () => {
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('C')])],
['C', new Set([makeConnection('D')])],
]);
const result = hasPath('A', 'D', adjacencyList);
expect(result).toBe(true);
});
it('should return false if the start node is not in the adjacency list', () => {
const adjacencyList = new Map<string, Set<IConnection>>([
['B', new Set([makeConnection('C')])],
['C', new Set([makeConnection('D')])],
]);
const result = hasPath('A', 'D', adjacencyList);
expect(result).toBe(false);
});
it('should return false if the end node is not in the adjacency list', () => {
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('C')])],
]);
const result = hasPath('A', 'D', adjacencyList);
expect(result).toBe(false);
});
it('should return true for a cyclic graph where a path exists', () => {
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('C')])],
['C', new Set([makeConnection('A')])],
]);
const result = hasPath('A', 'C', adjacencyList);
expect(result).toBe(true);
});
it('should return false for a cyclic graph where no path exists', () => {
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B')])],
['B', new Set([makeConnection('A')])],
['C', new Set([makeConnection('D')])],
]);
const result = hasPath('A', 'D', adjacencyList);
expect(result).toBe(false);
});
it('should return true for a self-loop', () => {
const adjacencyList = new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('A')])],
]);
const result = hasPath('A', 'A', adjacencyList);
expect(result).toBe(true);
});
});
describe('buildAdjacencyList', () => {
it('should build an adjacency list from connections by source node', () => {
const connectionsBySourceNode: IConnections = {
A: {
main: [
[
{ node: 'B', index: 0, type: 'main' },
{ node: 'C', index: 1, type: 'main' },
],
],
},
B: {
main: [[{ node: 'D', index: 0, type: 'main' }]],
},
};
const result = buildAdjacencyList(connectionsBySourceNode);
expect(result).toEqual(
new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B', 0), makeConnection('C', 1)])],
['B', new Set([makeConnection('D', 0)])],
]),
);
});
it('should handle an empty connections object', () => {
const connectionsBySourceNode = {};
const result = buildAdjacencyList(connectionsBySourceNode);
expect(result).toEqual(new Map());
});
it('should handle connections with multiple types', () => {
const connectionsBySourceNode: IConnections = {
A: {
main: [[{ node: 'B', index: 0, type: 'main' }]],
ai_tool: [[{ node: 'C', index: 1, type: 'ai_tool' }]],
},
};
const result = buildAdjacencyList(connectionsBySourceNode);
expect(result).toEqual(
new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B', 0, 'main'), makeConnection('C', 1, 'ai_tool')])],
]),
);
});
it('should handle connections with multiple indices', () => {
const connectionsBySourceNode: IConnections = {
A: {
main: [[{ node: 'B', index: 0, type: 'main' }], [{ node: 'C', index: 1, type: 'main' }]],
},
};
const result = buildAdjacencyList(connectionsBySourceNode);
expect(result).toEqual(
new Map<string, Set<IConnection>>([
['A', new Set([makeConnection('B', 0), makeConnection('C', 1)])],
]),
);
});
});
});

View File

@@ -196,7 +196,7 @@ describe('NodeReferenceParserUtils', () => {
nodes = [makeNode('B', ['$("D")'])];
nodeNames = ['B', 'D'];
const result = extractReferencesInNodeExpressions(nodes, nodeNames, startNodeName, 'B');
const result = extractReferencesInNodeExpressions(nodes, nodeNames, startNodeName, ['B']);
expect([...result.variables.entries()]).toEqual([]);
expect(result.nodes).toEqual([
{
@@ -210,7 +210,7 @@ describe('NodeReferenceParserUtils', () => {
nodes = [makeNode('B', ['$("E").item.json.x'])];
nodeNames = ['B'];
const result = extractReferencesInNodeExpressions(nodes, nodeNames, startNodeName, 'B');
const result = extractReferencesInNodeExpressions(nodes, nodeNames, startNodeName, ['B']);
expect([...result.variables.entries()]).toEqual([]);
expect(result.nodes).toEqual([
{
@@ -249,7 +249,7 @@ describe('NodeReferenceParserUtils', () => {
nodes = [makeNode('B', ['$json.a.b.c_d["e"]["f"]']), makeNode('C', ['$json.x.y.z'])];
nodeNames = ['A', 'B', 'C'];
const result = extractReferencesInNodeExpressions(nodes, nodeNames, startNodeName, 'B');
const result = extractReferencesInNodeExpressions(nodes, nodeNames, startNodeName, ['B']);
expect([...result.variables.entries()]).toEqual([['a_b_c_d', '$json.a.b.c_d']]);
expect(result.nodes).toEqual([
{
@@ -262,6 +262,31 @@ describe('NodeReferenceParserUtils', () => {
},
]);
});
it('should handle complex $json case for first node', () => {
nodes = [
{
parameters: {
p0: '=https://raw.githubusercontent.com/{{ $json.org }}/{{ $json.repo }}/refs/heads/master/package.json',
},
name: 'A',
} as unknown as INode,
];
nodeNames = ['A', 'B'];
const result = extractReferencesInNodeExpressions(nodes, nodeNames, startNodeName, ['A']);
expect([...result.variables.entries()]).toEqual([
['repo', '$json.repo'],
['org', '$json.org'],
]);
expect(result.nodes).toEqual([
{
name: 'A',
parameters: {
p0: '=https://raw.githubusercontent.com/{{ $json.org }}/{{ $json.repo }}/refs/heads/master/package.json',
},
},
]);
});
it('should support different node accessor patterns', () => {
nodes = [
makeNode('N', ['$("A").item.json.myField']),
@@ -621,6 +646,56 @@ describe('NodeReferenceParserUtils', () => {
},
]);
});
it('should handle assignments format of Set node correctly', () => {
nodes = [
{
parameters: {
assignments: {
assignments: [
{
id: 'cf8bd6cb-f28a-4a73-b141-02e5c22cfe74',
name: 'ghApiBaseUrl',
value: '={{ $("A").item.json.x.y.z }}',
type: 'string',
},
],
},
options: {},
},
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [80, 80],
id: '6e2fd284-2aba-4dee-8921-18be9a291484',
name: 'Params',
},
];
nodeNames = ['A', 'Params'];
const result = extractReferencesInNodeExpressions(nodes, nodeNames, startNodeName);
expect([...result.variables.entries()]).toEqual([['x_y_z', '$("A").item.json.x.y.z']]);
expect(result.nodes).toEqual([
{
parameters: {
assignments: {
assignments: [
{
id: 'cf8bd6cb-f28a-4a73-b141-02e5c22cfe74',
name: 'ghApiBaseUrl',
value: "={{ $('Start').item.json.x_y_z }}",
type: 'string',
},
],
},
options: {},
},
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [80, 80],
id: '6e2fd284-2aba-4dee-8921-18be9a291484',
name: 'Params',
},
]);
});
it('should carry over unrelated properties', () => {
nodes = [
{

View File

@@ -2418,4 +2418,227 @@ describe('Workflow', () => {
expect(nodes).toHaveLength(0);
});
});
describe('getConnectionsBetweenNodes', () => {
test('should return empty array if no connections exist between sources and targets', () => {
const result = SIMPLE_WORKFLOW.getConnectionsBetweenNodes(['Start'], ['Set1']);
expect(result).toEqual([]);
});
test('should return connections between a single source and target', () => {
const result = SIMPLE_WORKFLOW.getConnectionsBetweenNodes(['Start'], ['Set']);
expect(result).toEqual([
[
{ node: 'Start', index: 0, type: NodeConnectionTypes.Main },
{ node: 'Set', type: NodeConnectionTypes.Main, index: 0 },
],
]);
});
test('should return connections between multiple sources and a single target', () => {
const connections: IConnections = {
Node1: {
main: [[{ node: 'TargetNode', type: NodeConnectionTypes.Main, index: 0 }]],
},
Node2: {
main: [[{ node: 'TargetNode', type: NodeConnectionTypes.Main, index: 0 }]],
},
};
const workflow = new Workflow({
id: 'test',
nodes: [
{
id: 'Node1',
name: 'Node1',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
parameters: {},
},
{
id: 'Node2',
name: 'Node2',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
parameters: {},
},
{
id: 'TargetNode',
name: 'TargetNode',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
parameters: {},
},
],
connections,
active: false,
nodeTypes,
});
const result = workflow.getConnectionsBetweenNodes(['Node1', 'Node2'], ['TargetNode']);
expect(result).toEqual([
[
{ node: 'Node1', index: 0, type: NodeConnectionTypes.Main },
{ node: 'TargetNode', type: NodeConnectionTypes.Main, index: 0 },
],
[
{ node: 'Node2', index: 0, type: NodeConnectionTypes.Main },
{ node: 'TargetNode', type: NodeConnectionTypes.Main, index: 0 },
],
]);
});
test('should return connections between a single source and multiple targets', () => {
const connections: IConnections = {
Node1: {
main: [
[
{ node: 'TargetNode1', type: NodeConnectionTypes.Main, index: 0 },
{ node: 'TargetNode2', type: NodeConnectionTypes.Main, index: 0 },
],
],
},
};
const workflow = new Workflow({
id: 'test',
nodes: [
{
id: 'Node1',
name: 'Node1',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
parameters: {},
},
{
id: 'TargetNode1',
name: 'TargetNode1',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
parameters: {},
},
{
id: 'TargetNode2',
name: 'TargetNode2',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
parameters: {},
},
],
connections,
active: false,
nodeTypes,
});
const result = workflow.getConnectionsBetweenNodes(['Node1'], ['TargetNode1', 'TargetNode2']);
expect(result).toEqual([
[
{ node: 'Node1', index: 0, type: NodeConnectionTypes.Main },
{ node: 'TargetNode1', type: NodeConnectionTypes.Main, index: 0 },
],
[
{ node: 'Node1', index: 0, type: NodeConnectionTypes.Main },
{ node: 'TargetNode2', type: NodeConnectionTypes.Main, index: 0 },
],
]);
});
test('should handle workflows with multiple connection types', () => {
const connections: IConnections = {
Node1: {
main: [
[
{ node: 'TargetNode', type: NodeConnectionTypes.Main, index: 0 },
{ node: 'TargetNode', type: NodeConnectionTypes.Main, index: 1 },
],
],
[NodeConnectionTypes.AiAgent]: [
[{ node: 'TargetNode', type: NodeConnectionTypes.AiAgent, index: 0 }],
],
},
};
const workflow = new Workflow({
id: 'test',
nodes: [
{
id: 'Node1',
name: 'Node1',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
parameters: {},
},
{
id: 'TargetNode',
name: 'TargetNode',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
parameters: {},
},
],
connections,
active: false,
nodeTypes,
});
const result = workflow.getConnectionsBetweenNodes(['Node1'], ['TargetNode']);
expect(result).toEqual([
[
{ node: 'Node1', index: 0, type: NodeConnectionTypes.Main },
{ node: 'TargetNode', type: NodeConnectionTypes.Main, index: 0 },
],
[
{ node: 'Node1', index: 0, type: NodeConnectionTypes.Main },
{ node: 'TargetNode', type: NodeConnectionTypes.Main, index: 1 },
],
[
{ node: 'Node1', index: 0, type: NodeConnectionTypes.AiAgent },
{ node: 'TargetNode', type: NodeConnectionTypes.AiAgent, index: 0 },
],
]);
});
test('should handle nodes with no connections', () => {
const connections: IConnections = {
Node1: {
main: [[]],
},
};
const workflow = new Workflow({
id: 'test',
nodes: [
{
id: 'Node1',
name: 'Node1',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
parameters: {},
},
{
id: 'TargetNode',
name: 'TargetNode',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
parameters: {},
},
],
connections,
active: false,
nodeTypes,
});
const result = workflow.getConnectionsBetweenNodes(['Node1'], ['TargetNode']);
expect(result).toEqual([]);
});
});
});