first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
import { createWorkflow, testDb } from '@n8n/backend-test-utils';
|
||||
import { ExecutionRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { parse, stringify } from 'flatted';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IRun, INode, ITaskData } from 'n8n-workflow';
|
||||
import { createRunExecutionData, WAIT_INDEFINITELY } from 'n8n-workflow';
|
||||
|
||||
import * as WorkflowHelpers from '@/workflow-helpers';
|
||||
import { createExecution } from '@test-integration/db/executions';
|
||||
|
||||
describe('workflow-helpers', () => {
|
||||
let executionRepository: ExecutionRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
await testDb.init();
|
||||
executionRepository = Container.get(ExecutionRepository);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['ExecutionEntity']);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate();
|
||||
});
|
||||
|
||||
describe('updateParentExecutionWithChildResults', () => {
|
||||
test('should update parent nodeExecutionStack with child final output', async () => {
|
||||
// ARRANGE
|
||||
const workflow = await createWorkflow();
|
||||
|
||||
// Create parent execution with Execute Workflow node in nodeExecutionStack
|
||||
const parentData = createRunExecutionData({
|
||||
executionData: {
|
||||
nodeExecutionStack: [
|
||||
{
|
||||
node: mock<INode>({ name: 'Execute Workflow' }),
|
||||
data: { main: [[{ json: { type: 'parent-input' }, pairedItem: { item: 0 } }]] },
|
||||
source: { main: [null] },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const parent = await createExecution(
|
||||
{ status: 'waiting', data: stringify(parentData) },
|
||||
workflow,
|
||||
);
|
||||
|
||||
// Create child results with final output
|
||||
const finalNodeName = 'Final Node';
|
||||
const finalNodeNameTaskData: ITaskData = {
|
||||
startTime: Date.now(),
|
||||
executionTime: 5,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
data: { main: [[{ json: { type: 'child-output' }, pairedItem: { item: 0 } }]] },
|
||||
};
|
||||
const childResults: IRun = {
|
||||
mode: 'manual',
|
||||
startedAt: new Date(),
|
||||
storedAt: 'db',
|
||||
status: 'success',
|
||||
data: createRunExecutionData({
|
||||
resultData: {
|
||||
runData: { [finalNodeName]: [finalNodeNameTaskData] },
|
||||
lastNodeExecuted: finalNodeName,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
// ACT
|
||||
await WorkflowHelpers.updateParentExecutionWithChildResults(
|
||||
executionRepository,
|
||||
parent.id,
|
||||
childResults,
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
const updated = await executionRepository.findOne({
|
||||
where: { id: parent.id },
|
||||
relations: { executionData: true },
|
||||
});
|
||||
|
||||
const updatedData = parse(updated!.executionData.data);
|
||||
expect(updatedData.executionData.nodeExecutionStack[0].data.main[0][0].json.type).toBe(
|
||||
'child-output',
|
||||
);
|
||||
expect(updated!.status).toBe('waiting'); // Status unchanged
|
||||
});
|
||||
|
||||
test('should not update parent if status is not waiting', async () => {
|
||||
// ARRANGE
|
||||
const workflow = await createWorkflow();
|
||||
|
||||
const parentData = createRunExecutionData({
|
||||
executionData: {
|
||||
nodeExecutionStack: [
|
||||
{
|
||||
node: mock<INode>({ name: 'Execute Workflow' }),
|
||||
data: { main: [[{ json: { type: 'parent-input' }, pairedItem: { item: 0 } }]] },
|
||||
source: { main: [null] },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const parent = await createExecution(
|
||||
// Not waiting
|
||||
{ status: 'running', data: stringify(parentData) },
|
||||
workflow,
|
||||
);
|
||||
|
||||
const finalNodeName = 'Final Node';
|
||||
const finalNodeNameTaskData: ITaskData = {
|
||||
startTime: Date.now(),
|
||||
executionTime: 5,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
data: {
|
||||
main: [[{ json: { type: 'child-output' }, pairedItem: { item: 0 } }]],
|
||||
},
|
||||
};
|
||||
const childResults: IRun = {
|
||||
mode: 'manual',
|
||||
startedAt: new Date(),
|
||||
storedAt: 'db',
|
||||
status: 'success',
|
||||
data: createRunExecutionData({
|
||||
resultData: {
|
||||
runData: { [finalNodeName]: [finalNodeNameTaskData] },
|
||||
lastNodeExecuted: finalNodeName,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
// ACT
|
||||
await WorkflowHelpers.updateParentExecutionWithChildResults(
|
||||
executionRepository,
|
||||
parent.id,
|
||||
childResults,
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
const notUpdated = await executionRepository.findOne({
|
||||
where: { id: parent.id },
|
||||
relations: { executionData: true },
|
||||
});
|
||||
|
||||
const notUpdatedData = parse(notUpdated!.executionData.data);
|
||||
expect(notUpdatedData.executionData.nodeExecutionStack[0].data.main[0][0].json.type).toBe(
|
||||
'parent-input',
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle child results with no lastNodeExecuted', async () => {
|
||||
// ARRANGE
|
||||
const workflow = await createWorkflow();
|
||||
|
||||
const parentData = createRunExecutionData({
|
||||
executionData: {
|
||||
nodeExecutionStack: [
|
||||
{
|
||||
node: mock<INode>({ name: 'Execute Workflow' }),
|
||||
data: { main: [[{ json: { type: 'parent-input' }, pairedItem: { item: 0 } }]] },
|
||||
source: { main: [null] },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const parent = await createExecution(
|
||||
{
|
||||
status: 'waiting',
|
||||
data: stringify(parentData),
|
||||
},
|
||||
workflow,
|
||||
);
|
||||
|
||||
// Child results with no lastNodeExecuted
|
||||
const childResults: IRun = {
|
||||
mode: 'manual',
|
||||
startedAt: new Date(),
|
||||
storedAt: 'db',
|
||||
status: 'success',
|
||||
data: createRunExecutionData({ resultData: { runData: {} } }),
|
||||
};
|
||||
|
||||
// ACT
|
||||
await WorkflowHelpers.updateParentExecutionWithChildResults(
|
||||
executionRepository,
|
||||
parent.id,
|
||||
childResults,
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
const notUpdated = await executionRepository.findOne({
|
||||
where: { id: parent.id },
|
||||
relations: { executionData: true },
|
||||
});
|
||||
|
||||
const notUpdatedData = parse(notUpdated!.executionData.data);
|
||||
expect(notUpdatedData.executionData.nodeExecutionStack[0].data.main[0][0].json.type).toBe(
|
||||
'parent-input',
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle parent with empty nodeExecutionStack', async () => {
|
||||
// ARRANGE
|
||||
const workflow = await createWorkflow();
|
||||
|
||||
const parentData = createRunExecutionData({
|
||||
executionData: {
|
||||
nodeExecutionStack: [], // Empty
|
||||
},
|
||||
});
|
||||
|
||||
const parent = await createExecution(
|
||||
{
|
||||
status: 'waiting',
|
||||
waitTill: WAIT_INDEFINITELY,
|
||||
data: stringify(parentData),
|
||||
},
|
||||
workflow,
|
||||
);
|
||||
|
||||
const finalNodeName = 'Final Node';
|
||||
const finalNodeNameTaskData: ITaskData = {
|
||||
startTime: Date.now(),
|
||||
executionTime: 5,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
data: {
|
||||
main: [[{ json: { type: 'child-output' }, pairedItem: { item: 0 } }]],
|
||||
},
|
||||
};
|
||||
const childResults: IRun = {
|
||||
mode: 'manual',
|
||||
startedAt: new Date(),
|
||||
storedAt: 'db',
|
||||
status: 'success',
|
||||
data: createRunExecutionData({
|
||||
resultData: {
|
||||
runData: { [finalNodeName]: [finalNodeNameTaskData] },
|
||||
lastNodeExecuted: finalNodeName,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
// ACT & ASSERT
|
||||
await expect(
|
||||
WorkflowHelpers.updateParentExecutionWithChildResults(
|
||||
executionRepository,
|
||||
parent.id,
|
||||
childResults,
|
||||
),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user