diff --git a/src/platform/packages/shared/kbn-workflows/graph/build_execution_graph/build_execution_graph.ts b/src/platform/packages/shared/kbn-workflows/graph/build_execution_graph/build_execution_graph.ts index f2816037ac938..348a8aea50f0d 100644 --- a/src/platform/packages/shared/kbn-workflows/graph/build_execution_graph/build_execution_graph.ts +++ b/src/platform/packages/shared/kbn-workflows/graph/build_execution_graph/build_execution_graph.ts @@ -188,7 +188,7 @@ function createIfGraph(ifStep: IfStep, context: GraphBuildContext): graphlib.Gra }; const enterThenBranchNode: EnterConditionBranchNode = { id: `enterThen(${enterConditionNodeId})`, - type: 'enter-condition-branch', + type: 'enter-then-branch', condition: ifElseStep.condition, }; @@ -197,7 +197,7 @@ function createIfGraph(ifStep: IfStep, context: GraphBuildContext): graphlib.Gra const exitThenBranchNode: ExitConditionBranchNode = { id: `exitThen(${enterConditionNodeId})`, - type: 'exit-condition-branch', + type: 'exit-then-branch', startNodeId: enterThenBranchNode.id, }; const thenGraph = createStepsSequence(trueSteps, context); @@ -208,13 +208,13 @@ function createIfGraph(ifStep: IfStep, context: GraphBuildContext): graphlib.Gra if (falseSteps?.length > 0) { const enterElseBranchNode: EnterConditionBranchNode = { id: `enterElse(${enterConditionNodeId})`, - type: 'enter-condition-branch', + type: 'enter-else-branch', }; graph.setNode(enterElseBranchNode.id, enterElseBranchNode); graph.setEdge(enterConditionNodeId, enterElseBranchNode.id); const exitElseBranchNode: ExitConditionBranchNode = { id: `exitElse(${enterConditionNodeId})`, - type: 'exit-condition-branch', + type: 'exit-else-branch', startNodeId: enterElseBranchNode.id, }; const elseGraph = createStepsSequence(falseSteps, context); diff --git a/src/platform/packages/shared/kbn-workflows/graph/build_execution_graph/tests/build_execution_graph.test.ts b/src/platform/packages/shared/kbn-workflows/graph/build_execution_graph/tests/build_execution_graph.test.ts index 78c416380a5e8..590a3c2e9301f 100644 --- a/src/platform/packages/shared/kbn-workflows/graph/build_execution_graph/tests/build_execution_graph.test.ts +++ b/src/platform/packages/shared/kbn-workflows/graph/build_execution_graph/tests/build_execution_graph.test.ts @@ -369,7 +369,7 @@ describe('convertToWorkflowGraph', () => { const enterThenBranchNode = executionGraph.node('enterThen(testIfStep)'); expect(enterThenBranchNode).toEqual({ id: 'enterThen(testIfStep)', - type: 'enter-condition-branch', + type: 'enter-then-branch', condition: 'true', } as EnterConditionBranchNode); }); @@ -379,7 +379,7 @@ describe('convertToWorkflowGraph', () => { const exitThenBranchNode = executionGraph.node('exitThen(testIfStep)'); expect(exitThenBranchNode).toEqual({ id: 'exitThen(testIfStep)', - type: 'exit-condition-branch', + type: 'exit-then-branch', startNodeId: 'enterThen(testIfStep)', } as ExitConditionBranchNode); }); @@ -389,7 +389,7 @@ describe('convertToWorkflowGraph', () => { const enterElseBranchNode = executionGraph.node('enterElse(testIfStep)'); expect(enterElseBranchNode).toEqual({ id: 'enterElse(testIfStep)', - type: 'enter-condition-branch', + type: 'enter-else-branch', condition: undefined, } as EnterConditionBranchNode); }); @@ -399,7 +399,7 @@ describe('convertToWorkflowGraph', () => { const exitElseBranchNode = executionGraph.node('exitElse(testIfStep)'); expect(exitElseBranchNode).toEqual({ id: 'exitElse(testIfStep)', - type: 'exit-condition-branch', + type: 'exit-else-branch', startNodeId: 'enterElse(testIfStep)', } as ExitConditionBranchNode); }); @@ -537,7 +537,7 @@ describe('convertToWorkflowGraph', () => { const enterThenBranchNode = executionGraph.node('enterThen(if_firstThenTestConnectorStep)'); expect(enterThenBranchNode).toEqual({ id: 'enterThen(if_firstThenTestConnectorStep)', - type: 'enter-condition-branch', + type: 'enter-then-branch', condition: 'false', } as EnterConditionBranchNode); }); @@ -555,7 +555,7 @@ describe('convertToWorkflowGraph', () => { const exitThenBranchNode = executionGraph.node('exitThen(if_firstThenTestConnectorStep)'); expect(exitThenBranchNode).toEqual({ id: 'exitThen(if_firstThenTestConnectorStep)', - type: 'exit-condition-branch', + type: 'exit-then-branch', startNodeId: 'enterThen(if_firstThenTestConnectorStep)', } as ExitConditionBranchNode); }); diff --git a/src/platform/packages/shared/kbn-workflows/types/execution/nodes/branching_nodes.ts b/src/platform/packages/shared/kbn-workflows/types/execution/nodes/branching_nodes.ts index 8672607b74c92..d3ca5de72de6d 100644 --- a/src/platform/packages/shared/kbn-workflows/types/execution/nodes/branching_nodes.ts +++ b/src/platform/packages/shared/kbn-workflows/types/execution/nodes/branching_nodes.ts @@ -23,14 +23,14 @@ export type EnterIfNode = z.infer; export const EnterConditionBranchNodeSchema = z.object({ id: z.string(), - type: z.literal('enter-condition-branch'), + type: z.union([z.literal('enter-then-branch'), z.literal('enter-else-branch')]), condition: z.union([z.string(), z.undefined()]), }); export type EnterConditionBranchNode = z.infer; export const ExitConditionBranchNodeSchema = z.object({ id: z.string(), - type: z.literal('exit-condition-branch'), + type: z.union([z.literal('exit-then-branch'), z.literal('exit-else-branch')]), startNodeId: z.string(), }); export type ExitConditionBranchNode = z.infer; diff --git a/src/platform/packages/shared/kbn-workflows/types/v1.ts b/src/platform/packages/shared/kbn-workflows/types/v1.ts index 428ac8592fa9b..11795573e56a5 100644 --- a/src/platform/packages/shared/kbn-workflows/types/v1.ts +++ b/src/platform/packages/shared/kbn-workflows/types/v1.ts @@ -66,6 +66,9 @@ export interface EsWorkflowStepExecution { spaceId: string; id: string; stepId: string; + + /** Current step's scope path */ + path: string[]; workflowRunId: string; workflowId: string; status: ExecutionStatus; diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/enter_foreach_node_impl.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/enter_foreach_node_impl.ts index fee4f6786a715..aa1e502908b1c 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/enter_foreach_node_impl.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/enter_foreach_node_impl.ts @@ -22,63 +22,78 @@ export class EnterForeachNodeImpl implements StepImplementation, StepErrorCatche ) {} public async run(): Promise { - this.wfExecutionRuntimeManager.enterScope(); - let foreachState = this.wfExecutionRuntimeManager.getStepState(this.step.id); + if (!this.wfExecutionRuntimeManager.getStepState(this.step.id)) { + await this.enterForeach(); + } else { + await this.advanceIteration(); + } + } - if (!foreachState) { - await this.wfExecutionRuntimeManager.startStep(this.step.id); - const evaluatedItems = this.getItems(); + async catchError(): Promise { + await this.wfExecutionRuntimeManager.setStepState(this.step.id, undefined); + } - if (evaluatedItems.length === 0) { - this.workflowLogger.logDebug( - `Foreach step "${this.step.id}" has no items to iterate over. Skipping execution.`, - { - workflow: { step_id: this.step.id }, - } - ); - await this.wfExecutionRuntimeManager.setStepState(this.step.id, { - items: [], - total: 0, - }); - await this.wfExecutionRuntimeManager.finishStep(this.step.id); - this.wfExecutionRuntimeManager.goToStep(this.step.exitNodeId); - return; - } + private async enterForeach(): Promise { + let foreachState = this.wfExecutionRuntimeManager.getStepState(this.step.id); + await this.wfExecutionRuntimeManager.startStep(this.step.id); + const evaluatedItems = this.getItems(); + if (evaluatedItems.length === 0) { this.workflowLogger.logDebug( - `Foreach step "${this.step.id}" will iterate over ${evaluatedItems.length} items.`, + `Foreach step "${this.step.id}" has no items to iterate over. Skipping execution.`, { workflow: { step_id: this.step.id }, } ); - - // Initialize foreach state - foreachState = { - items: evaluatedItems, - item: evaluatedItems[0], - index: 0, - total: evaluatedItems.length, - }; - } else { - // Update items and index if they have changed - const items = foreachState.items; - const index = foreachState.index + 1; - const item = items[index]; - const total = foreachState.total; - foreachState = { - items, - index, - item, - total, - }; + await this.wfExecutionRuntimeManager.setStepState(this.step.id, { + items: [], + total: 0, + }); + await this.wfExecutionRuntimeManager.finishStep(this.step.id); + this.wfExecutionRuntimeManager.goToStep(this.step.exitNodeId); + return; } + this.workflowLogger.logDebug( + `Foreach step "${this.step.id}" will iterate over ${evaluatedItems.length} items.`, + { + workflow: { step_id: this.step.id }, + } + ); + + // Initialize foreach state + foreachState = { + items: evaluatedItems, + item: evaluatedItems[0], + index: 0, + total: evaluatedItems.length, + }; + // Enter a new scope for the whole foreach + this.wfExecutionRuntimeManager.enterScope(); + + // Enter a new scope for the first iteration + this.wfExecutionRuntimeManager.enterScope(foreachState.index!.toString()); await this.wfExecutionRuntimeManager.setStepState(this.step.id, foreachState); this.wfExecutionRuntimeManager.goToNextStep(); } - async catchError(): Promise { - await this.wfExecutionRuntimeManager.setStepState(this.step.id, undefined); + private async advanceIteration(): Promise { + let foreachState = this.wfExecutionRuntimeManager.getStepState(this.step.id)!; + // Update items and index if they have changed + const items = foreachState.items; + const index = foreachState.index + 1; + const item = items[index]; + const total = foreachState.total; + foreachState = { + items, + index, + item, + total, + }; + // Enter a new scope for the new iteration + this.wfExecutionRuntimeManager.enterScope(foreachState.index!.toString()); + await this.wfExecutionRuntimeManager.setStepState(this.step.id, foreachState); + this.wfExecutionRuntimeManager.goToNextStep(); } private getItems(): any[] { diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/exit_foreach_node_impl.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/exit_foreach_node_impl.ts index 8f7ddae9c9f71..9409248cd6ddf 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/exit_foreach_node_impl.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/exit_foreach_node_impl.ts @@ -20,18 +20,20 @@ export class ExitForeachNodeImpl implements StepImplementation { ) {} public async run(): Promise { - this.wfExecutionRuntimeManager.exitScope(); const foreachState = this.wfExecutionRuntimeManager.getStepState(this.step.startNodeId); if (!foreachState) { throw new Error(`Foreach state for step ${this.step.startNodeId} not found`); } + // Exit the scope of the current iteration + this.wfExecutionRuntimeManager.exitScope(); if (foreachState.items[foreachState.index + 1]) { this.wfExecutionRuntimeManager.goToStep(this.step.startNodeId); return; } - + // All items have been processed, exit the foreach scope + this.wfExecutionRuntimeManager.exitScope(); await this.wfExecutionRuntimeManager.setStepState(this.step.startNodeId, undefined); await this.wfExecutionRuntimeManager.finishStep(this.step.startNodeId); this.workflowLogger.logDebug( diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/tests/enter_foreach_node_impl.test.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/tests/enter_foreach_node_impl.test.ts index 86feef3543efb..e0ce893c31c50 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/tests/enter_foreach_node_impl.test.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/tests/enter_foreach_node_impl.test.ts @@ -69,10 +69,28 @@ describe('EnterForeachNodeImpl', () => { getStepState.mockReturnValue(undefined); }); - it('should enter scope', async () => { + it('should enter the whole foreach scope', async () => { await underTest.run(); - expect(enterScope).toHaveBeenCalledTimes(1); + expect(enterScope).toHaveBeenCalledWith(); + }); + + it('should enter the iteration scope', async () => { + await underTest.run(); + + expect(enterScope).toHaveBeenCalledWith('0'); + }); + + it('should enter scopes in correct order', async () => { + await underTest.run(); + expect(enterScope).toHaveBeenNthCalledWith(1); + expect(enterScope).toHaveBeenNthCalledWith(2, '0'); + }); + + it('should enter scope twice', async () => { + await underTest.run(); + + expect(enterScope).toHaveBeenCalledTimes(2); }); it('should start step', async () => { @@ -208,6 +226,18 @@ describe('EnterForeachNodeImpl', () => { }); }); + it('should enter iteration scope', async () => { + await underTest.run(); + + expect(enterScope).toHaveBeenCalledWith('1'); + }); + + it('should enter scope only once', async () => { + await underTest.run(); + + expect(enterScope).toHaveBeenCalledTimes(1); + }); + it('should not start step', async () => { await underTest.run(); diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/tests/exit_foreach_node_impl.test.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/tests/exit_foreach_node_impl.test.ts index 3907ec76e4f5a..46b572dd5c118 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/tests/exit_foreach_node_impl.test.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/foreach_step/tests/exit_foreach_node_impl.test.ts @@ -91,7 +91,7 @@ describe('ExitForeachNodeImpl', () => { expect(wfExecutionRuntimeManager.setStepResult).not.toHaveBeenCalled(); }); - it('should exit scope', async () => { + it('should exit iteration scope', async () => { await underTest.run(); expect(exitScope).toHaveBeenCalledTimes(1); }); @@ -137,9 +137,9 @@ describe('ExitForeachNodeImpl', () => { ); }); - it('should exit scope', async () => { + it('should exit iteration scope and whole foreach scope', async () => { await underTest.run(); - expect(exitScope).toHaveBeenCalledTimes(1); + expect(exitScope).toHaveBeenCalledTimes(2); }); }); }); diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/enter_condition_branch_node_impl.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/enter_condition_branch_node_impl.ts index a3202c8e25fff..e604793215d90 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/enter_condition_branch_node_impl.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/enter_condition_branch_node_impl.ts @@ -7,13 +7,22 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { EnterConditionBranchNode } from '@kbn/workflows'; import type { StepImplementation } from '../step_base'; import type { WorkflowExecutionRuntimeManager } from '../../workflow_context_manager/workflow_execution_runtime_manager'; export class EnterConditionBranchNodeImpl implements StepImplementation { - constructor(private wfExecutionRuntimeManager: WorkflowExecutionRuntimeManager) {} + constructor( + private node: EnterConditionBranchNode, + private wfExecutionRuntimeManager: WorkflowExecutionRuntimeManager + ) {} public async run(): Promise { + if (this.node.type === 'enter-then-branch') { + this.wfExecutionRuntimeManager.enterScope('true'); + } else { + this.wfExecutionRuntimeManager.enterScope('false'); + } this.wfExecutionRuntimeManager.goToNextStep(); } } diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/enter_if_node_impl.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/enter_if_node_impl.ts index 092e4a227ad49..1d59ee6afa8cd 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/enter_if_node_impl.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/enter_if_node_impl.ts @@ -24,15 +24,17 @@ export class EnterIfNodeImpl implements StepImplementation { ) {} public async run(): Promise { - this.wfExecutionRuntimeManager.enterScope(); await this.wfExecutionRuntimeManager.startStep(this.step.id); + this.wfExecutionRuntimeManager.enterScope(); const successors: any[] = this.wfExecutionRuntimeManager.getNodeSuccessors(this.step.id); - if (successors.some((node) => node.type !== 'enter-condition-branch')) { + if ( + successors.some((node) => !['enter-then-branch', 'enter-else-branch'].includes(node.type)) + ) { throw new Error( `EnterIfNode with id ${ this.step.id - } must have only 'enter-condition-branch' successors, but found: ${successors + } must have only 'enter-then-branch' or 'enter-else-branch' successors, but found: ${successors .map((node) => node.type) .join(', ')}.` ); diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/exit_condition_branch_node_impl.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/exit_condition_branch_node_impl.ts index d60cb1c43ea4b..5ecc3368a4aab 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/exit_condition_branch_node_impl.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/exit_condition_branch_node_impl.ts @@ -35,5 +35,6 @@ export class ExitConditionBranchNodeImpl implements StepImplementation { // After the branch finishes, we go to the end of If condition const exitIfNode = successors[0]; this.wfExecutionRuntimeManager.goToStep(exitIfNode.id); + this.wfExecutionRuntimeManager.exitScope(); } } diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/tests/enter_condition_branch_node_impl.test.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/tests/enter_condition_branch_node_impl.test.ts index 49cb58238b579..8a2d55ab86e50 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/tests/enter_condition_branch_node_impl.test.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/tests/enter_condition_branch_node_impl.test.ts @@ -13,16 +13,35 @@ import type { WorkflowExecutionRuntimeManager } from '../../../workflow_context_ describe('EnterConditionBranchNodeImpl', () => { let wfExecutionRuntimeManagerMock: WorkflowExecutionRuntimeManager; let impl: EnterConditionBranchNodeImpl; + const conditionBranchNode = { + id: 'testStep', + type: 'enter-then-branch', + } as any; beforeEach(() => { wfExecutionRuntimeManagerMock = { goToNextStep: jest.fn(), + enterScope: jest.fn(), } as any; - impl = new EnterConditionBranchNodeImpl(wfExecutionRuntimeManagerMock); + impl = new EnterConditionBranchNodeImpl(conditionBranchNode, wfExecutionRuntimeManagerMock); }); it('should go to next step', async () => { await impl.run(); expect(wfExecutionRuntimeManagerMock.goToNextStep).toHaveBeenCalledTimes(1); }); + + it('should enter true scope for enter-then-branch', async () => { + conditionBranchNode.type = 'enter-then-branch'; + await impl.run(); + expect(wfExecutionRuntimeManagerMock.enterScope).toHaveBeenCalledWith('true'); + expect(wfExecutionRuntimeManagerMock.enterScope).toHaveBeenCalledTimes(1); + }); + + it('should enter false scope for enter-else-branch', async () => { + conditionBranchNode.type = 'enter-else-branch'; + await impl.run(); + expect(wfExecutionRuntimeManagerMock.enterScope).toHaveBeenCalledWith('false'); + expect(wfExecutionRuntimeManagerMock.enterScope).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/tests/enter_if_node_impl.test.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/tests/enter_if_node_impl.test.ts index 526117a5b64ea..29ea5507537a9 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/tests/enter_if_node_impl.test.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/tests/enter_if_node_impl.test.ts @@ -59,12 +59,12 @@ describe('EnterIfNodeImpl', () => { getNodeSuccessors.mockReturnValue([ { id: 'thenNode', - type: 'enter-condition-branch', + type: 'enter-then-branch', condition: 'true', } as EnterConditionBranchNode, { id: 'elseNode', - type: 'enter-condition-branch', + type: 'enter-else-branch', } as EnterConditionBranchNode, ]); }); @@ -76,20 +76,30 @@ describe('EnterIfNodeImpl', () => { it('should enter scope', async () => { await impl.run(); + expect(wfExecutionRuntimeManagerMock.enterScope).toHaveBeenCalledWith(); expect(wfExecutionRuntimeManagerMock.enterScope).toHaveBeenCalledTimes(1); }); + it('should be called after startStep', async () => { + await impl.run(); + expect(startStep).toHaveBeenCalled(); + expect(enterScope).toHaveBeenCalled(); + expect(startStep.mock.invocationCallOrder[0]).toBeLessThan( + enterScope.mock.invocationCallOrder[0] + ); + }); + describe('then branch', () => { beforeEach(() => { getNodeSuccessors.mockReturnValueOnce([ { id: 'thenNode', - type: 'enter-condition-branch', + type: 'enter-then-branch', condition: 'event.type:alert', } as EnterConditionBranchNode, { id: 'elseNode', - type: 'enter-condition-branch', + type: 'enter-else-branch', } as EnterConditionBranchNode, ]); }); @@ -112,12 +122,12 @@ describe('EnterIfNodeImpl', () => { getNodeSuccessors.mockReturnValueOnce([ { id: 'thenNode', - type: 'enter-condition-branch', + type: 'enter-then-branch', condition: 'event.type:rule', } as EnterConditionBranchNode, { id: 'elseNode', - type: 'enter-condition-branch', + type: 'enter-else-branch', } as EnterConditionBranchNode, ]); }); @@ -140,7 +150,7 @@ describe('EnterIfNodeImpl', () => { getNodeSuccessors.mockReturnValueOnce([ { id: 'thenNode', - type: 'enter-condition-branch', + type: 'enter-then-branch', condition: 'event.type:rule', } as EnterConditionBranchNode, ]); @@ -160,10 +170,10 @@ describe('EnterIfNodeImpl', () => { }); }); - it('should throw an error if successors are not enter-condition-branch', async () => { + it('should throw an error if successors are not enter-then-branch or enter-else-branch', async () => { getNodeSuccessors.mockReturnValueOnce([{ id: 'someOtherNode', type: 'some-other-type' }]); await expect(impl.run()).rejects.toThrow( - `EnterIfNode with id ${step.id} must have only 'enter-condition-branch' successors, but found: some-other-type.` + `EnterIfNode with id ${step.id} must have only 'enter-then-branch' or 'enter-else-branch' successors, but found: some-other-type.` ); }); @@ -171,7 +181,7 @@ describe('EnterIfNodeImpl', () => { getNodeSuccessors.mockReturnValueOnce([ { id: 'thenNode', - type: 'enter-condition-branch', + type: 'enter-then-branch', condition: 'invalid""condition', } as EnterConditionBranchNode, ]); diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/tests/exit_condition_branch_node_impl.test.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/tests/exit_condition_branch_node_impl.test.ts index 29f33921d5357..a52490e8d2a9b 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/tests/exit_condition_branch_node_impl.test.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/if_step/tests/exit_condition_branch_node_impl.test.ts @@ -23,12 +23,13 @@ describe('ExitConditionBranchNodeImpl', () => { getNodeSuccessors = jest.fn(); step = { id: 'testStep', - type: 'exit-condition-branch', + type: 'exit-then-branch', startNodeId: 'startBranchNode', }; wfExecutionRuntimeManagerMock = { goToStep, getNodeSuccessors, + exitScope: jest.fn(), } as any; impl = new ExitConditionBranchNodeImpl(step, wfExecutionRuntimeManagerMock); @@ -70,4 +71,10 @@ describe('ExitConditionBranchNodeImpl', () => { await impl.run(); expect(wfExecutionRuntimeManagerMock.goToStep).toHaveBeenCalledWith('exitIfNode'); }); + + it('should exit scope after running', async () => { + await impl.run(); + expect(wfExecutionRuntimeManagerMock.exitScope).toHaveBeenCalled(); + expect(wfExecutionRuntimeManagerMock.exitScope).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/on_failure/retry_step/enter_retry_node_impl.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/on_failure/retry_step/enter_retry_node_impl.ts index 5ed354d4c483a..17de770f14e6e 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/on_failure/retry_step/enter_retry_node_impl.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/on_failure/retry_step/enter_retry_node_impl.ts @@ -25,8 +25,6 @@ export class EnterRetryNodeImpl implements StepImplementation, StepErrorCatcher ) {} public async run(): Promise { - this.workflowRuntime.enterScope(); - if (!this.workflowRuntime.getStepState(this.node.id)) { // If retry state exists, it means we are re-entering the retry step await this.initializeRetry(); @@ -63,6 +61,10 @@ export class EnterRetryNodeImpl implements StepImplementation, StepErrorCatcher private async initializeRetry(): Promise { await this.workflowRuntime.startStep(this.node.id); + // Enter whole retry step scope + this.workflowRuntime.enterScope(); + // Enter first attempt scope. Since attempt is 0 based, we add 1 to it. + this.workflowRuntime.enterScope('1-attempt'); await this.workflowRuntime.setStepState(this.node.id, { attempt: 0, }); @@ -74,6 +76,8 @@ export class EnterRetryNodeImpl implements StepImplementation, StepErrorCatcher const attempt = retryState.attempt + 1; this.workflowLogger.logDebug(`Retrying "${this.node.id}" step. (attempt ${attempt}).`); await this.workflowRuntime.setStepState(this.node.id, { attempt }); + // Enter a new scope for the new attempt. Since attempt is 0 based, we add 1 to it. + this.workflowRuntime.enterScope(`${attempt + 1}-attempt`); this.workflowRuntime.goToNextStep(); } diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/on_failure/retry_step/tests/enter_retry_node_impl.test.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/on_failure/retry_step/tests/enter_retry_node_impl.test.ts index c28f42b234ced..fe99c3f033eef 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/on_failure/retry_step/tests/enter_retry_node_impl.test.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/on_failure/retry_step/tests/enter_retry_node_impl.test.ts @@ -52,9 +52,25 @@ describe('EnterRetryNodeImpl', () => { workflowRuntime.goToNextStep = jest.fn(); }); - it('should enter scope', async () => { + it('should enter whole retry step scope', async () => { await underTest.run(); - expect(workflowRuntime.enterScope).toHaveBeenCalled(); + expect(workflowRuntime.enterScope).toHaveBeenCalledWith(); + }); + + it('should enter first attempt scope', async () => { + await underTest.run(); + expect(workflowRuntime.enterScope).toHaveBeenCalledWith('1-attempt'); + }); + + it('should enter scopes in correct order', async () => { + await underTest.run(); + expect(workflowRuntime.enterScope).toHaveBeenNthCalledWith(1); + expect(workflowRuntime.enterScope).toHaveBeenNthCalledWith(2, '1-attempt'); + }); + + it('should enter two scopes', async () => { + await underTest.run(); + expect(workflowRuntime.enterScope).toHaveBeenCalledTimes(2); }); it('should start step', async () => { @@ -81,9 +97,10 @@ describe('EnterRetryNodeImpl', () => { workflowRuntime.goToNextStep = jest.fn(); }); - it('should enter scope', async () => { + it('should enter next attempt scope', async () => { await underTest.run(); - expect(workflowRuntime.enterScope).toHaveBeenCalled(); + expect(workflowRuntime.enterScope).toHaveBeenCalledWith('3-attempt'); + expect(workflowRuntime.enterScope).toHaveBeenCalledTimes(1); }); it('should increment attempt in step state', async () => { diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/step/step_factory.ts b/src/platform/plugins/shared/workflows_execution_engine/server/step/step_factory.ts index f20374bccee30..be49fc731b96b 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/step/step_factory.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/step/step_factory.ts @@ -98,9 +98,11 @@ export class StepFactory { this.contextManager, this.workflowLogger ); - case 'enter-condition-branch': - return new EnterConditionBranchNodeImpl(this.workflowRuntime); - case 'exit-condition-branch': + case 'enter-then-branch': + case 'enter-else-branch': + return new EnterConditionBranchNodeImpl(step as any, this.workflowRuntime); + case 'exit-then-branch': + case 'exit-else-branch': return new ExitConditionBranchNodeImpl(step as any, this.workflowRuntime); case 'exit-if': return new ExitIfNodeImpl(step as any, this.workflowRuntime); diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/tests/workflow_execution_runtime_manager.test.ts b/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/tests/workflow_execution_runtime_manager.test.ts index d499162fbb30c..0dd013b0b86c2 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/tests/workflow_execution_runtime_manager.test.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/tests/workflow_execution_runtime_manager.test.ts @@ -279,6 +279,18 @@ describe('WorkflowExecutionRuntimeManager', () => { workflow: { step_id: 'node3' }, }); }); + + it('should save step path from the workflow execution stack', async () => { + workflowExecutionState.getWorkflowExecution = jest.fn().mockReturnValue({ + stack: ['scope1', 'scope2', 'node3'], + }); + await underTest.startStep('node3'); + expect(workflowExecutionState.upsertStep).toHaveBeenCalledWith( + expect.objectContaining({ + path: ['scope1', 'scope2', 'node3'], + }) + ); + }); }); describe('finishStep', () => { @@ -518,4 +530,52 @@ describe('WorkflowExecutionRuntimeManager', () => { }); }); }); + + describe('enterScope', () => { + beforeEach(() => { + underTest.goToStep('node1'); + }); + + it('should enter a new scope when no name is provided', () => { + (workflowExecutionState.getWorkflowExecution as jest.Mock).mockReturnValue({ + stack: ['some-scope'], + } as Partial); + underTest.enterScope(); + expect(workflowExecutionState.updateWorkflowExecution).toHaveBeenCalledWith( + expect.objectContaining({ + stack: ['some-scope', 'node1'], + }) + ); + }); + + it('should enter a new scope with the provided name', () => { + (workflowExecutionState.getWorkflowExecution as jest.Mock).mockReturnValue({ + stack: ['some-scope'], + } as Partial); + underTest.enterScope('my-scope'); + expect(workflowExecutionState.updateWorkflowExecution).toHaveBeenCalledWith( + expect.objectContaining({ + stack: ['some-scope', 'my-scope'], + }) + ); + }); + }); + + describe('exitScope', () => { + beforeEach(() => { + underTest.goToStep('node1'); + }); + + it('should pop the last element', () => { + (workflowExecutionState.getWorkflowExecution as jest.Mock).mockReturnValue({ + stack: ['scope1', 'scope2'], + } as Partial); + underTest.exitScope(); + expect(workflowExecutionState.updateWorkflowExecution).toHaveBeenCalledWith( + expect.objectContaining({ + stack: ['scope1'], + }) + ); + }); + }); }); diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/workflow_execution_runtime_manager.ts b/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/workflow_execution_runtime_manager.ts index 733fc955f4b67..5c1db72cfcfa5 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/workflow_execution_runtime_manager.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/workflow_execution_runtime_manager.ts @@ -126,10 +126,13 @@ export class WorkflowExecutionRuntimeManager { this.currentStepIndex = -1; } - public enterScope(): void { - const currentStep = this.getCurrentStep(); + public enterScope(scopeId?: string): void { + if (!scopeId) { + scopeId = this.getCurrentStep().id; + } + const stack = [...this.workflowExecutionState.getWorkflowExecution().stack]; - stack.push(currentStep.id); + stack.push(scopeId as string); this.workflowExecutionState.updateWorkflowExecution({ stack, }); @@ -222,6 +225,7 @@ export class WorkflowExecutionRuntimeManager { const stepExecution = { stepId: nodeId, + path: [...(workflowExecution.stack || [])], topologicalIndex: this.topologicalOrder.findIndex((id) => id === stepId), status: ExecutionStatus.RUNNING, startedAt: stepStartedAt.toISOString(), diff --git a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_step_execution_list.stories.tsx b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_step_execution_list.stories.tsx index ed88087a8bf28..0fdc8f908ce04 100644 --- a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_step_execution_list.stories.tsx +++ b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_step_execution_list.stories.tsx @@ -41,6 +41,7 @@ export const Default: StoryObj = { topologicalIndex: 1, executionIndex: 0, spaceId: 'default', + path: [], }, { id: '1', @@ -52,6 +53,7 @@ export const Default: StoryObj = { topologicalIndex: 2, executionIndex: 0, spaceId: 'default', + path: [], }, { id: '2', @@ -63,6 +65,7 @@ export const Default: StoryObj = { topologicalIndex: 3, executionIndex: 0, spaceId: 'default', + path: [], }, { id: '4', @@ -74,6 +77,7 @@ export const Default: StoryObj = { topologicalIndex: 4, executionIndex: 0, spaceId: 'default', + path: [], }, { id: '5', @@ -85,6 +89,7 @@ export const Default: StoryObj = { topologicalIndex: 5, executionIndex: 0, spaceId: 'default', + path: [], }, { id: '6', @@ -96,6 +101,7 @@ export const Default: StoryObj = { topologicalIndex: 6, executionIndex: 0, spaceId: 'default', + path: [], }, { id: '7', @@ -107,6 +113,7 @@ export const Default: StoryObj = { topologicalIndex: 7, executionIndex: 0, spaceId: 'default', + path: [], }, ], }, diff --git a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_step_execution_list_item.stories.tsx b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_step_execution_list_item.stories.tsx index 448a2510947e3..10c2b39ca63a7 100644 --- a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_step_execution_list_item.stories.tsx +++ b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_step_execution_list_item.stories.tsx @@ -35,6 +35,7 @@ export const Default: Story = { topologicalIndex: 1, executionIndex: 0, spaceId: 'default', + path: [], }, }, }; @@ -52,6 +53,7 @@ export const Running: Story = { topologicalIndex: 1, executionIndex: 0, spaceId: 'default', + path: [], }, }, }; @@ -69,6 +71,7 @@ export const Failed: Story = { topologicalIndex: 1, executionIndex: 0, spaceId: 'default', + path: [], }, }, }; diff --git a/src/platform/plugins/shared/workflows_management/public/widgets/workflow_yaml_editor/ui/workflow_yaml_editor.stories.tsx b/src/platform/plugins/shared/workflows_management/public/widgets/workflow_yaml_editor/ui/workflow_yaml_editor.stories.tsx index 6a5a936c7d103..b1f31d9e2d10a 100644 --- a/src/platform/plugins/shared/workflows_management/public/widgets/workflow_yaml_editor/ui/workflow_yaml_editor.stories.tsx +++ b/src/platform/plugins/shared/workflows_management/public/widgets/workflow_yaml_editor/ui/workflow_yaml_editor.stories.tsx @@ -128,6 +128,7 @@ export const WithStepExecutions: Story = { startedAt: new Date().toISOString(), topologicalIndex: 0, executionIndex: 0, + path: [], }, { stepId: 'debug_ai_response', @@ -139,6 +140,7 @@ export const WithStepExecutions: Story = { startedAt: new Date().toISOString(), topologicalIndex: 0, executionIndex: 0, + path: [], }, { stepId: 'print-enter-dash', @@ -150,6 +152,7 @@ export const WithStepExecutions: Story = { startedAt: new Date().toISOString(), topologicalIndex: 0, executionIndex: 0, + path: [], }, { stepId: 'foreachstep', @@ -161,6 +164,7 @@ export const WithStepExecutions: Story = { startedAt: new Date().toISOString(), topologicalIndex: 0, executionIndex: 0, + path: [], }, { stepId: 'log-name-surname', @@ -172,6 +176,7 @@ export const WithStepExecutions: Story = { startedAt: new Date().toISOString(), topologicalIndex: 0, executionIndex: 0, + path: [], }, { stepId: 'slack_it', @@ -183,6 +188,7 @@ export const WithStepExecutions: Story = { startedAt: new Date().toISOString(), topologicalIndex: 0, executionIndex: 0, + path: [], }, { stepId: 'print-exit-dash', @@ -194,6 +200,7 @@ export const WithStepExecutions: Story = { startedAt: new Date().toISOString(), topologicalIndex: 0, executionIndex: 0, + path: [], }, ], },