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:
Lucio Lelii 2026-07-24 14:17:28 +02:00
parent 98cf28bd9f
commit 1fff0d3dc8
25 changed files with 245 additions and 171 deletions

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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() {

View File

@ -3,7 +3,7 @@ package it.cnr.isti.workflow.manager.flows.model.capabilities;
public enum NodeVisualRole {
ACTIVITY,
DECISION,
MERGE,
BRANCH_REJOIN,
END,
CONTAINER
}

View File

@ -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()));
}

View File

@ -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,

View File

@ -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",

View File

@ -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();

View File

@ -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());
}
}

View File

@ -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()

View File

@ -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());

View File

@ -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()

View File

@ -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));
}

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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");
}

View File

@ -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");

View File

@ -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()))