refactor: rename ExclusiveMergeBlock to BranchRejoinBlock
"Merge" was misleading: the block never combines values, it only lets exactly one of several mutually-exclusive branches (an ExclusiveOR reconvergence, e.g. after a HumanDecisionBlock split) pass through as the single downstream value; two arriving values is an error, not something to combine. BranchRejoinBlock names what it actually does. There is still no block in the platform that genuinely merges/combines multiple parallel values into one - this rename doesn't add one, it just stops "merge" from implying it exists. Renamed throughout: block/config/factory/executor/activation-policy classes and their tests, the NodeVisualRole.MERGE -> BRANCH_REJOIN visual role and NodeTypeCapabilities.merge() -> branchRejoin() factory method, the six EXCLUSIVE_MERGE_* validation error codes -> BRANCH_REJOIN_*, the bundled flows.json (11 uses across the Jensen flows), and the two docs that reference it. Added a legacy-id fallback in DynamicBlockConfigurationTypeResolver (type id "ExclusiveMergeBlockConfiguration") and BlockTypes (typeName "ExclusiveMergeBlock"), mirroring the existing ChatHumanInteraction -> ChatInteraction precedent - found necessary the hard way: without it, any flow already persisted under the old type id fails to deserialize and brings the whole application down at startup, not just that one flow. Locked this in with LegacyBlockTypeIdTest. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
98cf28bd9f
commit
1fff0d3dc8
|
|
@ -5,7 +5,7 @@ import java.util.List;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.Structural;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiOrder;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiUniqueItemsBy;
|
||||
|
|
@ -22,7 +22,7 @@ import lombok.NonNull;
|
|||
@Getter
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
|
||||
public class ExclusiveMergeBlockConfiguration extends BlockConfiguration<ExclusiveMergeBlockType> {
|
||||
public class BranchRejoinBlockConfiguration extends BlockConfiguration<BranchRejoinBlockType> {
|
||||
|
||||
@UiOrder(30)
|
||||
@Structural
|
||||
|
|
@ -30,7 +30,7 @@ public class ExclusiveMergeBlockConfiguration extends BlockConfiguration<Exclusi
|
|||
@Size(min = 2, max = 10)
|
||||
@UiUniqueItemsBy("name")
|
||||
@JsonProperty(required = true)
|
||||
private List<ExclusiveMergeInput> inputs = List.of();
|
||||
private List<BranchRejoinInput> inputs = List.of();
|
||||
|
||||
@UiOrder(40)
|
||||
@Structural
|
||||
|
|
@ -40,7 +40,7 @@ public class ExclusiveMergeBlockConfiguration extends BlockConfiguration<Exclusi
|
|||
private String outputName = "output";
|
||||
|
||||
@Builder
|
||||
public ExclusiveMergeBlockConfiguration(@NonNull String name, List<ExclusiveMergeInput> inputs,
|
||||
public BranchRejoinBlockConfiguration(@NonNull String name, List<BranchRejoinInput> inputs,
|
||||
String outputName) {
|
||||
super(name);
|
||||
this.inputs = inputs == null ? List.of() : List.copyOf(inputs);
|
||||
|
|
@ -48,14 +48,14 @@ public class ExclusiveMergeBlockConfiguration extends BlockConfiguration<Exclusi
|
|||
}
|
||||
|
||||
@Override
|
||||
public Class<ExclusiveMergeBlockType> getBlockType() {
|
||||
return ExclusiveMergeBlockType.class;
|
||||
public Class<BranchRejoinBlockType> getBlockType() {
|
||||
return BranchRejoinBlockType.class;
|
||||
}
|
||||
|
||||
public static ExclusiveMergeBlockConfiguration empty() {
|
||||
return ExclusiveMergeBlockConfiguration.builder()
|
||||
.name(ExclusiveMergeBlockType.TYPE)
|
||||
.inputs(List.of(new ExclusiveMergeInput("branchA"), new ExclusiveMergeInput("branchB")))
|
||||
public static BranchRejoinBlockConfiguration empty() {
|
||||
return BranchRejoinBlockConfiguration.builder()
|
||||
.name(BranchRejoinBlockType.TYPE)
|
||||
.inputs(List.of(new BranchRejoinInput("branchA"), new BranchRejoinInput("branchB")))
|
||||
.outputName("output")
|
||||
.build();
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ public class ExclusiveMergeBlockConfiguration extends BlockConfiguration<Exclusi
|
|||
.count();
|
||||
long distinct = inputs.stream()
|
||||
.filter(input -> input != null && input.name() != null && !input.name().isBlank())
|
||||
.map(ExclusiveMergeInput::name)
|
||||
.map(BranchRejoinInput::name)
|
||||
.distinct()
|
||||
.count();
|
||||
return valid == inputs.size() && distinct == valid;
|
||||
|
|
@ -6,16 +6,16 @@ import it.cnr.isti.workflow.manager.ios.IOType;
|
|||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record ExclusiveMergeInput(
|
||||
public record BranchRejoinInput(
|
||||
@NotBlank @Size(max = 64) String name,
|
||||
@Size(max = 255) String label,
|
||||
@JsonProperty(required = false) IOType type) {
|
||||
|
||||
public ExclusiveMergeInput(String name) {
|
||||
public BranchRejoinInput(String name) {
|
||||
this(name, name, IOType.ANY);
|
||||
}
|
||||
|
||||
public ExclusiveMergeInput {
|
||||
public BranchRejoinInput {
|
||||
label = label == null || label.isBlank() ? name : label;
|
||||
type = type == null ? IOType.ANY : type;
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ public class DynamicBlockConfigurationTypeResolver extends TypeIdResolverBase {
|
|||
|
||||
private static final String CHAT_INTERACTION_LEGACY_CONFIGURATION_ID = "ChatHumanInteractionBlockConfiguration";
|
||||
private static final String CHAT_INTERACTION_CONFIGURATION_ID = "ChatInteractionBlockConfiguration";
|
||||
private static final String BRANCH_REJOIN_LEGACY_CONFIGURATION_ID = "ExclusiveMergeBlockConfiguration";
|
||||
private static final String BRANCH_REJOIN_CONFIGURATION_ID = "BranchRejoinBlockConfiguration";
|
||||
|
||||
private Map<String, Class<?>> idToClass = new HashMap<>();
|
||||
private Map<Class<?>, String> classToId = new HashMap<>();
|
||||
|
|
@ -51,6 +53,9 @@ public class DynamicBlockConfigurationTypeResolver extends TypeIdResolverBase {
|
|||
if (CHAT_INTERACTION_LEGACY_CONFIGURATION_ID.equals(id)) {
|
||||
id = CHAT_INTERACTION_CONFIGURATION_ID;
|
||||
}
|
||||
if (BRANCH_REJOIN_LEGACY_CONFIGURATION_ID.equals(id)) {
|
||||
id = BRANCH_REJOIN_CONFIGURATION_ID;
|
||||
}
|
||||
Class<?> clazz = idToClass.get(id);
|
||||
if (clazz == null && id != null) {
|
||||
String normalized = id;
|
||||
|
|
|
|||
|
|
@ -8,24 +8,24 @@ import org.springframework.stereotype.Component;
|
|||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.IOCapability;
|
||||
import it.cnr.isti.workflow.manager.blocks.IOCapabilityType;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.ios.IODescriptor;
|
||||
import it.cnr.isti.workflow.manager.ios.IOType;
|
||||
|
||||
@Component
|
||||
public class ExclusiveMergeBlockFactory
|
||||
implements BlockFactory<ExclusiveMergeBlockType, ExclusiveMergeBlockConfiguration> {
|
||||
public class BranchRejoinBlockFactory
|
||||
implements BlockFactory<BranchRejoinBlockType, BranchRejoinBlockConfiguration> {
|
||||
|
||||
public static final String DEFAULT_OUTPUT = "output";
|
||||
private static final List<IOCapability> ANY_CAPABILITY = List.of(new IOCapability(IOCapabilityType.ANY, false));
|
||||
|
||||
@Autowired
|
||||
ExclusiveMergeBlockType blockType;
|
||||
BranchRejoinBlockType blockType;
|
||||
|
||||
@Override
|
||||
public Block<ExclusiveMergeBlockType> create(ExclusiveMergeBlockConfiguration configuration) {
|
||||
Block.BlockBuilder<ExclusiveMergeBlockType> builder = Block.<ExclusiveMergeBlockType>builder()
|
||||
public Block<BranchRejoinBlockType> create(BranchRejoinBlockConfiguration configuration) {
|
||||
Block.BlockBuilder<BranchRejoinBlockType> builder = Block.<BranchRejoinBlockType>builder()
|
||||
.specificConfiguration(configuration)
|
||||
.type(blockType);
|
||||
configuration.getInputs().forEach(input -> builder.input(IODescriptor.input(
|
||||
|
|
@ -35,13 +35,13 @@ public class ExclusiveMergeBlockFactory
|
|||
}
|
||||
|
||||
@Override
|
||||
public Block<ExclusiveMergeBlockType> createEmpty() {
|
||||
return create(ExclusiveMergeBlockConfiguration.empty());
|
||||
public Block<BranchRejoinBlockType> createEmpty() {
|
||||
return create(BranchRejoinBlockConfiguration.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<ExclusiveMergeBlockType> getBlockType() {
|
||||
return ExclusiveMergeBlockType.class;
|
||||
public Class<BranchRejoinBlockType> getBlockType() {
|
||||
return BranchRejoinBlockType.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -11,6 +11,7 @@ public class BlockTypes {
|
|||
|
||||
private static Map<String, BlockType> blockTypes = new HashMap<>();
|
||||
private static final String CHAT_INTERACTION_LEGACY_TYPE = "ChatHumanInteraction";
|
||||
private static final String BRANCH_REJOIN_LEGACY_TYPE = "ExclusiveMergeBlock";
|
||||
|
||||
|
||||
public static BlockType get(String name) {
|
||||
|
|
@ -21,6 +22,9 @@ public class BlockTypes {
|
|||
if (CHAT_INTERACTION_LEGACY_TYPE.equals(name)) {
|
||||
return blockTypes.get("ChatInteraction");
|
||||
}
|
||||
if (BRANCH_REJOIN_LEGACY_TYPE.equals(name)) {
|
||||
return blockTypes.get("BranchRejoinBlock");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ package it.cnr.isti.workflow.manager.blocks.types;
|
|||
import org.springframework.stereotype.Component;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.flows.model.capabilities.NodeTypeCapabilities;
|
||||
|
||||
@Component(ExclusiveMergeBlockType.TYPE)
|
||||
public class ExclusiveMergeBlockType implements BlockType {
|
||||
@Component(BranchRejoinBlockType.TYPE)
|
||||
public class BranchRejoinBlockType implements BlockType {
|
||||
|
||||
public static final String TYPE = "ExclusiveMergeBlock";
|
||||
public static final String TYPE = "BranchRejoinBlock";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
|
|
@ -33,11 +33,11 @@ public class ExclusiveMergeBlockType implements BlockType {
|
|||
|
||||
@Override
|
||||
public NodeTypeCapabilities getCapabilities() {
|
||||
return NodeTypeCapabilities.merge();
|
||||
return NodeTypeCapabilities.branchRejoin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends BlockConfiguration<?>> getBlockConfigurationClass() {
|
||||
return ExclusiveMergeBlockConfiguration.class;
|
||||
return BranchRejoinBlockConfiguration.class;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@ import java.util.Map;
|
|||
import org.springframework.stereotype.Component;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionEventLogger;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.NodeExecutionException;
|
||||
|
|
@ -15,30 +15,30 @@ import it.cnr.isti.workflow.manager.executions.steps.Input;
|
|||
import it.cnr.isti.workflow.manager.executions.steps.InputResolutionState;
|
||||
|
||||
@Component
|
||||
public class ExclusiveMergeExecutor implements BlockExecutor<ExclusiveMergeBlockType> {
|
||||
public class BranchRejoinExecutor implements BlockExecutor<BranchRejoinBlockType> {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<ExclusiveMergeBlockType> block, List<Input> inputs,
|
||||
public Map<String, Object> execute(Block<BranchRejoinBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors, ExecutionEventLogger eventLogger) {
|
||||
List<Input> selected = inputs.stream()
|
||||
.filter(input -> input.getResolutionState() == InputResolutionState.VALUE)
|
||||
.toList();
|
||||
if (inputs.stream().anyMatch(input -> input.getResolutionState() == InputResolutionState.UNAVAILABLE)) {
|
||||
throw new NodeExecutionException("EXCLUSIVE_MERGE_INPUT_UNAVAILABLE",
|
||||
throw new NodeExecutionException("BRANCH_REJOIN_INPUT_UNAVAILABLE",
|
||||
"Exclusive merge received an unavailable input");
|
||||
}
|
||||
if (selected.size() != 1) {
|
||||
throw new NodeExecutionException("EXCLUSIVE_MERGE_MULTIPLE_VALUES",
|
||||
throw new NodeExecutionException("BRANCH_REJOIN_MULTIPLE_VALUES",
|
||||
"Exclusive merge expected exactly one selected input but received " + selected.size());
|
||||
}
|
||||
ExclusiveMergeBlockConfiguration configuration =
|
||||
(ExclusiveMergeBlockConfiguration) block.getSpecificConfiguration();
|
||||
BranchRejoinBlockConfiguration configuration =
|
||||
(BranchRejoinBlockConfiguration) block.getSpecificConfiguration();
|
||||
return Map.of(configuration.getOutputName(), selected.getFirst().getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<ExclusiveMergeBlockType> getBlockType() {
|
||||
return ExclusiveMergeBlockType.class;
|
||||
public Class<BranchRejoinBlockType> getBlockType() {
|
||||
return BranchRejoinBlockType.class;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import it.cnr.isti.workflow.manager.executions.steps.Input;
|
|||
import it.cnr.isti.workflow.manager.executions.steps.InputResolutionState;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.StepSkipReason;
|
||||
|
||||
public final class ExclusiveMergeActivationPolicy implements StepActivationPolicy {
|
||||
public final class BranchRejoinActivationPolicy implements StepActivationPolicy {
|
||||
|
||||
@Override
|
||||
public StepActivationDecision evaluate(List<Input> inputs, boolean dependenciesSatisfied,
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
package it.cnr.isti.workflow.manager.executions.steps.activation;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowNode;
|
||||
|
||||
public final class StepActivationPolicyRegistry {
|
||||
|
||||
private static final StepActivationPolicy DEFAULT_POLICY = new AllRequiredActivationPolicy();
|
||||
private static final StepActivationPolicy EXCLUSIVE_MERGE_POLICY = new ExclusiveMergeActivationPolicy();
|
||||
private static final StepActivationPolicy BRANCH_REJOIN_POLICY = new BranchRejoinActivationPolicy();
|
||||
|
||||
private StepActivationPolicyRegistry() {
|
||||
}
|
||||
|
|
@ -16,8 +16,8 @@ public final class StepActivationPolicyRegistry {
|
|||
if (node == null) {
|
||||
throw new IllegalArgumentException("Flow node is required to resolve its activation policy");
|
||||
}
|
||||
if (node instanceof Block<?> block && block.getType() instanceof ExclusiveMergeBlockType) {
|
||||
return EXCLUSIVE_MERGE_POLICY;
|
||||
if (node instanceof Block<?> block && block.getType() instanceof BranchRejoinBlockType) {
|
||||
return BRANCH_REJOIN_POLICY;
|
||||
}
|
||||
return DEFAULT_POLICY;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ public record NodeTypeCapabilities(
|
|||
return new NodeTypeCapabilities(NodeVisualRole.DECISION, false, true, true, true, true, true);
|
||||
}
|
||||
|
||||
public static NodeTypeCapabilities merge() {
|
||||
return new NodeTypeCapabilities(NodeVisualRole.MERGE, false, true, true, true, true, true);
|
||||
public static NodeTypeCapabilities branchRejoin() {
|
||||
return new NodeTypeCapabilities(NodeVisualRole.BRANCH_REJOIN, false, true, true, true, true, true);
|
||||
}
|
||||
|
||||
public static NodeTypeCapabilities end() {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package it.cnr.isti.workflow.manager.flows.model.capabilities;
|
|||
public enum NodeVisualRole {
|
||||
ACTIVITY,
|
||||
DECISION,
|
||||
MERGE,
|
||||
BRANCH_REJOIN,
|
||||
END,
|
||||
CONTAINER
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ import org.springframework.stereotype.Component;
|
|||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.EndBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.BlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.ConditionalBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.SwitchBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ConditionalBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.SwitchBlockType;
|
||||
import it.cnr.isti.workflow.manager.containers.Container;
|
||||
|
|
@ -99,7 +99,7 @@ public class FlowDataValidator implements ConstraintValidator<ValidFlowStructure
|
|||
|
||||
validateDependencies(dependencies, nodesById, connections);
|
||||
|
||||
validateExclusiveMergeNodes(nodes, connections);
|
||||
validateBranchRejoinNodes(nodes, connections);
|
||||
validateEndNodes(nodes, connections);
|
||||
validateExclusiveRoutingBranches(nodes, connections, nodesById);
|
||||
}
|
||||
|
|
@ -545,7 +545,7 @@ public class FlowDataValidator implements ConstraintValidator<ValidFlowStructure
|
|||
Set<String> mergedNodes = new LinkedHashSet<>(reachableByOutput.get(leftOutput));
|
||||
mergedNodes.retainAll(reachableByOutput.get(rightOutput));
|
||||
Set<String> explicitlyMergedNodes = mergedNodes.stream()
|
||||
.filter(nodeId -> isExclusiveMerge(nodesById.get(nodeId)))
|
||||
.filter(nodeId -> isBranchRejoin(nodesById.get(nodeId)))
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
Set<String> allowedSharedNodes = new LinkedHashSet<>();
|
||||
for (String mergeNodeId : explicitlyMergedNodes) {
|
||||
|
|
@ -585,21 +585,21 @@ public class FlowDataValidator implements ConstraintValidator<ValidFlowStructure
|
|||
return List.of();
|
||||
}
|
||||
|
||||
private boolean isExclusiveMerge(FlowNode node) {
|
||||
return node instanceof Block<?> block && ExclusiveMergeBlockType.TYPE.equals(block.getType().getName());
|
||||
private boolean isBranchRejoin(FlowNode node) {
|
||||
return node instanceof Block<?> block && BranchRejoinBlockType.TYPE.equals(block.getType().getName());
|
||||
}
|
||||
|
||||
private void validateExclusiveMergeNodes(List<FlowNode> nodes, List<Connection> connections) {
|
||||
private void validateBranchRejoinNodes(List<FlowNode> nodes, List<Connection> connections) {
|
||||
for (FlowNode node : nodes) {
|
||||
if (!(node instanceof Block<?> block)
|
||||
|| !(block.getSpecificConfiguration() instanceof ExclusiveMergeBlockConfiguration configuration)) {
|
||||
|| !(block.getSpecificConfiguration() instanceof BranchRejoinBlockConfiguration configuration)) {
|
||||
continue;
|
||||
}
|
||||
List<Connection> incoming = connections.stream()
|
||||
.filter(connection -> block.getId().equals(connection.getTargetId()))
|
||||
.toList();
|
||||
if (configuration.getInputs().size() < 2 || incoming.size() < 2) {
|
||||
throw validationError(error(ValidationErrorCode.EXCLUSIVE_MERGE_MIN_INPUTS, "block", block.getId(),
|
||||
throw validationError(error(ValidationErrorCode.BRANCH_REJOIN_MIN_INPUTS, "block", block.getId(),
|
||||
"inputs", "Exclusive merge requires at least two connected inputs"));
|
||||
}
|
||||
for (IODescriptor input : block.getInputs()) {
|
||||
|
|
@ -607,11 +607,11 @@ public class FlowDataValidator implements ConstraintValidator<ValidFlowStructure
|
|||
.filter(connection -> input.getName().equals(connection.getTargetName()))
|
||||
.count();
|
||||
if (connectionCount == 0) {
|
||||
throw validationError(error(ValidationErrorCode.EXCLUSIVE_MERGE_INPUT_NOT_CONNECTED,
|
||||
throw validationError(error(ValidationErrorCode.BRANCH_REJOIN_INPUT_NOT_CONNECTED,
|
||||
"block", block.getId(), "inputs", "Exclusive merge input is not connected: " + input.getName()));
|
||||
}
|
||||
if (connectionCount > 1) {
|
||||
throw validationError(error(ValidationErrorCode.EXCLUSIVE_MERGE_INPUT_MULTIPLE_CONNECTIONS,
|
||||
throw validationError(error(ValidationErrorCode.BRANCH_REJOIN_INPUT_MULTIPLE_CONNECTIONS,
|
||||
"block", block.getId(), "inputs",
|
||||
"Exclusive merge input has multiple connections: " + input.getName()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,12 +66,12 @@ public enum ValidationErrorCode {
|
|||
NODE_CANNOT_DEPEND_ON_OTHER_NODES,
|
||||
NODE_CANNOT_HAVE_DEPENDENT_NODES,
|
||||
EXCLUSIVE_BRANCH_MERGE,
|
||||
EXCLUSIVE_MERGE_MIN_INPUTS,
|
||||
EXCLUSIVE_MERGE_INPUT_NOT_CONNECTED,
|
||||
EXCLUSIVE_MERGE_INPUT_MULTIPLE_CONNECTIONS,
|
||||
EXCLUSIVE_MERGE_MULTIPLE_VALUES,
|
||||
EXCLUSIVE_MERGE_INPUT_UNAVAILABLE,
|
||||
UNSTRUCTURED_EXCLUSIVE_MERGE,
|
||||
BRANCH_REJOIN_MIN_INPUTS,
|
||||
BRANCH_REJOIN_INPUT_NOT_CONNECTED,
|
||||
BRANCH_REJOIN_INPUT_MULTIPLE_CONNECTIONS,
|
||||
BRANCH_REJOIN_MULTIPLE_VALUES,
|
||||
BRANCH_REJOIN_INPUT_UNAVAILABLE,
|
||||
UNSTRUCTURED_BRANCH_REJOIN,
|
||||
HUMAN_DECISION_INVALID_CHOICE,
|
||||
HUMAN_DECISION_RATIONALE_REQUIRED,
|
||||
END_OUTCOME_REQUIRED,
|
||||
|
|
|
|||
|
|
@ -671,7 +671,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Jensen Recruitment Process - Full Revised (Structured)",
|
||||
"description": "Node-per-node structured port of the Full Revised Jensen recruitment UML, built with explicit HumanDecisionBlock gateways, ExclusiveMergeBlock rejoins, EndBlock outcomes and FlowLane swimlanes. Does not replace the existing macro-flow seed.",
|
||||
"description": "Node-per-node structured port of the Full Revised Jensen recruitment UML, built with explicit HumanDecisionBlock gateways, BranchRejoinBlock rejoins, EndBlock outcomes and FlowLane swimlanes. Does not replace the existing macro-flow seed.",
|
||||
"owner": "testuser",
|
||||
"createdAt": "2026-07-22T10:00:00",
|
||||
"lastUpdateAt": "2026-07-22T10:00:00",
|
||||
|
|
@ -1056,7 +1056,7 @@
|
|||
],
|
||||
"laneId": "2bc166dc-fe39-550d-a1cf-f4d7a6e7686c",
|
||||
"specificConfiguration": {
|
||||
"type": "ExclusiveMergeBlockConfiguration",
|
||||
"type": "BranchRejoinBlockConfiguration",
|
||||
"name": "merge-job-description",
|
||||
"inputs": [
|
||||
{
|
||||
|
|
@ -1072,7 +1072,7 @@
|
|||
],
|
||||
"outputName": "output"
|
||||
},
|
||||
"typeName": "ExclusiveMergeBlock"
|
||||
"typeName": "BranchRejoinBlock"
|
||||
},
|
||||
{
|
||||
"id": "287ca7f6-074d-5430-87fb-131a76acc91e",
|
||||
|
|
@ -1224,7 +1224,7 @@
|
|||
],
|
||||
"laneId": "18a8d278-ebcd-53f2-b3e8-3a4fb4c4a83e",
|
||||
"specificConfiguration": {
|
||||
"type": "ExclusiveMergeBlockConfiguration",
|
||||
"type": "BranchRejoinBlockConfiguration",
|
||||
"name": "merge-job-advertisement",
|
||||
"inputs": [
|
||||
{
|
||||
|
|
@ -1240,7 +1240,7 @@
|
|||
],
|
||||
"outputName": "output"
|
||||
},
|
||||
"typeName": "ExclusiveMergeBlock"
|
||||
"typeName": "BranchRejoinBlock"
|
||||
},
|
||||
{
|
||||
"id": "fb6beef9-8de2-5b40-a169-33e8d0b5777f",
|
||||
|
|
@ -1433,7 +1433,7 @@
|
|||
],
|
||||
"laneId": "8036d9f4-67bd-5198-942f-53bb41afa29c",
|
||||
"specificConfiguration": {
|
||||
"type": "ExclusiveMergeBlockConfiguration",
|
||||
"type": "BranchRejoinBlockConfiguration",
|
||||
"name": "merge-external-recruitment-start",
|
||||
"inputs": [
|
||||
{
|
||||
|
|
@ -1449,7 +1449,7 @@
|
|||
],
|
||||
"outputName": "output"
|
||||
},
|
||||
"typeName": "ExclusiveMergeBlock"
|
||||
"typeName": "BranchRejoinBlock"
|
||||
},
|
||||
{
|
||||
"id": "e67b2d57-359c-5710-b63a-6df27e3b6bd0",
|
||||
|
|
@ -1682,7 +1682,7 @@
|
|||
],
|
||||
"laneId": "91bbb8f9-8882-5cc3-ba02-f41fc60e75b7",
|
||||
"specificConfiguration": {
|
||||
"type": "ExclusiveMergeBlockConfiguration",
|
||||
"type": "BranchRejoinBlockConfiguration",
|
||||
"name": "merge-application-submission-entry",
|
||||
"inputs": [
|
||||
{
|
||||
|
|
@ -1698,7 +1698,7 @@
|
|||
],
|
||||
"outputName": "output"
|
||||
},
|
||||
"typeName": "ExclusiveMergeBlock"
|
||||
"typeName": "BranchRejoinBlock"
|
||||
},
|
||||
{
|
||||
"id": "298edbd7-7bda-511c-b72e-93cf65468939",
|
||||
|
|
@ -2006,7 +2006,7 @@
|
|||
],
|
||||
"laneId": "47b1f830-b4d1-5788-85a6-538193521f23",
|
||||
"specificConfiguration": {
|
||||
"type": "ExclusiveMergeBlockConfiguration",
|
||||
"type": "BranchRejoinBlockConfiguration",
|
||||
"name": "merge-post-assessment",
|
||||
"inputs": [
|
||||
{
|
||||
|
|
@ -2027,7 +2027,7 @@
|
|||
],
|
||||
"outputName": "output"
|
||||
},
|
||||
"typeName": "ExclusiveMergeBlock"
|
||||
"typeName": "BranchRejoinBlock"
|
||||
},
|
||||
{
|
||||
"id": "851978f2-057f-59bf-ad7f-e75e0f412005",
|
||||
|
|
@ -3009,7 +3009,7 @@
|
|||
],
|
||||
"laneId": "df5fcc5f-c0cd-5592-bb50-7815a7f96d70",
|
||||
"specificConfiguration": {
|
||||
"type": "ExclusiveMergeBlockConfiguration",
|
||||
"type": "BranchRejoinBlockConfiguration",
|
||||
"name": "merge-job-description",
|
||||
"inputs": [
|
||||
{
|
||||
|
|
@ -3025,7 +3025,7 @@
|
|||
],
|
||||
"outputName": "output"
|
||||
},
|
||||
"typeName": "ExclusiveMergeBlock"
|
||||
"typeName": "BranchRejoinBlock"
|
||||
},
|
||||
{
|
||||
"id": "0159e7e4-9dba-5bde-b86a-e5a8cd97c005",
|
||||
|
|
@ -3322,7 +3322,7 @@
|
|||
],
|
||||
"laneId": "906bae7d-fbe8-5dc1-a33a-91094e302d78",
|
||||
"specificConfiguration": {
|
||||
"type": "ExclusiveMergeBlockConfiguration",
|
||||
"type": "BranchRejoinBlockConfiguration",
|
||||
"name": "merge-advertisement-review-method",
|
||||
"inputs": [
|
||||
{
|
||||
|
|
@ -3338,7 +3338,7 @@
|
|||
],
|
||||
"outputName": "output"
|
||||
},
|
||||
"typeName": "ExclusiveMergeBlock"
|
||||
"typeName": "BranchRejoinBlock"
|
||||
},
|
||||
{
|
||||
"id": "1163d218-5c38-5ed2-96f4-3ba9901f8daa",
|
||||
|
|
@ -3397,7 +3397,7 @@
|
|||
],
|
||||
"laneId": "906bae7d-fbe8-5dc1-a33a-91094e302d78",
|
||||
"specificConfiguration": {
|
||||
"type": "ExclusiveMergeBlockConfiguration",
|
||||
"type": "BranchRejoinBlockConfiguration",
|
||||
"name": "merge-job-advertisement",
|
||||
"inputs": [
|
||||
{
|
||||
|
|
@ -3413,7 +3413,7 @@
|
|||
],
|
||||
"outputName": "output"
|
||||
},
|
||||
"typeName": "ExclusiveMergeBlock"
|
||||
"typeName": "BranchRejoinBlock"
|
||||
},
|
||||
{
|
||||
"id": "d6aa9f6d-ae17-5b8b-b42b-f274768c227a",
|
||||
|
|
@ -3686,7 +3686,7 @@
|
|||
],
|
||||
"laneId": "e3844222-ee0a-5bd8-8448-187364789eca",
|
||||
"specificConfiguration": {
|
||||
"type": "ExclusiveMergeBlockConfiguration",
|
||||
"type": "BranchRejoinBlockConfiguration",
|
||||
"name": "merge-external-recruitment-start",
|
||||
"inputs": [
|
||||
{
|
||||
|
|
@ -3702,7 +3702,7 @@
|
|||
],
|
||||
"outputName": "output"
|
||||
},
|
||||
"typeName": "ExclusiveMergeBlock"
|
||||
"typeName": "BranchRejoinBlock"
|
||||
},
|
||||
{
|
||||
"id": "188536f9-3f93-5b9e-9fa5-93223be338e4",
|
||||
|
|
@ -3992,7 +3992,7 @@
|
|||
],
|
||||
"laneId": "efaffb7a-47b3-521c-b11f-0f9074212cdf",
|
||||
"specificConfiguration": {
|
||||
"type": "ExclusiveMergeBlockConfiguration",
|
||||
"type": "BranchRejoinBlockConfiguration",
|
||||
"name": "merge-application-submission-entry",
|
||||
"inputs": [
|
||||
{
|
||||
|
|
@ -4008,7 +4008,7 @@
|
|||
],
|
||||
"outputName": "output"
|
||||
},
|
||||
"typeName": "ExclusiveMergeBlock"
|
||||
"typeName": "BranchRejoinBlock"
|
||||
},
|
||||
{
|
||||
"id": "a3903787-c097-5602-acf0-b7d0012a62e4",
|
||||
|
|
@ -4387,7 +4387,7 @@
|
|||
],
|
||||
"laneId": "595b4e4b-0c5d-504f-aa1c-b35e9f2a6a15",
|
||||
"specificConfiguration": {
|
||||
"type": "ExclusiveMergeBlockConfiguration",
|
||||
"type": "BranchRejoinBlockConfiguration",
|
||||
"name": "merge-post-assessment",
|
||||
"inputs": [
|
||||
{
|
||||
|
|
@ -4408,7 +4408,7 @@
|
|||
],
|
||||
"outputName": "output"
|
||||
},
|
||||
"typeName": "ExclusiveMergeBlock"
|
||||
"typeName": "BranchRejoinBlock"
|
||||
},
|
||||
{
|
||||
"id": "2ba31b21-8d5e-5a9e-8f75-140274fb6389",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import tools.jackson.databind.ObjectMapper;
|
|||
|
||||
/**
|
||||
* Bean-validation coverage for the dynamic configurations introduced alongside
|
||||
* ExclusiveMergeBlock, HumanDecisionBlock and EndBlock: cardinality bounds, uniqueness
|
||||
* BranchRejoinBlock, HumanDecisionBlock and EndBlock: cardinality bounds, uniqueness
|
||||
* checks and required fields.
|
||||
*/
|
||||
class BlockConfigurationValidationTest {
|
||||
|
|
@ -25,10 +25,10 @@ class BlockConfigurationValidationTest {
|
|||
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
|
||||
|
||||
@Test
|
||||
void exclusiveMergeRejectsFewerThanTwoInputs() {
|
||||
ExclusiveMergeBlockConfiguration configuration = ExclusiveMergeBlockConfiguration.builder()
|
||||
void branchRejoinRejectsFewerThanTwoInputs() {
|
||||
BranchRejoinBlockConfiguration configuration = BranchRejoinBlockConfiguration.builder()
|
||||
.name("merge")
|
||||
.inputs(List.of(new ExclusiveMergeInput("only")))
|
||||
.inputs(List.of(new BranchRejoinInput("only")))
|
||||
.outputName("output")
|
||||
.build();
|
||||
|
||||
|
|
@ -36,11 +36,11 @@ class BlockConfigurationValidationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void exclusiveMergeRejectsMoreThanTenInputs() {
|
||||
List<ExclusiveMergeInput> inputs = java.util.stream.IntStream.range(0, 11)
|
||||
.mapToObj(index -> new ExclusiveMergeInput("branch" + index))
|
||||
void branchRejoinRejectsMoreThanTenInputs() {
|
||||
List<BranchRejoinInput> inputs = java.util.stream.IntStream.range(0, 11)
|
||||
.mapToObj(index -> new BranchRejoinInput("branch" + index))
|
||||
.toList();
|
||||
ExclusiveMergeBlockConfiguration configuration = ExclusiveMergeBlockConfiguration.builder()
|
||||
BranchRejoinBlockConfiguration configuration = BranchRejoinBlockConfiguration.builder()
|
||||
.name("merge")
|
||||
.inputs(inputs)
|
||||
.outputName("output")
|
||||
|
|
@ -50,23 +50,23 @@ class BlockConfigurationValidationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void exclusiveMergeRejectsDuplicateInputNames() {
|
||||
ExclusiveMergeBlockConfiguration configuration = ExclusiveMergeBlockConfiguration.builder()
|
||||
void branchRejoinRejectsDuplicateInputNames() {
|
||||
BranchRejoinBlockConfiguration configuration = BranchRejoinBlockConfiguration.builder()
|
||||
.name("merge")
|
||||
.inputs(List.of(new ExclusiveMergeInput("branch", "A", IOType.ANY),
|
||||
new ExclusiveMergeInput("branch", "B", IOType.ANY)))
|
||||
.inputs(List.of(new BranchRejoinInput("branch", "A", IOType.ANY),
|
||||
new BranchRejoinInput("branch", "B", IOType.ANY)))
|
||||
.outputName("output")
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<ExclusiveMergeBlockConfiguration>> violations = validator.validate(configuration);
|
||||
Set<ConstraintViolation<BranchRejoinBlockConfiguration>> violations = validator.validate(configuration);
|
||||
assertTrue(violations.stream().anyMatch(v -> v.getMessage().contains("unique")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exclusiveMergeAcceptsAWellFormedConfiguration() {
|
||||
ExclusiveMergeBlockConfiguration configuration = ExclusiveMergeBlockConfiguration.builder()
|
||||
void branchRejoinAcceptsAWellFormedConfiguration() {
|
||||
BranchRejoinBlockConfiguration configuration = BranchRejoinBlockConfiguration.builder()
|
||||
.name("merge")
|
||||
.inputs(List.of(new ExclusiveMergeInput("existing"), new ExclusiveMergeInput("new")))
|
||||
.inputs(List.of(new BranchRejoinInput("existing"), new BranchRejoinInput("new")))
|
||||
.outputName("output")
|
||||
.build();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
package it.cnr.isti.workflow.manager.blocks.configurations;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
|
||||
/**
|
||||
* A renamed block type (e.g. ExclusiveMergeBlock -> BranchRejoinBlock) must
|
||||
* still deserialize flows that were persisted under the old type id before
|
||||
* the rename - flow_data in the database isn't migrated automatically.
|
||||
* DynamicBlockConfigurationTypeResolver/BlockTypes carry a legacy-id
|
||||
* fallback for exactly this; this test locks in the current one (found the
|
||||
* hard way: renaming without it breaks application startup on any
|
||||
* already-persisted flow using the old id).
|
||||
*/
|
||||
@SpringBootTest
|
||||
@TestPropertySource(locations = "classpath:test.properties")
|
||||
class LegacyBlockTypeIdTest {
|
||||
|
||||
private static final String LEGACY_BLOCK_JSON = """
|
||||
{
|
||||
"id": "legacy-merge-block",
|
||||
"name": "merge",
|
||||
"inputs": [
|
||||
{"name": "a", "type": "ANY", "multiple": false},
|
||||
{"name": "b", "type": "ANY", "multiple": false}
|
||||
],
|
||||
"outputs": [{"name": "output", "type": "ANY", "multiple": false}],
|
||||
"specificConfiguration": {
|
||||
"type": "ExclusiveMergeBlockConfiguration",
|
||||
"name": "merge",
|
||||
"inputs": [
|
||||
{"name": "a", "label": "A", "type": "ANY"},
|
||||
{"name": "b", "label": "B", "type": "ANY"}
|
||||
],
|
||||
"outputName": "output"
|
||||
},
|
||||
"typeName": "ExclusiveMergeBlock"
|
||||
}
|
||||
""";
|
||||
|
||||
@Test
|
||||
void legacyExclusiveMergeConfigurationIdResolvesToBranchRejoin() throws Exception {
|
||||
Block<?> block = ObjectMapperHolder.mapper.readValue(LEGACY_BLOCK_JSON, Block.class);
|
||||
|
||||
assertNotNull(block.getSpecificConfiguration());
|
||||
assertInstanceOf(BranchRejoinBlockConfiguration.class, block.getSpecificConfiguration());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyExclusiveMergeTypeNameResolvesToBranchRejoinBlockType() throws Exception {
|
||||
Block<?> block = ObjectMapperHolder.mapper.readValue(LEGACY_BLOCK_JSON, Block.class);
|
||||
|
||||
assertNotNull(block.getType());
|
||||
assertEquals(BranchRejoinBlockType.TYPE, block.getType().getName());
|
||||
}
|
||||
}
|
||||
|
|
@ -22,28 +22,28 @@ import org.springframework.test.context.TestPropertySource;
|
|||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ConditionalBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.EndBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeInput;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinInput;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.ConditionalBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.EndBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.ExclusiveMergeBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.BranchRejoinBlockFactory;
|
||||
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.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.StepStatus;
|
||||
import it.cnr.isti.workflow.manager.flows.model.Connection;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowData;
|
||||
import it.cnr.isti.workflow.manager.ios.IOType;
|
||||
|
||||
/**
|
||||
* Concurrency coverage for ExclusiveMergeBlock scheduling: repeated split+merge executions
|
||||
* Concurrency coverage for BranchRejoinBlock scheduling: repeated split+merge executions
|
||||
* under the engine's own asynchronous executors must never double-schedule the merge, must
|
||||
* only complete it once every input is resolved, and must stay consistent under cancel and
|
||||
* under concurrent snapshot reads.
|
||||
*/
|
||||
@SpringBootTest
|
||||
@TestPropertySource(locations = "classpath:test.properties")
|
||||
class ExclusiveMergeConcurrencyTest {
|
||||
class BranchRejoinConcurrencyTest {
|
||||
|
||||
@Autowired
|
||||
ExecutionsService executionsService;
|
||||
|
|
@ -52,7 +52,7 @@ class ExclusiveMergeConcurrencyTest {
|
|||
ConditionalBlockFactory conditionalBlockFactory;
|
||||
|
||||
@Autowired
|
||||
ExclusiveMergeBlockFactory exclusiveMergeBlockFactory;
|
||||
BranchRejoinBlockFactory branchRejoinBlockFactory;
|
||||
|
||||
@Autowired
|
||||
EndBlockFactory endBlockFactory;
|
||||
|
|
@ -98,7 +98,7 @@ class ExclusiveMergeConcurrencyTest {
|
|||
.filter(block -> block.getType() instanceof ConditionalBlockType)
|
||||
.findFirst().orElseThrow();
|
||||
Block<?> merge = flow.getBlocks().stream()
|
||||
.filter(block -> block.getType() instanceof ExclusiveMergeBlockType)
|
||||
.filter(block -> block.getType() instanceof BranchRejoinBlockType)
|
||||
.findFirst().orElseThrow();
|
||||
|
||||
ExecutionObject execution = executionsService.createExecution("Concurrent merge - " + decision, flow);
|
||||
|
|
@ -212,12 +212,12 @@ class ExclusiveMergeConcurrencyTest {
|
|||
.condition("${{decision}} == 'yes'")
|
||||
.outputTemplate("${{payload}}")
|
||||
.build());
|
||||
Block<ExclusiveMergeBlockType> merge = exclusiveMergeBlockFactory.create(
|
||||
ExclusiveMergeBlockConfiguration.builder()
|
||||
Block<BranchRejoinBlockType> merge = branchRejoinBlockFactory.create(
|
||||
BranchRejoinBlockConfiguration.builder()
|
||||
.name("Eligibility merge")
|
||||
.inputs(List.of(
|
||||
new ExclusiveMergeInput("accepted", "Accepted", IOType.TEXT),
|
||||
new ExclusiveMergeInput("rejected", "Rejected", IOType.TEXT)))
|
||||
new BranchRejoinInput("accepted", "Accepted", IOType.TEXT),
|
||||
new BranchRejoinInput("rejected", "Rejected", IOType.TEXT)))
|
||||
.outputName("result")
|
||||
.build());
|
||||
Block<EndBlockType> end = endBlockFactory.create(EndBlockConfiguration.builder()
|
||||
|
|
@ -16,17 +16,17 @@ import org.springframework.test.context.TestPropertySource;
|
|||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ConditionalBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.EndBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeInput;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinInput;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HumanDecisionBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HumanDecisionOption;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.ConditionalBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.EndBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.ExclusiveMergeBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.BranchRejoinBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.HumanDecisionBlockFactory;
|
||||
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.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.bias.BiasExecutionContext;
|
||||
import it.cnr.isti.workflow.manager.executions.bias.BiasExecutionMode;
|
||||
|
|
@ -54,7 +54,7 @@ class ControlFlowGatewayIntegrationTest {
|
|||
ConditionalBlockFactory conditionalBlockFactory;
|
||||
|
||||
@Autowired
|
||||
ExclusiveMergeBlockFactory exclusiveMergeBlockFactory;
|
||||
BranchRejoinBlockFactory branchRejoinBlockFactory;
|
||||
|
||||
@Autowired
|
||||
HumanDecisionBlockFactory humanDecisionBlockFactory;
|
||||
|
|
@ -69,12 +69,12 @@ class ControlFlowGatewayIntegrationTest {
|
|||
.condition("${{decision}} == 'yes'")
|
||||
.outputTemplate("${{payload}}")
|
||||
.build());
|
||||
Block<ExclusiveMergeBlockType> merge = exclusiveMergeBlockFactory.create(
|
||||
ExclusiveMergeBlockConfiguration.builder()
|
||||
Block<BranchRejoinBlockType> merge = branchRejoinBlockFactory.create(
|
||||
BranchRejoinBlockConfiguration.builder()
|
||||
.name("Eligibility merge")
|
||||
.inputs(List.of(
|
||||
new ExclusiveMergeInput("accepted", "Accepted", IOType.TEXT),
|
||||
new ExclusiveMergeInput("rejected", "Rejected", IOType.TEXT)))
|
||||
new BranchRejoinInput("accepted", "Accepted", IOType.TEXT),
|
||||
new BranchRejoinInput("rejected", "Rejected", IOType.TEXT)))
|
||||
.outputName("result")
|
||||
.build());
|
||||
Block<EndBlockType> end = endBlockFactory.create(end("DONE", "Completed"));
|
||||
|
|
@ -88,7 +88,7 @@ class ControlFlowGatewayIntegrationTest {
|
|||
.build();
|
||||
|
||||
for (String decision : List.of("yes", "no")) {
|
||||
ExecutionObject execution = executionsService.createExecution("Explicit exclusive merge", flow);
|
||||
ExecutionObject execution = executionsService.createExecution("Explicit branch rejoin", flow);
|
||||
executionsService.prepareInput(execution.getId(), router.getId(), "decision", decision);
|
||||
executionsService.prepareInput(execution.getId(), router.getId(), "payload", "case-42");
|
||||
executionsService.startExecution(execution.getId());
|
||||
|
|
|
|||
|
|
@ -17,15 +17,15 @@ import tools.jackson.databind.JsonNode;
|
|||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.EndBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeInput;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinInput;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HumanDecisionBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HumanDecisionOption;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.EndBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.ExclusiveMergeBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.BranchRejoinBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.HumanDecisionBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.EndBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.InputResolutionState;
|
||||
import it.cnr.isti.workflow.manager.flows.model.Connection;
|
||||
|
|
@ -48,7 +48,7 @@ class JsonDossierExecutionIntegrationTest {
|
|||
HumanDecisionBlockFactory humanDecisionBlockFactory;
|
||||
|
||||
@Autowired
|
||||
ExclusiveMergeBlockFactory exclusiveMergeBlockFactory;
|
||||
BranchRejoinBlockFactory branchRejoinBlockFactory;
|
||||
|
||||
@Autowired
|
||||
EndBlockFactory endBlockFactory;
|
||||
|
|
@ -62,12 +62,12 @@ class JsonDossierExecutionIntegrationTest {
|
|||
new HumanDecisionOption("existing", "Existing position"),
|
||||
new HumanDecisionOption("new", "New position")))
|
||||
.build());
|
||||
Block<ExclusiveMergeBlockType> merge = exclusiveMergeBlockFactory.create(
|
||||
ExclusiveMergeBlockConfiguration.builder()
|
||||
Block<BranchRejoinBlockType> merge = branchRejoinBlockFactory.create(
|
||||
BranchRejoinBlockConfiguration.builder()
|
||||
.name("merge-existing-position")
|
||||
.inputs(List.of(
|
||||
new ExclusiveMergeInput("existing", "Existing position", IOType.JSON),
|
||||
new ExclusiveMergeInput("new", "New position", IOType.JSON)))
|
||||
new BranchRejoinInput("existing", "Existing position", IOType.JSON),
|
||||
new BranchRejoinInput("new", "New position", IOType.JSON)))
|
||||
.outputName("output")
|
||||
.build());
|
||||
Block<EndBlockType> end = endBlockFactory.create(EndBlockConfiguration.builder()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import org.springframework.boot.test.context.SpringBootTest;
|
|||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.types.EndBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType;
|
||||
import it.cnr.isti.workflow.manager.controllers.BiasCapabilitiesController;
|
||||
import it.cnr.isti.workflow.manager.flows.model.bias.BiasActivationMode;
|
||||
|
|
@ -34,8 +34,8 @@ class ControlFlowBlockBiasCapabilitiesTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void exclusiveMergeBlockDoesNotSupportRoutingOverride() {
|
||||
var capabilities = capabilitiesController.getCapabilities(ExclusiveMergeBlockType.TYPE);
|
||||
void branchRejoinBlockDoesNotSupportRoutingOverride() {
|
||||
var capabilities = capabilitiesController.getCapabilities(BranchRejoinBlockType.TYPE);
|
||||
assertFalse(capabilities.activationModes().contains(BiasActivationMode.ROUTING_OVERRIDE));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@ import java.util.Map;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeInput;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinInput;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.NodeExecutionException;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.ios.IODescriptor;
|
||||
import it.cnr.isti.workflow.manager.ios.IOType;
|
||||
|
||||
class ExclusiveMergeExecutorTest {
|
||||
class BranchRejoinExecutorTest {
|
||||
|
||||
private final ExclusiveMergeExecutor executor = new ExclusiveMergeExecutor();
|
||||
private final BranchRejoinExecutor executor = new BranchRejoinExecutor();
|
||||
|
||||
@Test
|
||||
void propagatesTheSingleSelectedValueUnderTheConfiguredOutputName() {
|
||||
|
|
@ -41,7 +41,7 @@ class ExclusiveMergeExecutorTest {
|
|||
NodeExecutionException exception = assertThrows(NodeExecutionException.class,
|
||||
() -> executor.execute(block(), List.of(existing, newValue), Map.of(), Map.of(), Map.of(), null));
|
||||
|
||||
assertEquals("EXCLUSIVE_MERGE_MULTIPLE_VALUES", exception.getErrorCode());
|
||||
assertEquals("BRANCH_REJOIN_MULTIPLE_VALUES", exception.getErrorCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -53,18 +53,18 @@ class ExclusiveMergeExecutorTest {
|
|||
NodeExecutionException exception = assertThrows(NodeExecutionException.class,
|
||||
() -> executor.execute(block(), List.of(existing, unavailable), Map.of(), Map.of(), Map.of(), null));
|
||||
|
||||
assertEquals("EXCLUSIVE_MERGE_INPUT_UNAVAILABLE", exception.getErrorCode());
|
||||
assertEquals("BRANCH_REJOIN_INPUT_UNAVAILABLE", exception.getErrorCode());
|
||||
}
|
||||
|
||||
private Block<ExclusiveMergeBlockType> block() {
|
||||
ExclusiveMergeBlockConfiguration configuration = ExclusiveMergeBlockConfiguration.builder()
|
||||
private Block<BranchRejoinBlockType> block() {
|
||||
BranchRejoinBlockConfiguration configuration = BranchRejoinBlockConfiguration.builder()
|
||||
.name("merge")
|
||||
.inputs(List.of(new ExclusiveMergeInput("existing"), new ExclusiveMergeInput("new")))
|
||||
.inputs(List.of(new BranchRejoinInput("existing"), new BranchRejoinInput("new")))
|
||||
.outputName("output")
|
||||
.build();
|
||||
return Block.<ExclusiveMergeBlockType>builder()
|
||||
return Block.<BranchRejoinBlockType>builder()
|
||||
.specificConfiguration(configuration)
|
||||
.type(new ExclusiveMergeBlockType())
|
||||
.type(new BranchRejoinBlockType())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,9 @@ import it.cnr.isti.workflow.manager.executions.steps.StepSkipReason;
|
|||
import it.cnr.isti.workflow.manager.ios.IODescriptor;
|
||||
import it.cnr.isti.workflow.manager.ios.IOType;
|
||||
|
||||
class ExclusiveMergeActivationPolicyTest {
|
||||
class BranchRejoinActivationPolicyTest {
|
||||
|
||||
private final ExclusiveMergeActivationPolicy policy = new ExclusiveMergeActivationPolicy();
|
||||
private final BranchRejoinActivationPolicy policy = new BranchRejoinActivationPolicy();
|
||||
private final IODescriptor existingInput = IODescriptor.of("existing", IOType.TEXT);
|
||||
private final IODescriptor newInput = IODescriptor.of("new", IOType.TEXT);
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ import org.springframework.test.context.TestPropertySource;
|
|||
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.EndBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowNode;
|
||||
import it.cnr.isti.workflow.manager.flows.model.bias.BlockBiasAnnotation;
|
||||
|
|
@ -96,13 +96,13 @@ class JensenStructuredFlowsTest {
|
|||
.filter(node -> node instanceof Block<?> block && block.getType() instanceof HumanDecisionBlockType)
|
||||
.count();
|
||||
long merges = nodes.stream()
|
||||
.filter(node -> node instanceof Block<?> block && block.getType() instanceof ExclusiveMergeBlockType)
|
||||
.filter(node -> node instanceof Block<?> block && block.getType() instanceof BranchRejoinBlockType)
|
||||
.count();
|
||||
long ends = nodes.stream()
|
||||
.filter(node -> node instanceof Block<?> block && block.getType() instanceof EndBlockType)
|
||||
.count();
|
||||
assertEquals(expectedDecisions, decisions, () -> flow.name() + ": HumanDecisionBlock count");
|
||||
assertEquals(expectedMerges, merges, () -> flow.name() + ": ExclusiveMergeBlock count");
|
||||
assertEquals(expectedMerges, merges, () -> flow.name() + ": BranchRejoinBlock count");
|
||||
assertEquals(expectedEnds, ends, () -> flow.name() + ": EndBlock count");
|
||||
assertEquals(expectedLanes, flow.flow().getLanes().size(), () -> flow.name() + ": lane count");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ import org.springframework.test.context.TestPropertySource;
|
|||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ConditionalBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeInput;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinInput;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HumanInteractiveBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.ConditionalBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.ExclusiveMergeBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.BranchRejoinBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.HumanInteractiveBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ConditionalBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanInteractionBlockType;
|
||||
import it.cnr.isti.workflow.manager.flows.model.Connection;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowData;
|
||||
|
|
@ -30,12 +30,12 @@ import jakarta.validation.Validator;
|
|||
/**
|
||||
* Confirms the "merge incrociato rifiutato" requirement: two exclusive branches from the same
|
||||
* Conditional cannot reconverge on a shared downstream node unless that node is an explicit
|
||||
* ExclusiveMergeBlock. Complements the Phase 0 characterization tests, which only assert this
|
||||
* BranchRejoinBlock. Complements the Phase 0 characterization tests, which only assert this
|
||||
* behavior indirectly.
|
||||
*/
|
||||
@SpringBootTest
|
||||
@TestPropertySource(locations = "classpath:test.properties")
|
||||
class ExclusiveMergeStructureValidationTest {
|
||||
class BranchRejoinStructureValidationTest {
|
||||
|
||||
@Autowired
|
||||
Validator validator;
|
||||
|
|
@ -47,10 +47,10 @@ class ExclusiveMergeStructureValidationTest {
|
|||
HumanInteractiveBlockFactory humanInteractiveBlockFactory;
|
||||
|
||||
@Autowired
|
||||
ExclusiveMergeBlockFactory exclusiveMergeBlockFactory;
|
||||
BranchRejoinBlockFactory branchRejoinBlockFactory;
|
||||
|
||||
@Test
|
||||
void implicitReconvergenceWithoutAnExclusiveMergeBlockIsRejected() {
|
||||
void implicitReconvergenceWithoutAnBranchRejoinBlockIsRejected() {
|
||||
Block<ConditionalBlockType> conditional = conditional();
|
||||
Block<HumanInteractionBlockType> shared = humanInteraction("shared-downstream");
|
||||
|
||||
|
|
@ -67,12 +67,12 @@ class ExclusiveMergeStructureValidationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void reconvergenceThroughAnExplicitExclusiveMergeBlockIsAccepted() {
|
||||
void reconvergenceThroughAnExplicitBranchRejoinBlockIsAccepted() {
|
||||
Block<ConditionalBlockType> conditional = conditional();
|
||||
Block<ExclusiveMergeBlockType> merge = exclusiveMergeBlockFactory.create(
|
||||
ExclusiveMergeBlockConfiguration.builder()
|
||||
Block<BranchRejoinBlockType> merge = branchRejoinBlockFactory.create(
|
||||
BranchRejoinBlockConfiguration.builder()
|
||||
.name("merge")
|
||||
.inputs(List.of(new ExclusiveMergeInput("onTrue"), new ExclusiveMergeInput("onFalse")))
|
||||
.inputs(List.of(new BranchRejoinInput("onTrue"), new BranchRejoinInput("onFalse")))
|
||||
.outputName("output")
|
||||
.build());
|
||||
Block<HumanInteractionBlockType> shared = humanInteraction("shared-downstream");
|
||||
|
|
@ -18,7 +18,7 @@ import it.cnr.isti.workflow.manager.blocks.configurations.HumanInteractiveBlockC
|
|||
import it.cnr.isti.workflow.manager.blocks.factories.EndBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.HumanInteractiveBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.EndBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanInteractionBlockType;
|
||||
import it.cnr.isti.workflow.manager.containers.types.GenericContainerType;
|
||||
|
|
@ -68,8 +68,8 @@ class NodeTypeCapabilitiesIntegrationTest {
|
|||
assertEquals(NodeVisualRole.DECISION, decision.visualRole());
|
||||
assertTrue(decision.biasAnnotationsAllowed());
|
||||
|
||||
var merge = blocksController.getConfigurationDescriptorForType(ExclusiveMergeBlockType.TYPE).capabilities();
|
||||
assertEquals(NodeVisualRole.MERGE, merge.visualRole());
|
||||
var branchRejoin = blocksController.getConfigurationDescriptorForType(BranchRejoinBlockType.TYPE).capabilities();
|
||||
assertEquals(NodeVisualRole.BRANCH_REJOIN, branchRejoin.visualRole());
|
||||
|
||||
var container = containersController.getTypes().stream()
|
||||
.filter(descriptor -> GenericContainerType.TYPE.equals(descriptor.type()))
|
||||
|
|
|
|||
Loading…
Reference in New Issue