diff --git a/src/main/java/it/cnr/isti/workflow/manager/executions/ExecutionsService.java b/src/main/java/it/cnr/isti/workflow/manager/executions/ExecutionsService.java index 0aef4a5..b753365 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executions/ExecutionsService.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executions/ExecutionsService.java @@ -1060,7 +1060,12 @@ public class ExecutionsService { } } setExecutionVariableDescriptors(child.getId(), parent.getContext().getExecutionVariableDescriptors()); - Map globalInputs = ExecutionRuntimeContextSupport.globalView(parent.getContext().getExecutionVariables()); + // The parent's own global inputs, read from the map that holds them. This used to take a + // "global." view of getExecutionVariables(), which is the *unprefixed* variables map and so + // never matched the prefix: every child was handed null values, sat in CREATED and failed + // the parent with "is not in READY status". The prefixed keys live in the runtime map, + // which is deliberately not exposed - so go to the source instead. + Map globalInputs = new LinkedHashMap<>(parent.getContext().getGlobalInputs()); Map globalDescriptors = new LinkedHashMap<>(); for (IODescriptor required : child.getRequiredGlobalInputs()) { globalDescriptors.put(required.getName(), ExecutionVariableDescriptor.builder() diff --git a/src/test/java/it/cnr/isti/workflow/manager/executions/ExecutionTest.java b/src/test/java/it/cnr/isti/workflow/manager/executions/ExecutionTest.java index d439087..072e626 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/executions/ExecutionTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/executions/ExecutionTest.java @@ -1381,6 +1381,58 @@ public class ExecutionTest { && event.getDetails().get("innerExecutionId") != null)); } + @Test + public void iteratorSubflowInheritsTheGlobalInputsSetOnTheParent() { + // A subflow is its own execution, so the values typed on the parent have to be carried into + // it. When they are not, the child sits in CREATED and the parent fails with "is not in + // READY status", naming a child execution the user never saw and explaining nothing. + Block internalBlock = llmBlockFactory.create(LLMBlockConfiguration.builder() + .name("Internal LLM") + .llmDescriptor(llmBrick) + .prompt("Hello, ${{name}} from ${{global.who}}!") + .build()); + + Container container = iteratorContainerFactory.create(IteratorContainerConfiguration.builder() + .name("Iterator") + .subFlow(FlowData.builder() + .block(internalBlock) + .globalInput(IODescriptor.input("who", IOType.TEXT, false, null)) + .build()) + .iterationInput("name") + .build()); + + ExecutionObject execObject = executionsService.createExecution("Iterator flow", FlowData.builder() + .container(container) + .globalInput(IODescriptor.input("who", IOType.TEXT, false, null)) + .build()); + + // The parent collects what the subflow needs: it is the only place the user can supply it. + assertTrue(execObject.getRequiredGlobalInputs().stream() + .anyMatch(descriptor -> descriptor.getName().equals("who"))); + + executionsService.setGlobalInput(execObject.getId(), "who", "HR"); + executionsService.prepareInput(execObject.getId(), container.getId(), "name", List.of("Alice", "Bob")); + assertEquals(ExecutionStatus.READY, execObject.getContext().getStatus()); + + execObject = executionsService.startExecution(execObject.getId()); + // WAITING is transient here: the parent suspends between iterations while a child runs. + for (int attempt = 0; attempt < 200 && !execObject.getContext().getStatus().isFinalState(); attempt++) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + execObject = executionsService.getExecution(execObject.getId()); + } + + ExecutionObject finished = execObject; + assertEquals(ExecutionStatus.SUCCESS, finished.getContext().getStatus(), + () -> "errors: " + finished.getContext().getErrors()); + assertEquals(List.of("Hello, Alice from HR!", "Hello, Bob from HR!"), + finished.getContext().getResult().values().stream().findFirst().orElseThrow()); + } + @Test public void genericContainerSuspendsForInnerInteractionAndResumesFromChild() { Block innerInteraction = humanInteractiveBlockFactory.create(