diff --git a/src/main/java/it/cnr/isti/workflow/manager/executions/ExecutionContext.java b/src/main/java/it/cnr/isti/workflow/manager/executions/ExecutionContext.java index 670dd82..994bc5a 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executions/ExecutionContext.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executions/ExecutionContext.java @@ -340,6 +340,11 @@ public class ExecutionContext implements ExecutionListener { loops.forEach(loop -> this.loopsByGuard.put(loop.loop().guardId(), loop)); } + /** The loops this execution may go round, in no particular order. */ + public List getLoops() { + return List.copyOf(this.loopsByGuard.values()); + } + private boolean isInLoop(String stepId) { return this.loopsByGuard.values().stream().anyMatch(loop -> loop.loop().contains(stepId)); } diff --git a/src/main/java/it/cnr/isti/workflow/manager/executions/api/ExecutionContextView.java b/src/main/java/it/cnr/isti/workflow/manager/executions/api/ExecutionContextView.java index 0b9b77c..ae59f8d 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executions/api/ExecutionContextView.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executions/api/ExecutionContextView.java @@ -39,6 +39,8 @@ public class ExecutionContextView { private Map steps; /** Loop steps as each earlier round left them, oldest first; the steps above are the current round. */ private java.util.List stepHistory; + /** The loops the execution may go round, so a page can tell whose rounds those are. */ + private java.util.List loops; private ExecutionStatus status; private java.util.List waitingSteps; @@ -66,6 +68,7 @@ public class ExecutionContextView { (left, right) -> right, java.util.LinkedHashMap::new))) .stepHistory(context.getStepHistory()) + .loops(context.getLoops().stream().map(ExecutionLoopView::of).toList()) .status(context.getStatus()) .waitingSteps(context.getWaitingSteps()) .build(); diff --git a/src/main/java/it/cnr/isti/workflow/manager/executions/api/ExecutionLoopView.java b/src/main/java/it/cnr/isti/workflow/manager/executions/api/ExecutionLoopView.java new file mode 100644 index 0000000..1dc1dd0 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/executions/api/ExecutionLoopView.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025-2026 Lucio Lelii - ISTI-CNR +// SPDX-License-Identifier: AGPL-3.0-or-later +// Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM. + +package it.cnr.isti.workflow.manager.executions.api; + +import java.util.List; + +import it.cnr.isti.workflow.manager.executions.LoopRuntime; + +/** + * A loop of a running flow, as the engine runs it. + * + * @param guardId the step that decides each time whether to go round again + * @param entryId where each new round starts + * @param backOutput the guard's output that goes round; any other one leaves the loop + * @param maxIterations how many rounds are allowed before the execution fails + * @param memberIds every step that runs again on each round + */ +public record ExecutionLoopView(String guardId, String entryId, String backOutput, int maxIterations, + List memberIds) { + + public static ExecutionLoopView of(LoopRuntime runtime) { + return new ExecutionLoopView(runtime.loop().guardId(), runtime.loop().entryId(), runtime.backOutput(), + runtime.loop().maxIterations(), runtime.loop().memberIds().stream().sorted().toList()); + } +} diff --git a/src/test/java/it/cnr/isti/workflow/manager/executions/LoopExecutionTest.java b/src/test/java/it/cnr/isti/workflow/manager/executions/LoopExecutionTest.java index 8a063a4..51814e6 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/executions/LoopExecutionTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/executions/LoopExecutionTest.java @@ -38,6 +38,7 @@ import it.cnr.isti.workflow.manager.blocks.types.ConditionalBlockType; import it.cnr.isti.workflow.manager.blocks.types.EndBlockType; import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType; import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType; +import it.cnr.isti.workflow.manager.executions.api.ExecutionContextView; import it.cnr.isti.workflow.manager.executions.persistence.ExecutionStepSnapshot; import it.cnr.isti.workflow.manager.executions.steps.StepStatus; import it.cnr.isti.workflow.manager.flows.model.Connection; @@ -166,6 +167,15 @@ class LoopExecutionTest { .findFirst().orElseThrow().getValue()) .toList()); + ExecutionContextView view = ExecutionContextView.fromContext(execution.getContext()); + assertEquals(1, view.getLoops().size(), "the page is told which loop those rounds belong to"); + assertEquals(loop.check().getId(), view.getLoops().getFirst().guardId()); + assertEquals(loop.draft().getId(), view.getLoops().getFirst().entryId()); + assertEquals(ConditionalBlockFactory.FALSE_OUTPUT, view.getLoops().getFirst().backOutput()); + assertEquals(10, view.getLoops().getFirst().maxIterations()); + assertEquals(java.util.Set.of(loop.draft().getId(), loop.check().getId()), + java.util.Set.copyOf(view.getLoops().getFirst().memberIds())); + List rounds = execution.getContext().getEvents().stream() .filter(event -> event.getType() == ExecutionEventType.LOOP_ITERATION_STARTED) .toList(); @@ -255,6 +265,8 @@ class LoopExecutionTest { ExecutionObject reloaded = executionsService.getExecution(execution.getId()); assertEquals(3, reloaded.getContext().getSteps().get(draft.getId()).getIteration()); assertEquals(4, reloaded.getContext().getStepHistory().size(), "two rounds of two steps each"); + assertEquals(List.of(review.getId()), reloaded.getContext().getLoops().stream() + .map(runtime -> runtime.loop().guardId()).toList(), "the loop is known again after the reload"); executionsService.resumeExecution(execution.getId()); executionsService.setInteractionValue(execution.getId(), review.getId(),