mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): Changes to workflow after execution should not affect logs (#14703)
This commit is contained in:
@@ -2,13 +2,16 @@ import {
|
||||
createTestLogEntry,
|
||||
createTestNode,
|
||||
createTestTaskData,
|
||||
createTestWorkflow,
|
||||
createTestWorkflowExecutionResponse,
|
||||
createTestWorkflowObject,
|
||||
} from '@/__tests__/mocks';
|
||||
import {
|
||||
createAiData,
|
||||
findLogEntryToAutoSelect,
|
||||
getTreeNodeData,
|
||||
createLogEntries,
|
||||
findSelectedLogEntry,
|
||||
getTreeNodeData,
|
||||
getTreeNodeDataV2,
|
||||
} from '@/components/RunDataAi/utils';
|
||||
import {
|
||||
AGENT_LANGCHAIN_NODE_TYPE,
|
||||
@@ -16,6 +19,8 @@ import {
|
||||
type ITaskData,
|
||||
NodeConnectionTypes,
|
||||
} from 'n8n-workflow';
|
||||
import { type LogEntrySelection } from '../CanvasChat/types/logs';
|
||||
import { type IExecutionResponse } from '@/Interface';
|
||||
|
||||
describe(getTreeNodeData, () => {
|
||||
it('should generate one node per execution', () => {
|
||||
@@ -184,140 +189,320 @@ describe(getTreeNodeData, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(findLogEntryToAutoSelect, () => {
|
||||
it('should return undefined if no log entry is provided', () => {
|
||||
expect(
|
||||
findLogEntryToAutoSelect(
|
||||
[],
|
||||
{
|
||||
A: createTestNode({ name: 'A' }),
|
||||
B: createTestNode({ name: 'B' }),
|
||||
C: createTestNode({ name: 'C' }),
|
||||
describe(getTreeNodeDataV2, () => {
|
||||
it('should generate one node per execution', () => {
|
||||
const workflow = createTestWorkflowObject({
|
||||
nodes: [
|
||||
createTestNode({ name: 'A' }),
|
||||
createTestNode({ name: 'B' }),
|
||||
createTestNode({ name: 'C' }),
|
||||
],
|
||||
connections: {
|
||||
B: { ai_tool: [[{ node: 'A', type: NodeConnectionTypes.AiTool, index: 0 }]] },
|
||||
C: {
|
||||
ai_languageModel: [[{ node: 'B', type: NodeConnectionTypes.AiLanguageModel, index: 0 }]],
|
||||
},
|
||||
{
|
||||
A: [],
|
||||
B: [],
|
||||
C: [],
|
||||
},
|
||||
),
|
||||
).toBe(undefined);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
it('should return first log entry with error', () => {
|
||||
expect(
|
||||
findLogEntryToAutoSelect(
|
||||
[
|
||||
createTestLogEntry({ node: 'A', runIndex: 0 }),
|
||||
createTestLogEntry({ node: 'B', runIndex: 0 }),
|
||||
createTestLogEntry({ node: 'C', runIndex: 0 }),
|
||||
createTestLogEntry({ node: 'C', runIndex: 1 }),
|
||||
createTestLogEntry({ node: 'C', runIndex: 2 }),
|
||||
],
|
||||
{
|
||||
A: createTestNode({ name: 'A' }),
|
||||
B: createTestNode({ name: 'B' }),
|
||||
C: createTestNode({ name: 'C' }),
|
||||
},
|
||||
{
|
||||
A: [createTestTaskData({ executionStatus: 'success' })],
|
||||
B: [createTestTaskData({ executionStatus: 'success' })],
|
||||
C: [
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ error: {} as ExecutionError, executionStatus: 'error' }),
|
||||
createTestTaskData({ error: {} as ExecutionError, executionStatus: 'error' }),
|
||||
],
|
||||
},
|
||||
),
|
||||
).toEqual(expect.objectContaining({ node: 'C', runIndex: 1 }));
|
||||
});
|
||||
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 } };
|
||||
|
||||
it("should return first log entry with error even if it's on a sub node", () => {
|
||||
expect(
|
||||
findLogEntryToAutoSelect(
|
||||
[
|
||||
createTestLogEntry({ node: 'A', runIndex: 0 }),
|
||||
createTestLogEntry({
|
||||
node: 'B',
|
||||
runIndex: 0,
|
||||
children: [
|
||||
createTestLogEntry({ node: 'C', runIndex: 0 }),
|
||||
createTestLogEntry({ node: 'C', runIndex: 1 }),
|
||||
createTestLogEntry({ node: 'C', runIndex: 2 }),
|
||||
],
|
||||
getTreeNodeDataV2('A', createTestTaskData({}), workflow, {
|
||||
A: [createTestTaskData({ startTime: Date.parse('2025-02-26T00:00:00.000Z') })],
|
||||
B: [
|
||||
createTestTaskData({
|
||||
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||
data: { main: [[{ json: jsonB1 }]] },
|
||||
}),
|
||||
createTestTaskData({
|
||||
startTime: Date.parse('2025-02-26T00:00:03.000Z'),
|
||||
data: { main: [[{ json: jsonB2 }]] },
|
||||
}),
|
||||
],
|
||||
{
|
||||
A: createTestNode({ name: 'A' }),
|
||||
B: createTestNode({ name: 'B' }),
|
||||
C: createTestNode({ name: 'C' }),
|
||||
},
|
||||
{
|
||||
A: [createTestTaskData({ executionStatus: 'success' })],
|
||||
B: [createTestTaskData({ executionStatus: 'success' })],
|
||||
C: [
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ error: {} as ExecutionError, executionStatus: 'error' }),
|
||||
createTestTaskData({ error: {} as ExecutionError, executionStatus: 'error' }),
|
||||
],
|
||||
C: [
|
||||
createTestTaskData({
|
||||
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
|
||||
data: { main: [[{ json: jsonC1 }]] },
|
||||
}),
|
||||
createTestTaskData({ startTime: Date.parse('2025-02-26T00:00:04.000Z') }),
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
depth: 0,
|
||||
id: 'A:0',
|
||||
node: expect.objectContaining({ name: 'A' }),
|
||||
runIndex: 0,
|
||||
runData: expect.objectContaining({ startTime: 0 }),
|
||||
parent: undefined,
|
||||
consumedTokens: {
|
||||
completionTokens: 0,
|
||||
promptTokens: 0,
|
||||
totalTokens: 0,
|
||||
isEstimate: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
depth: 1,
|
||||
id: 'B:0',
|
||||
node: expect.objectContaining({ name: 'B' }),
|
||||
runIndex: 0,
|
||||
runData: expect.objectContaining({
|
||||
startTime: Date.parse('2025-02-26T00:00:01.000Z'),
|
||||
}),
|
||||
parent: expect.objectContaining({ node: expect.objectContaining({ name: 'A' }) }),
|
||||
consumedTokens: {
|
||||
completionTokens: 1,
|
||||
promptTokens: 2,
|
||||
totalTokens: 3,
|
||||
isEstimate: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
children: [],
|
||||
depth: 2,
|
||||
id: 'C:0',
|
||||
node: expect.objectContaining({ name: 'C' }),
|
||||
runIndex: 0,
|
||||
runData: expect.objectContaining({
|
||||
startTime: Date.parse('2025-02-26T00:00:02.000Z'),
|
||||
}),
|
||||
parent: expect.objectContaining({ node: expect.objectContaining({ name: 'B' }) }),
|
||||
consumedTokens: {
|
||||
completionTokens: 7,
|
||||
promptTokens: 8,
|
||||
totalTokens: 9,
|
||||
isEstimate: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
depth: 1,
|
||||
id: 'B:1',
|
||||
node: expect.objectContaining({ name: 'B' }),
|
||||
runIndex: 1,
|
||||
runData: expect.objectContaining({
|
||||
startTime: Date.parse('2025-02-26T00:00:03.000Z'),
|
||||
}),
|
||||
parent: expect.objectContaining({ node: expect.objectContaining({ name: 'A' }) }),
|
||||
consumedTokens: {
|
||||
completionTokens: 4,
|
||||
promptTokens: 5,
|
||||
totalTokens: 6,
|
||||
isEstimate: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
children: [],
|
||||
depth: 2,
|
||||
id: 'C:1',
|
||||
node: expect.objectContaining({ name: 'C' }),
|
||||
runIndex: 1,
|
||||
runData: expect.objectContaining({
|
||||
startTime: Date.parse('2025-02-26T00:00:04.000Z'),
|
||||
}),
|
||||
parent: expect.objectContaining({ node: expect.objectContaining({ name: 'B' }) }),
|
||||
consumedTokens: {
|
||||
completionTokens: 0,
|
||||
promptTokens: 0,
|
||||
totalTokens: 0,
|
||||
isEstimate: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(findSelectedLogEntry, () => {
|
||||
function find(state: LogEntrySelection, response: IExecutionResponse) {
|
||||
return findSelectedLogEntry(state, {
|
||||
...response,
|
||||
tree: createLogEntries(
|
||||
createTestWorkflowObject(response.workflowData),
|
||||
response.data?.resultData.runData ?? {},
|
||||
),
|
||||
).toEqual(expect.objectContaining({ node: 'C', runIndex: 1 }));
|
||||
});
|
||||
}
|
||||
|
||||
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 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return first log entry for AI agent node if there is no log entry with error', () => {
|
||||
expect(
|
||||
findLogEntryToAutoSelect(
|
||||
[
|
||||
createTestLogEntry({ node: 'A', runIndex: 0 }),
|
||||
createTestLogEntry({ node: 'B', runIndex: 0 }),
|
||||
createTestLogEntry({ node: 'C', runIndex: 0 }),
|
||||
createTestLogEntry({ node: 'C', runIndex: 1 }),
|
||||
createTestLogEntry({ node: 'C', runIndex: 2 }),
|
||||
],
|
||||
{
|
||||
A: createTestNode({ name: 'A' }),
|
||||
B: createTestNode({ name: 'B', type: AGENT_LANGCHAIN_NODE_TYPE }),
|
||||
C: createTestNode({ name: 'C' }),
|
||||
describe('when log is manually selected', () => {
|
||||
it('should return manually selected log', () => {
|
||||
const nodeA = createTestNode({ name: 'A' });
|
||||
const response = createTestWorkflowExecutionResponse({
|
||||
workflowData: createTestWorkflow({
|
||||
id: 'test-wf-id',
|
||||
nodes: [nodeA, createTestNode({ name: 'B' })],
|
||||
}),
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
A: [createTestTaskData({ executionStatus: 'success' })],
|
||||
B: [createTestTaskData({ error: {} as ExecutionError, executionStatus: 'error' })],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
A: [createTestTaskData({ executionStatus: 'success' })],
|
||||
B: [createTestTaskData({ executionStatus: 'success' })],
|
||||
C: [
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
],
|
||||
},
|
||||
),
|
||||
).toEqual(expect.objectContaining({ node: 'B', runIndex: 0 }));
|
||||
});
|
||||
});
|
||||
|
||||
it('should return first log entry if there is no log entry with error nor executed AI agent node', () => {
|
||||
expect(
|
||||
findLogEntryToAutoSelect(
|
||||
[
|
||||
createTestLogEntry({ node: 'A', runIndex: 0 }),
|
||||
createTestLogEntry({ node: 'B', runIndex: 0 }),
|
||||
createTestLogEntry({ node: 'C', runIndex: 0 }),
|
||||
createTestLogEntry({ node: 'C', runIndex: 1 }),
|
||||
createTestLogEntry({ node: 'C', runIndex: 2 }),
|
||||
],
|
||||
const result = find(
|
||||
{
|
||||
A: createTestNode({ name: 'A' }),
|
||||
B: createTestNode({ name: 'B' }),
|
||||
C: createTestNode({ name: 'C' }),
|
||||
type: 'selected',
|
||||
workflowId: 'test-wf-id',
|
||||
data: createTestLogEntry({ node: nodeA, runIndex: 0 }),
|
||||
},
|
||||
{
|
||||
A: [createTestTaskData({ executionStatus: 'success' })],
|
||||
B: [createTestTaskData({ executionStatus: 'success' })],
|
||||
C: [
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
createTestTaskData({ executionStatus: 'success' }),
|
||||
],
|
||||
},
|
||||
),
|
||||
).toEqual(expect.objectContaining({ node: 'A', runIndex: 0 }));
|
||||
response,
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({ node: expect.objectContaining({ name: 'A' }), runIndex: 0 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -361,10 +546,10 @@ describe(createLogEntries, () => {
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
expect.objectContaining({ node: 'A', runIndex: 0 }),
|
||||
expect.objectContaining({ node: 'B', runIndex: 0 }),
|
||||
expect.objectContaining({ node: 'C', runIndex: 1 }),
|
||||
expect.objectContaining({ node: 'C', runIndex: 0 }),
|
||||
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 }),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -411,13 +596,13 @@ describe(createLogEntries, () => {
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
expect.objectContaining({ node: 'A', runIndex: 0 }),
|
||||
expect.objectContaining({ node: expect.objectContaining({ name: 'A' }), runIndex: 0 }),
|
||||
expect.objectContaining({
|
||||
node: 'B',
|
||||
node: expect.objectContaining({ name: 'B' }),
|
||||
runIndex: 0,
|
||||
children: [
|
||||
expect.objectContaining({ node: 'C', runIndex: 1 }),
|
||||
expect.objectContaining({ node: 'C', runIndex: 0 }),
|
||||
expect.objectContaining({ node: expect.objectContaining({ name: 'C' }), runIndex: 1 }),
|
||||
expect.objectContaining({ node: expect.objectContaining({ name: 'C' }), runIndex: 0 }),
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user