Files
n8n-enterprise-unlocked/packages/frontend/editor-ui/src/features/logs/logs.utils.test.ts

1274 lines
39 KiB
TypeScript

import {
createTestNode,
createTestTaskData,
createTestWorkflow,
createTestWorkflowExecutionResponse,
createTestWorkflowObject,
} from '@/__tests__/mocks';
import {
createLogTree,
deepToRaw,
findSelectedLogEntry,
getDefaultCollapsedEntries,
getTreeNodeData,
mergeStartData,
restoreChatHistory,
} from './logs.utils';
import {
AGENT_LANGCHAIN_NODE_TYPE,
NodeConnectionTypes,
type ExecutionError,
type ITaskStartedData,
} from 'n8n-workflow';
import {
aiAgentNode,
aiChatWorkflow,
aiModelNode,
createTestLogTreeCreationContext,
} from './__test__/data';
import type { LogEntrySelection } from './logs.types';
import type { IExecutionResponse } from '@/Interface';
import { isReactive, reactive } from 'vue';
import { createTestLogEntry } from './__test__/mocks';
import { AGENT_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE } from '@/constants';
describe(getTreeNodeData, () => {
it('should generate one node per execution', () => {
const nodeA = createTestNode({ name: 'A', id: 'test-node-id-a' });
const workflow = createTestWorkflowObject({
id: 'test-wf-id',
nodes: [
nodeA,
createTestNode({ name: 'B', id: 'test-node-id-b' }),
createTestNode({ name: 'C', id: 'test-node-id-c' }),
],
connections: {
B: { ai_tool: [[{ node: 'A', type: NodeConnectionTypes.AiTool, index: 0 }]] },
C: {
ai_languageModel: [[{ node: 'B', type: NodeConnectionTypes.AiLanguageModel, index: 0 }]],
},
},
});
const jsonB1 = { tokenUsage: { completionTokens: 1, promptTokens: 2, totalTokens: 3 } };
const jsonB2 = { tokenUsage: { completionTokens: 4, promptTokens: 5, totalTokens: 6 } };
const jsonC1 = { tokenUsageEstimate: { completionTokens: 7, promptTokens: 8, totalTokens: 9 } };
const ctx = createTestLogTreeCreationContext(workflow, {
A: [createTestTaskData({ startTime: 1740528000000 })],
B: [
createTestTaskData({
startTime: 1740528000001,
data: { main: [[{ json: jsonB1 }]] },
}),
createTestTaskData({
startTime: 1740528000002,
data: { main: [[{ json: jsonB2 }]] },
}),
],
C: [
createTestTaskData({
startTime: 1740528000003,
data: { main: [[{ json: jsonC1 }]] },
}),
createTestTaskData({ startTime: 1740528000004 }),
],
});
const logTree = getTreeNodeData(nodeA, ctx.data.resultData.runData.A[0], undefined, ctx);
expect(logTree.length).toBe(1);
expect(logTree[0].id).toBe('test-wf-id:A:test-execution-id:0');
expect(logTree[0].depth).toBe(0);
expect(logTree[0].runIndex).toBe(0);
expect(logTree[0].parent).toBe(undefined);
expect(logTree[0].runData?.startTime).toBe(1740528000000);
expect(logTree[0].children.length).toBe(2);
expect(logTree[0].children[0].id).toBe('test-wf-id:B:test-execution-id:0');
expect(logTree[0].children[0].depth).toBe(1);
expect(logTree[0].children[0].runIndex).toBe(0);
expect(logTree[0].children[0].parent?.node.name).toBe('A');
expect(logTree[0].children[0].runData?.startTime).toBe(1740528000001);
expect(logTree[0].children[0].consumedTokens.isEstimate).toBe(false);
expect(logTree[0].children[0].consumedTokens.completionTokens).toBe(1);
expect(logTree[0].children[0].children.length).toBe(1);
expect(logTree[0].children[0].children[0].id).toBe('test-wf-id:C:test-execution-id:0');
expect(logTree[0].children[0].children[0].depth).toBe(2);
expect(logTree[0].children[0].children[0].runIndex).toBe(0);
expect(logTree[0].children[0].children[0].parent?.node.name).toBe('B');
expect(logTree[0].children[0].children[0].consumedTokens.isEstimate).toBe(true);
expect(logTree[0].children[0].children[0].consumedTokens.completionTokens).toBe(7);
expect(logTree[0].children[1].id).toBe('test-wf-id:B:test-execution-id:1');
expect(logTree[0].children[1].depth).toBe(1);
expect(logTree[0].children[1].runIndex).toBe(1);
expect(logTree[0].children[1].parent?.node.name).toBe('A');
expect(logTree[0].children[1].consumedTokens.isEstimate).toBe(false);
expect(logTree[0].children[1].consumedTokens.completionTokens).toBe(4);
expect(logTree[0].children[1].children.length).toBe(1);
expect(logTree[0].children[1].children[0].id).toBe('test-wf-id:C:test-execution-id:1');
expect(logTree[0].children[1].children[0].depth).toBe(2);
expect(logTree[0].children[1].children[0].runIndex).toBe(1);
expect(logTree[0].children[1].children[0].parent?.node.name).toBe('B');
expect(logTree[0].children[1].children[0].consumedTokens.completionTokens).toBe(0);
});
it('should filter node executions based on source node', () => {
const rootNode1 = createTestNode({ name: 'RootNode1' });
const rootNode2 = createTestNode({ name: 'RootNode2' });
const workflow = createTestWorkflowObject({
nodes: [rootNode1, rootNode2, createTestNode({ name: 'SharedSubNode' })],
connections: {
SharedSubNode: {
ai_tool: [
[{ node: 'RootNode1', type: NodeConnectionTypes.AiTool, index: 0 }],
[{ node: 'RootNode2', type: NodeConnectionTypes.AiTool, index: 0 }],
],
},
},
});
// Create test run data with source information
const runData = {
RootNode1: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:00.000Z'),
executionIndex: 0,
}),
],
RootNode2: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
executionIndex: 1,
}),
],
SharedSubNode: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
executionIndex: 2,
source: [{ previousNode: 'RootNode1', previousNodeRun: 0 }],
data: { main: [[{ json: { result: 'from RootNode1' } }]] },
}),
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:03.000Z'),
executionIndex: 3,
source: [{ previousNode: 'RootNode2', previousNodeRun: 1 }],
data: { main: [[{ json: { result: 'from RootNode2' } }]] },
}),
],
};
// Test for RootNode1 - should only show SharedSubNode with source RootNode1
const rootNode1Tree = getTreeNodeData(
rootNode1,
runData.RootNode1[0],
undefined,
createTestLogTreeCreationContext(workflow, runData),
);
expect(rootNode1Tree[0].children.length).toBe(1);
expect(rootNode1Tree[0].children[0].node.name).toBe('SharedSubNode');
expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
// Test for RootNode2 - should only show SharedSubNode with source RootNode2
const rootNode2Tree = getTreeNodeData(
rootNode2,
runData.RootNode2[0],
undefined,
createTestLogTreeCreationContext(workflow, runData),
);
expect(rootNode2Tree[0].children.length).toBe(1);
expect(rootNode2Tree[0].children[0].node.name).toBe('SharedSubNode');
expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
});
it('should filter node executions based on source run index', () => {
const rootNode = createTestNode({ name: 'RootNode' });
const workflow = createTestWorkflowObject({
nodes: [rootNode, createTestNode({ name: 'SubNode' })],
connections: {
SubNode: {
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
},
},
});
// Create test run data with source information
const runData = {
RootNode: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:00.000Z'),
executionIndex: 0,
}),
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
executionIndex: 2,
}),
],
SubNode: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
executionIndex: 1,
}),
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:03.000Z'),
executionIndex: 3,
}),
],
};
// Test for run #1 of RootNode - should only show SubNode with source run index 0
const rootNode1Tree = getTreeNodeData(
rootNode,
runData.RootNode[0],
0,
createTestLogTreeCreationContext(workflow, runData),
);
expect(rootNode1Tree[0].children.length).toBe(1);
expect(rootNode1Tree[0].children[0].node.name).toBe('SubNode');
expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
// Test for run #2 of RootNode - should only show SubNode with source run index 1
const rootNode2Tree = getTreeNodeData(
rootNode,
runData.RootNode[1],
1,
createTestLogTreeCreationContext(workflow, runData),
);
expect(rootNode2Tree[0].children.length).toBe(1);
expect(rootNode2Tree[0].children[0].node.name).toBe('SubNode');
expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
});
it('should include nodes without source information', () => {
const rootNode = createTestNode({ name: 'RootNode' });
const workflow = createTestWorkflowObject({
nodes: [rootNode, createTestNode({ name: 'SubNode' })],
connections: {
SubNode: {
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
},
},
});
// Create test run data with a node that has no source field
const runData = {
RootNode: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:00.000Z'),
executionIndex: 0,
}),
],
SubNode: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
executionIndex: 1,
// No source field
data: { main: [[{ json: { result: 'from RootNode' } }]] },
}),
],
};
// Test for RootNode - should still show SubNode even without source info
const rootNodeTree = getTreeNodeData(
rootNode,
runData.RootNode[0],
undefined,
createTestLogTreeCreationContext(workflow, runData),
);
expect(rootNodeTree[0].children.length).toBe(1);
expect(rootNodeTree[0].children[0].node.name).toBe('SubNode');
expect(rootNodeTree[0].children[0].runIndex).toBe(0);
});
it('should include nodes with empty source array', () => {
const rootNode = createTestNode({ name: 'RootNode' });
const workflow = createTestWorkflowObject({
nodes: [rootNode, createTestNode({ name: 'SubNode' })],
connections: {
SubNode: {
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
},
},
});
// Create test run data with a node that has empty source array
const runData = {
RootNode: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:00.000Z'),
executionIndex: 0,
}),
],
SubNode: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
executionIndex: 1,
source: [], // Empty array
data: { main: [[{ json: { result: 'from RootNode' } }]] },
}),
],
};
// Test for RootNode - should still show SubNode even with empty source array
const rootNodeTree = getTreeNodeData(
rootNode,
runData.RootNode[0],
undefined,
createTestLogTreeCreationContext(workflow, runData),
);
expect(rootNodeTree[0].children.length).toBe(1);
expect(rootNodeTree[0].children[0].node.name).toBe('SubNode');
expect(rootNodeTree[0].children[0].runIndex).toBe(0);
});
it('should include nodes with source array without previous node', () => {
const rootNode = createTestNode({ name: 'RootNode' });
const workflow = createTestWorkflowObject({
nodes: [rootNode, createTestNode({ name: 'SubNode' })],
connections: {
SubNode: {
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
},
},
});
// Create test run data with source information
const runData = {
RootNode: [createTestTaskData({ executionIndex: 0 })],
SubNode: [createTestTaskData({ executionIndex: 1, source: [null] })],
};
const rootNodeTree = getTreeNodeData(
rootNode,
runData.RootNode[0],
undefined,
createTestLogTreeCreationContext(workflow, runData),
);
expect(rootNodeTree[0].children.length).toBe(1);
expect(rootNodeTree[0].children[0].node.name).toBe('SubNode');
});
it('should filter deeper nested nodes based on source', () => {
const rootNode1 = createTestNode({ name: 'RootNode1' });
const rootNode2 = createTestNode({ name: 'RootNode2' });
const workflow = createTestWorkflowObject({
nodes: [
rootNode1,
rootNode2,
createTestNode({ name: 'SharedSubNode' }),
createTestNode({ name: 'DeepSubNode' }),
],
connections: {
SharedSubNode: {
ai_tool: [
[{ node: 'RootNode1', type: NodeConnectionTypes.AiTool, index: 0 }],
[{ node: 'RootNode2', type: NodeConnectionTypes.AiTool, index: 0 }],
],
},
DeepSubNode: {
ai_tool: [[{ node: 'SharedSubNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
},
},
});
// Create test run data with source information
const runData = {
RootNode1: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:00.000Z'),
executionIndex: 0,
}),
],
RootNode2: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
executionIndex: 1,
}),
],
SharedSubNode: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
executionIndex: 2,
source: [{ previousNode: 'RootNode1' }],
data: { main: [[{ json: { result: 'from RootNode1' } }]] },
}),
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:03.000Z'),
executionIndex: 3,
source: [{ previousNode: 'RootNode2' }],
data: { main: [[{ json: { result: 'from RootNode2' } }]] },
}),
],
DeepSubNode: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:04.000Z'),
executionIndex: 4,
source: [{ previousNode: 'SharedSubNode' }],
data: { main: [[{ json: { result: 'from SharedSubNode run 0' } }]] },
}),
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:05.000Z'),
executionIndex: 5,
source: [{ previousNode: 'SharedSubNode' }],
data: { main: [[{ json: { result: 'from SharedSubNode run 1' } }]] },
}),
],
};
// Test filtering for RootNode1
const rootNode1Tree = getTreeNodeData(
rootNode1,
runData.RootNode1[0],
undefined,
createTestLogTreeCreationContext(workflow, runData),
);
expect(rootNode1Tree[0].children.length).toBe(1);
expect(rootNode1Tree[0].children[0].node.name).toBe('SharedSubNode');
expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
expect(rootNode1Tree[0].children[0].children.length).toBe(1);
expect(rootNode1Tree[0].children[0].children[0].node.name).toBe('DeepSubNode');
expect(rootNode1Tree[0].children[0].children[0].runIndex).toBe(0);
// Test filtering for RootNode2
const rootNode2Tree = getTreeNodeData(
rootNode2,
runData.RootNode2[0],
undefined,
createTestLogTreeCreationContext(workflow, runData),
);
expect(rootNode2Tree[0].children.length).toBe(1);
expect(rootNode2Tree[0].children[0].node.name).toBe('SharedSubNode');
expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
expect(rootNode2Tree[0].children[0].children.length).toBe(1);
expect(rootNode2Tree[0].children[0].children[0].node.name).toBe('DeepSubNode');
expect(rootNode2Tree[0].children[0].children[0].runIndex).toBe(1);
});
it('should handle complex tree with multiple branches and filters correctly', () => {
const rootNode1 = createTestNode({ name: 'RootNode1' });
const rootNode2 = createTestNode({ name: 'RootNode2' });
const workflow = createTestWorkflowObject({
nodes: [
rootNode1,
rootNode2,
createTestNode({ name: 'SubNodeA' }),
createTestNode({ name: 'SubNodeB' }),
createTestNode({ name: 'DeepNode' }),
],
connections: {
SubNodeA: {
ai_tool: [[{ node: 'RootNode1', type: NodeConnectionTypes.AiTool, index: 0 }]],
},
SubNodeB: {
ai_tool: [[{ node: 'RootNode2', type: NodeConnectionTypes.AiTool, index: 0 }]],
},
DeepNode: {
ai_tool: [
[{ node: 'SubNodeA', type: NodeConnectionTypes.AiTool, index: 0 }],
[{ node: 'SubNodeB', type: NodeConnectionTypes.AiTool, index: 0 }],
],
},
},
});
// Create test run data with source information
const runData = {
RootNode1: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:00.000Z'),
executionIndex: 0,
}),
],
RootNode2: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
executionIndex: 1,
}),
],
SubNodeA: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
executionIndex: 2,
source: [{ previousNode: 'RootNode1' }],
data: { main: [[{ json: { result: 'from RootNode1' } }]] },
}),
],
SubNodeB: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:03.000Z'),
executionIndex: 3,
source: [{ previousNode: 'RootNode2' }],
data: { main: [[{ json: { result: 'from RootNode2' } }]] },
}),
],
DeepNode: [
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:04.000Z'),
executionIndex: 4,
source: [{ previousNode: 'SubNodeA' }],
data: { main: [[{ json: { result: 'from SubNodeA' } }]] },
}),
createTestTaskData({
startTime: Date.parse('2025-02-26T00:00:05.000Z'),
executionIndex: 5,
source: [{ previousNode: 'SubNodeB' }],
data: { main: [[{ json: { result: 'from SubNodeB' } }]] },
}),
],
};
// Test filtering for RootNode1 -> SubNodeA -> DeepNode
const rootNode1Tree = getTreeNodeData(
rootNode1,
runData.RootNode1[0],
undefined,
createTestLogTreeCreationContext(workflow, runData),
);
expect(rootNode1Tree[0].children.length).toBe(1);
expect(rootNode1Tree[0].children[0].node.name).toBe('SubNodeA');
expect(rootNode1Tree[0].children[0].children.length).toBe(1);
expect(rootNode1Tree[0].children[0].children[0].node.name).toBe('DeepNode');
expect(rootNode1Tree[0].children[0].children[0].runIndex).toBe(0); // First DeepNode execution
// Test filtering for RootNode2 -> SubNodeB -> DeepNode
const rootNode2Tree = getTreeNodeData(
rootNode2,
runData.RootNode2[0],
undefined,
createTestLogTreeCreationContext(workflow, runData),
);
expect(rootNode2Tree[0].children.length).toBe(1);
expect(rootNode2Tree[0].children[0].node.name).toBe('SubNodeB');
expect(rootNode2Tree[0].children[0].children.length).toBe(1);
expect(rootNode2Tree[0].children[0].children[0].node.name).toBe('DeepNode');
const deepNodeRunIndex = rootNode2Tree[0].children[0].children[0].runIndex;
expect(typeof deepNodeRunIndex).toBe('number');
});
});
describe(findSelectedLogEntry, () => {
function find(state: LogEntrySelection, response: IExecutionResponse) {
return findSelectedLogEntry(
state,
createLogTree(createTestWorkflowObject(response.workflowData), response),
);
}
describe('when log is not manually selected', () => {
it('should return undefined if no execution data exists', () => {
const response = createTestWorkflowExecutionResponse({
workflowData: createTestWorkflow({
nodes: [
createTestNode({ name: 'A' }),
createTestNode({ name: 'B' }),
createTestNode({ name: 'C' }),
],
}),
data: { resultData: { runData: {} } },
});
expect(find({ type: 'initial' }, response)).toBe(undefined);
});
it('should return first log entry with error', () => {
const response = createTestWorkflowExecutionResponse({
workflowData: createTestWorkflow({
nodes: [
createTestNode({ name: 'A' }),
createTestNode({ name: 'B' }),
createTestNode({ name: 'C' }),
],
}),
data: {
resultData: {
runData: {
A: [createTestTaskData({ executionStatus: 'success' })],
B: [createTestTaskData({ executionStatus: 'success' })],
C: [
createTestTaskData({ executionStatus: 'success' }),
createTestTaskData({ error: {} as ExecutionError, executionStatus: 'error' }),
createTestTaskData({ error: {} as ExecutionError, executionStatus: 'error' }),
],
},
},
},
});
expect(find({ type: 'initial' }, response)).toEqual(
expect.objectContaining({ node: expect.objectContaining({ name: 'C' }), runIndex: 1 }),
);
});
it("should return first log entry with error even if it's on a sub node", () => {
const response = createTestWorkflowExecutionResponse({
workflowData: createTestWorkflow({
nodes: [
createTestNode({ name: 'A' }),
createTestNode({ name: 'B' }),
createTestNode({ name: 'C' }),
],
connections: {
C: {
[NodeConnectionTypes.AiLanguageModel]: [
[{ node: 'B', type: NodeConnectionTypes.AiLanguageModel, index: 0 }],
],
},
},
}),
data: {
resultData: {
runData: {
A: [createTestTaskData({ executionStatus: 'success' })],
B: [createTestTaskData({ executionStatus: 'success' })],
C: [
createTestTaskData({ executionStatus: 'success' }),
createTestTaskData({ error: {} as ExecutionError, executionStatus: 'error' }),
createTestTaskData({ error: {} as ExecutionError, executionStatus: 'error' }),
],
},
},
},
});
expect(find({ type: 'initial' }, response)).toEqual(
expect.objectContaining({ node: expect.objectContaining({ name: 'C' }), runIndex: 1 }),
);
});
it('should return first log entry for AI agent node if there is no log entry with error', () => {
const response = createTestWorkflowExecutionResponse({
workflowData: createTestWorkflow({
nodes: [
createTestNode({ name: 'A' }),
createTestNode({ name: 'B', type: AGENT_LANGCHAIN_NODE_TYPE }),
createTestNode({ name: 'C' }),
],
}),
data: {
resultData: {
runData: {
A: [createTestTaskData({ executionStatus: 'success' })],
B: [createTestTaskData({ executionStatus: 'success' })],
C: [
createTestTaskData({ executionStatus: 'success' }),
createTestTaskData({ error: {} as ExecutionError, executionStatus: 'error' }),
createTestTaskData({ error: {} as ExecutionError, executionStatus: 'error' }),
],
},
},
},
});
expect(find({ type: 'initial' }, response)).toEqual(
expect.objectContaining({ node: expect.objectContaining({ name: 'B' }), runIndex: 0 }),
);
});
it('should return first log entry if there is no log entry with error nor executed AI agent node', () => {
const response = createTestWorkflowExecutionResponse({
workflowData: createTestWorkflow({
nodes: [
createTestNode({ name: 'A' }),
createTestNode({ name: 'B' }),
createTestNode({ name: 'C' }),
],
}),
data: {
resultData: {
runData: {
A: [createTestTaskData({ executionStatus: 'success' })],
B: [createTestTaskData({ executionStatus: 'success' })],
C: [
createTestTaskData({ executionStatus: 'success' }),
createTestTaskData({ executionStatus: 'success' }),
createTestTaskData({ executionStatus: 'success' }),
],
},
},
},
});
expect(find({ type: 'initial' }, response)).toEqual(
expect.objectContaining({ node: expect.objectContaining({ name: 'A' }), runIndex: 0 }),
);
});
});
describe('when log is manually selected', () => {
it('should return manually selected log', () => {
const response = createTestWorkflowExecutionResponse({
id: 'my-exec-id',
workflowData: createTestWorkflow({
id: 'test-wf-id',
nodes: [createTestNode({ name: 'A' }), createTestNode({ name: 'B' })],
}),
data: {
resultData: {
runData: {
A: [createTestTaskData({ executionStatus: 'success' })],
B: [createTestTaskData({ error: {} as ExecutionError, executionStatus: 'error' })],
},
},
},
});
const result = find({ type: 'selected', id: 'test-wf-id:A:my-exec-id:0' }, response);
expect(result).toEqual(
expect.objectContaining({ node: expect.objectContaining({ name: 'A' }), runIndex: 0 }),
);
});
});
});
describe(createLogTree, () => {
it('should return root node log entries in ascending order of executionIndex', () => {
const workflow = createTestWorkflowObject({
nodes: [
createTestNode({ name: 'A' }),
createTestNode({ name: 'B' }),
createTestNode({ name: 'C' }),
],
connections: {
B: { main: [[{ node: 'A', type: NodeConnectionTypes.Main, index: 0 }]] },
C: { main: [[{ node: 'B', type: NodeConnectionTypes.Main, index: 0 }]] },
},
});
const execution = createTestWorkflowExecutionResponse({
data: {
resultData: {
runData: {
A: [
createTestTaskData({
startTime: Date.parse('2025-04-04T00:00:00.000Z'),
executionIndex: 0,
}),
],
B: [
createTestTaskData({
startTime: Date.parse('2025-04-04T00:00:01.000Z'),
executionIndex: 1,
}),
],
C: [
createTestTaskData({
startTime: Date.parse('2025-04-04T00:00:02.000Z'),
executionIndex: 3,
}),
createTestTaskData({
startTime: Date.parse('2025-04-04T00:00:02.000Z'),
executionIndex: 2,
}),
],
},
},
},
});
expect(createLogTree(workflow, execution)).toEqual([
expect.objectContaining({ node: expect.objectContaining({ name: 'A' }), runIndex: 0 }),
expect.objectContaining({ node: expect.objectContaining({ name: 'B' }), runIndex: 0 }),
expect.objectContaining({ node: expect.objectContaining({ name: 'C' }), runIndex: 1 }),
expect.objectContaining({ node: expect.objectContaining({ name: 'C' }), runIndex: 0 }),
]);
});
it('should return sub node log entries in ascending order of executionIndex', () => {
const workflow = createTestWorkflowObject({
nodes: [
createTestNode({ name: 'A' }),
createTestNode({ name: 'B' }),
createTestNode({ name: 'C' }),
],
connections: {
A: { main: [[{ node: 'B', type: NodeConnectionTypes.Main, index: 0 }]] },
C: {
[NodeConnectionTypes.AiLanguageModel]: [
[{ node: 'B', type: NodeConnectionTypes.AiLanguageModel, index: 0 }],
],
},
},
});
expect(
createLogTree(
workflow,
createTestWorkflowExecutionResponse({
data: {
resultData: {
runData: {
A: [
createTestTaskData({
startTime: Date.parse('2025-04-04T00:00:00.000Z'),
executionIndex: 0,
}),
],
B: [
createTestTaskData({
startTime: Date.parse('2025-04-04T00:00:01.000Z'),
executionIndex: 1,
}),
],
C: [
createTestTaskData({
startTime: Date.parse('2025-04-04T00:00:02.000Z'),
executionIndex: 3,
}),
createTestTaskData({
startTime: Date.parse('2025-04-04T00:00:02.000Z'),
executionIndex: 2,
}),
],
},
},
},
}),
),
).toEqual([
expect.objectContaining({ node: expect.objectContaining({ name: 'A' }), runIndex: 0 }),
expect.objectContaining({
node: expect.objectContaining({ name: 'B' }),
runIndex: 0,
children: [
expect.objectContaining({ node: expect.objectContaining({ name: 'C' }), runIndex: 1 }),
expect.objectContaining({ node: expect.objectContaining({ name: 'C' }), runIndex: 0 }),
],
}),
]);
});
it('should not include runs for disabled nodes', () => {
const workflow = createTestWorkflowObject({
nodes: [createTestNode({ name: 'A' }), createTestNode({ name: 'B', disabled: true })],
connections: {
A: { main: [[{ node: 'B', type: NodeConnectionTypes.Main, index: 0 }]] },
},
});
const response = createTestWorkflowExecutionResponse({
data: {
resultData: {
runData: {
A: [createTestTaskData()],
B: [createTestTaskData()],
},
},
},
});
expect(createLogTree(workflow, response)).toEqual([
expect.objectContaining({ node: expect.objectContaining({ name: 'A' }) }),
]);
});
it('should include runs of a sub execution', () => {
const workflow = createTestWorkflowObject({
id: 'root-workflow-id',
nodes: [createTestNode({ name: 'A' }), createTestNode({ name: 'B' })],
connections: {
A: { main: [[{ node: 'B', type: NodeConnectionTypes.Main, index: 0 }]] },
},
});
const subWorkflow = createTestWorkflowObject({
id: 'sub-workflow-id',
nodes: [createTestNode({ name: 'C' })],
});
const rootExecutionData = createTestWorkflowExecutionResponse({
id: 'root-exec-id',
data: {
resultData: {
runData: {
A: [createTestTaskData()],
B: [
createTestTaskData({
metadata: {
subExecution: { workflowId: 'sub-workflow-id', executionId: 'sub-exec-id' },
},
}),
],
},
},
},
});
const subExecutionData = {
resultData: { runData: { C: [createTestTaskData(), createTestTaskData()] } },
};
const logs = createLogTree(
workflow,
rootExecutionData,
{ 'sub-workflow-id': subWorkflow },
{ 'sub-exec-id': subExecutionData },
);
expect(logs).toHaveLength(2);
expect(logs[0].node.name).toBe('A');
expect(logs[0].depth).toBe(0);
expect(logs[0].workflow).toBe(workflow);
expect(logs[0].execution).toBe(rootExecutionData.data);
expect(logs[0].executionId).toBe('root-exec-id');
expect(logs[0].children).toHaveLength(0);
expect(logs[1].node.name).toBe('B');
expect(logs[1].depth).toBe(0);
expect(logs[1].workflow).toBe(workflow);
expect(logs[1].execution).toBe(rootExecutionData.data);
expect(logs[1].executionId).toBe('root-exec-id');
expect(logs[1].children).toHaveLength(2);
expect(logs[1].children[0].node.name).toBe('C');
expect(logs[1].children[0].depth).toBe(1);
expect(logs[1].children[0].workflow).toBe(subWorkflow);
expect(logs[1].children[0].execution).toBe(subExecutionData);
expect(logs[1].children[0].executionId).toBe('sub-exec-id');
expect(logs[1].children[0].children).toHaveLength(0);
expect(logs[1].children[1].node.name).toBe('C');
expect(logs[1].children[1].depth).toBe(1);
expect(logs[1].children[1].workflow).toBe(subWorkflow);
expect(logs[1].children[1].execution).toBe(subExecutionData);
expect(logs[1].children[1].executionId).toBe('sub-exec-id');
expect(logs[1].children[1].children).toHaveLength(0);
});
it('should include all runs of sub nodes in sub execution under correct parent run', () => {
const workflow = createTestWorkflowObject({
id: 'root-workflow-id',
nodes: [createTestNode({ name: 'A' })],
});
const subWorkflow = createTestWorkflowObject({
id: 'sub-workflow-id',
nodes: [createTestNode({ name: 'B' }), createTestNode({ name: 'C' })],
connections: {
C: {
[NodeConnectionTypes.AiLanguageModel]: [
[{ node: 'B', type: NodeConnectionTypes.AiLanguageModel, index: 0 }],
],
},
},
});
const rootExecutionData = createTestWorkflowExecutionResponse({
id: 'root-exec-id',
data: {
resultData: {
runData: {
A: [
createTestTaskData({
metadata: {
subExecution: { workflowId: 'sub-workflow-id', executionId: 'sub-exec-id' },
},
}),
],
},
},
},
});
const subExecutionData = {
resultData: {
runData: {
B: [createTestTaskData(), createTestTaskData()],
C: [
createTestTaskData({ source: [{ previousNode: 'B', previousNodeRun: 0 }] }),
createTestTaskData({ source: [{ previousNode: 'B', previousNodeRun: 1 }] }),
createTestTaskData({ source: [{ previousNode: 'B', previousNodeRun: 1 }] }),
],
},
},
};
const logs = createLogTree(
workflow,
rootExecutionData,
{ 'sub-workflow-id': subWorkflow },
{ 'sub-exec-id': subExecutionData },
);
expect(logs).toHaveLength(1);
expect(logs[0].node.name).toBe('A');
expect(logs[0].children).toHaveLength(2);
expect(logs[0].children[0].node.name).toBe('B');
expect(logs[0].children[0].children).toHaveLength(1);
expect(logs[0].children[0].children[0].node.name).toBe('C');
expect(logs[0].children[1].node.name).toBe('B');
expect(logs[0].children[1].children).toHaveLength(2);
expect(logs[0].children[1].children[0].node.name).toBe('C');
expect(logs[0].children[1].children[1].node.name).toBe('C');
});
it('should not include nodes without run data', () => {
const logs = createLogTree(
createTestWorkflowObject(aiChatWorkflow),
createTestWorkflowExecutionResponse({ data: { resultData: { runData: {} } } }),
);
expect(logs).toHaveLength(0);
});
it('should include sub node log without run data in its root node', () => {
const taskData = createTestTaskData({
source: [{ previousNode: 'PartialExecutionToolExecutor' }],
});
const logs = createLogTree(
createTestWorkflowObject(aiChatWorkflow),
createTestWorkflowExecutionResponse({
data: { resultData: { runData: { [aiModelNode.name]: [taskData] } } },
}),
);
expect(logs).toHaveLength(1);
expect(logs[0].node.name).toBe(aiAgentNode.name);
expect(logs[0].runData).toBe(undefined);
expect(logs[0].children).toHaveLength(1);
expect(logs[0].children[0].node.name).toBe(aiModelNode.name);
});
it('should include sub node log with its root node disabled', () => {
const taskData = createTestTaskData({
source: [{ previousNode: 'PartialExecutionToolExecutor' }],
});
const logs = createLogTree(
createTestWorkflowObject({
...aiChatWorkflow,
nodes: [{ ...aiAgentNode, disabled: true }, aiModelNode],
}),
createTestWorkflowExecutionResponse({
data: { resultData: { runData: { [aiModelNode.name]: [taskData] } } },
}),
);
expect(logs).toHaveLength(1);
expect(logs[0].node.name).toBe(aiAgentNode.name);
expect(logs[0].runData).toBe(undefined);
expect(logs[0].children).toHaveLength(1);
expect(logs[0].children[0].node.name).toBe(aiModelNode.name);
});
it('should not include duplicate sub node log when the node belongs to multiple root nodes with no run data', () => {
const taskData = createTestTaskData({
source: [{ previousNode: 'PartialExecutionToolExecutor' }],
});
const logs = createLogTree(
createTestWorkflowObject({
nodes: [
{ ...aiAgentNode, name: 'Agent A' },
{ ...aiAgentNode, name: 'Agent B' },
aiModelNode,
],
connections: {
[aiModelNode.name]: {
[NodeConnectionTypes.AiLanguageModel]: [
[
{ node: 'Agent A', index: 0, type: NodeConnectionTypes.AiLanguageModel },
{ node: 'Agent B', index: 0, type: NodeConnectionTypes.AiLanguageModel },
],
],
},
},
}),
createTestWorkflowExecutionResponse({
data: { resultData: { runData: { [aiModelNode.name]: [taskData] } } },
}),
);
expect(logs).toHaveLength(1);
expect(logs[0].node.name).toBe('Agent B');
expect(logs[0].runData).toBe(undefined);
expect(logs[0].children).toHaveLength(1);
expect(logs[0].children[0].node.name).toBe(aiModelNode.name);
});
});
describe(deepToRaw, () => {
it('should convert reactive fields to raw in data with circular structure', () => {
const data = reactive({
foo: reactive({ bar: {} }),
bazz: {},
});
data.foo.bar = data;
data.bazz = data;
const raw = deepToRaw(data);
expect(isReactive(data)).toBe(true);
expect(isReactive(data.foo)).toBe(true);
expect(isReactive(data.foo.bar)).toBe(true);
expect(isReactive(data.bazz)).toBe(true);
expect(isReactive(raw)).toBe(false);
expect(isReactive(raw.foo)).toBe(false);
expect(isReactive(raw.foo.bar)).toBe(false);
expect(isReactive(raw.bazz)).toBe(false);
});
});
describe(mergeStartData, () => {
it('should return unchanged execution response if start data is empty', () => {
const response = createTestWorkflowExecutionResponse({
data: {
resultData: {
runData: {
A: [createTestTaskData()],
B: [createTestTaskData(), createTestTaskData()],
},
},
},
});
expect(mergeStartData({}, response)).toEqual(response);
});
it('should add runs in start data to the execution response as running state', () => {
const response = createTestWorkflowExecutionResponse({
data: {
resultData: {
runData: {
A: [createTestTaskData({ startTime: 0, executionIndex: 0 })],
B: [
createTestTaskData({ startTime: 1, executionIndex: 1 }),
createTestTaskData({ startTime: 2, executionIndex: 2 }),
],
},
},
},
});
const startData: { [nodeName: string]: ITaskStartedData[] } = {
B: [{ startTime: 3, executionIndex: 3, source: [] }],
C: [{ startTime: 4, executionIndex: 4, source: [] }],
};
const merged = mergeStartData(startData, response);
expect(merged.data?.resultData.runData.A).toEqual(response.data?.resultData.runData.A);
expect(merged.data?.resultData.runData.B).toEqual([
response.data!.resultData.runData.B[0],
response.data!.resultData.runData.B[1],
{ ...startData.B[0], executionStatus: 'running', executionTime: 0 },
]);
expect(merged.data?.resultData.runData.C).toEqual([
{ ...startData.C[0], executionStatus: 'running', executionTime: 0 },
]);
});
it('should not add runs in start data if a run with the same executionIndex already exists in response', () => {
const response = createTestWorkflowExecutionResponse({
data: {
resultData: {
runData: {
A: [createTestTaskData({ executionIndex: 0 })],
},
},
},
});
const startData = {
A: [createTestTaskData({ executionIndex: 0 })],
};
const merged = mergeStartData(startData, response);
expect(merged.data?.resultData.runData.A).toEqual(response.data?.resultData.runData.A);
});
it('should not add runs in start data if a run for the same node with larger start time already exists in response', () => {
const response = createTestWorkflowExecutionResponse({
data: {
resultData: {
runData: {
A: [createTestTaskData({ startTime: 1, executionIndex: 1 })],
},
},
},
});
const startData = {
A: [createTestTaskData({ startTime: 0, executionIndex: 0 })],
};
const merged = mergeStartData(startData, response);
expect(merged.data?.resultData.runData.A).toEqual(response.data?.resultData.runData.A);
});
});
describe(getDefaultCollapsedEntries, () => {
it('should recursively find logs for runs with a sub execution and has no child logs', () => {
const entries = [
// Has sub execution and has no children
createTestLogEntry({
id: 'l0',
runData: createTestTaskData({
metadata: { subExecution: { workflowId: 'w0', executionId: 'e0' } },
}),
children: [],
}),
// Has no sub execution
createTestLogEntry({ id: 'l1' }),
// Has sub execution and has children
createTestLogEntry({
id: 'l2',
runData: createTestTaskData({
metadata: { subExecution: { workflowId: 'w0', executionId: 'e0' } },
}),
children: [
// Has no sub execution - nested
createTestLogEntry({ id: 'l3' }),
// Has sub execution and has no children - nested
createTestLogEntry({
id: 'l4',
runData: createTestTaskData({
metadata: {
subExecution: { workflowId: 'w0', executionId: 'e0' },
},
}),
}),
],
}),
];
expect(getDefaultCollapsedEntries(entries)).toEqual({ l0: true, l4: true });
});
});
describe(restoreChatHistory, () => {
it('should return extracted chat input and bot message from workflow execution data', () => {
expect(
restoreChatHistory({
id: 'test-exec-id',
workflowData: createTestWorkflow({
nodes: [
createTestNode({ name: 'A', type: CHAT_TRIGGER_NODE_TYPE }),
createTestNode({ name: 'B', type: AGENT_NODE_TYPE }),
],
}),
data: {
resultData: {
lastNodeExecuted: 'B',
runData: {
A: [
createTestTaskData({
startTime: Date.parse('2025-04-20T00:00:01.000Z'),
data: { [NodeConnectionTypes.Main]: [[{ json: { chatInput: 'test input' } }]] },
}),
],
B: [
createTestTaskData({
startTime: Date.parse('2025-04-20T00:00:02.000Z'),
executionTime: 999,
data: { [NodeConnectionTypes.Main]: [[{ json: { output: 'test output' } }]] },
}),
],
},
},
},
finished: true,
mode: 'manual',
status: 'success',
startedAt: '2025-04-20T00:00:00.000Z',
createdAt: '2025-04-20T00:00:00.000Z',
}),
).toEqual([
{ id: expect.any(String), sender: 'user', text: 'test input' },
{ id: 'test-exec-id', sender: 'bot', text: 'test output' },
]);
});
});