feat: add node capabilities and biased seed flow

This commit is contained in:
Lucio Lelii 2026-07-22 17:38:42 +02:00
parent 007bca14fb
commit a0ed4874d8
16 changed files with 641 additions and 1390 deletions

View File

@ -3,6 +3,7 @@ package it.cnr.isti.workflow.manager.blocks.types;
import com.fasterxml.jackson.annotation.JsonIgnore;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
import it.cnr.isti.workflow.manager.flows.model.capabilities.NodeTypeCapabilities;
public interface BlockType {
@ -14,6 +15,10 @@ public interface BlockType {
boolean isUserInteractive();
default NodeTypeCapabilities getCapabilities() {
return NodeTypeCapabilities.activity();
}
@JsonIgnore
Class<? extends BlockConfiguration<?>> getBlockConfigurationClass();

View File

@ -4,6 +4,7 @@ import org.springframework.stereotype.Component;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.ConditionalBlockConfiguration;
import it.cnr.isti.workflow.manager.flows.model.capabilities.NodeTypeCapabilities;
@Component(ConditionalBlockType.TYPE)
public class ConditionalBlockType implements BlockType {
@ -30,6 +31,11 @@ public class ConditionalBlockType implements BlockType {
return false;
}
@Override
public NodeTypeCapabilities getCapabilities() {
return NodeTypeCapabilities.decision();
}
@Override
public Class<? extends BlockConfiguration<?>> getBlockConfigurationClass() {
return ConditionalBlockConfiguration.class;

View File

@ -4,6 +4,7 @@ import org.springframework.stereotype.Component;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.EndBlockConfiguration;
import it.cnr.isti.workflow.manager.flows.model.capabilities.NodeTypeCapabilities;
@Component(EndBlockType.TYPE)
public class EndBlockType implements BlockType {
@ -30,6 +31,11 @@ public class EndBlockType implements BlockType {
return false;
}
@Override
public NodeTypeCapabilities getCapabilities() {
return NodeTypeCapabilities.end();
}
@Override
public Class<? extends BlockConfiguration<?>> getBlockConfigurationClass() {
return EndBlockConfiguration.class;

View File

@ -4,6 +4,7 @@ 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.flows.model.capabilities.NodeTypeCapabilities;
@Component(ExclusiveMergeBlockType.TYPE)
public class ExclusiveMergeBlockType implements BlockType {
@ -30,6 +31,11 @@ public class ExclusiveMergeBlockType implements BlockType {
return false;
}
@Override
public NodeTypeCapabilities getCapabilities() {
return NodeTypeCapabilities.merge();
}
@Override
public Class<? extends BlockConfiguration<?>> getBlockConfigurationClass() {
return ExclusiveMergeBlockConfiguration.class;

View File

@ -4,6 +4,7 @@ import org.springframework.stereotype.Component;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.HumanDecisionBlockConfiguration;
import it.cnr.isti.workflow.manager.flows.model.capabilities.NodeTypeCapabilities;
@Component(HumanDecisionBlockType.TYPE)
public class HumanDecisionBlockType implements BlockType {
@ -30,6 +31,11 @@ public class HumanDecisionBlockType implements BlockType {
return true;
}
@Override
public NodeTypeCapabilities getCapabilities() {
return NodeTypeCapabilities.decision();
}
@Override
public Class<? extends BlockConfiguration<?>> getBlockConfigurationClass() {
return HumanDecisionBlockConfiguration.class;

View File

@ -4,6 +4,7 @@ import org.springframework.stereotype.Component;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.SwitchBlockConfiguration;
import it.cnr.isti.workflow.manager.flows.model.capabilities.NodeTypeCapabilities;
@Component(SwitchBlockType.TYPE)
public class SwitchBlockType implements BlockType {
@ -30,6 +31,11 @@ public class SwitchBlockType implements BlockType {
return false;
}
@Override
public NodeTypeCapabilities getCapabilities() {
return NodeTypeCapabilities.decision();
}
@Override
public Class<? extends BlockConfiguration<?>> getBlockConfigurationClass() {
return SwitchBlockConfiguration.class;

View File

@ -2,6 +2,7 @@ package it.cnr.isti.workflow.manager.containers.types;
import com.fasterxml.jackson.annotation.JsonIgnore;
import it.cnr.isti.workflow.manager.containers.configurations.ContainerConfiguration;
import it.cnr.isti.workflow.manager.flows.model.capabilities.NodeTypeCapabilities;
public interface ContainerType {
@ -13,6 +14,10 @@ public interface ContainerType {
boolean isUserInteractive();
default NodeTypeCapabilities getCapabilities() {
return NodeTypeCapabilities.container();
}
@JsonIgnore
Class<? extends ContainerConfiguration<?>> getContainerConfigurationClass();
}

View File

@ -11,6 +11,7 @@ import it.cnr.isti.workflow.manager.blocks.configurations.JsonSchemaProducer;
import it.cnr.isti.workflow.manager.blocks.factories.BlockFactory;
import it.cnr.isti.workflow.manager.blocks.types.BlockType;
import it.cnr.isti.workflow.manager.executions.design.FlowSharedVariableCatalogService;
import it.cnr.isti.workflow.manager.flows.model.capabilities.NodeTypeCapabilities;
import java.util.List;
import java.util.Map;
@ -55,6 +56,7 @@ public class BlocksController {
String type,
String description,
boolean userInteractive,
NodeTypeCapabilities capabilities,
InteractionContractDescriptor interactionContract,
boolean hasExampleBlock,
String exampleBlockEndpoint,
@ -143,6 +145,7 @@ public class BlocksController {
blockType.getName(),
blockType.getDescription(),
blockType.isUserInteractive(),
blockType.getCapabilities(),
resolveInteractionContract(blockType),
true,
getExampleEndpoint(blockType),
@ -156,6 +159,7 @@ public class BlocksController {
blockType.getName(),
blockType.getDescription(),
blockType.isUserInteractive(),
blockType.getCapabilities(),
resolveInteractionContract(blockType),
true,
getExampleEndpoint(blockType),
@ -174,6 +178,7 @@ public class BlocksController {
descriptor.type(),
descriptor.description(),
descriptor.userInteractive(),
descriptor.capabilities(),
descriptor.interactionContract(),
descriptor.hasExampleBlock(),
descriptor.exampleBlockEndpoint(),

View File

@ -25,6 +25,7 @@ import it.cnr.isti.workflow.manager.containers.types.ContainerType;
import it.cnr.isti.workflow.manager.blocks.configurations.JsonSchemaProducer;
import it.cnr.isti.workflow.manager.blocks.configurations.JsonSchemaCatalogBundler;
import it.cnr.isti.workflow.manager.flows.model.FlowData;
import it.cnr.isti.workflow.manager.flows.model.capabilities.NodeTypeCapabilities;
import it.cnr.isti.workflow.manager.flows.validation.ValidationError;
import it.cnr.isti.workflow.manager.ios.IODescriptor;
import tools.jackson.databind.JsonNode;
@ -60,6 +61,7 @@ public class ContainersController {
String type,
String description,
boolean userInteractive,
NodeTypeCapabilities capabilities,
boolean hasExampleContainer,
String exampleContainerEndpoint,
String configurationType,
@ -194,6 +196,7 @@ public class ContainersController {
containerType.getName(),
containerType.getDescription(),
containerType.isUserInteractive(),
containerType.getCapabilities(),
true,
"/containers/types/" + containerType.getName() + "/example",
configurationClass == null ? null : configurationClass.getSimpleName(),
@ -206,6 +209,7 @@ public class ContainersController {
descriptor.type(),
descriptor.description(),
descriptor.userInteractive(),
descriptor.capabilities(),
descriptor.hasExampleContainer(),
descriptor.exampleContainerEndpoint(),
descriptor.configurationType(),

View File

@ -0,0 +1,35 @@
package it.cnr.isti.workflow.manager.flows.model.capabilities;
public record NodeTypeCapabilities(
NodeVisualRole visualRole,
boolean terminal,
boolean biasAnnotationsAllowed,
boolean allowsIncomingConnections,
boolean allowsOutgoingConnections,
boolean canDependOnOtherNodes,
boolean canHaveDependentNodes) {
public NodeTypeCapabilities {
visualRole = visualRole == null ? NodeVisualRole.ACTIVITY : visualRole;
}
public static NodeTypeCapabilities activity() {
return new NodeTypeCapabilities(NodeVisualRole.ACTIVITY, false, true, true, true, true, true);
}
public static NodeTypeCapabilities decision() {
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 end() {
return new NodeTypeCapabilities(NodeVisualRole.END, true, false, true, false, false, false);
}
public static NodeTypeCapabilities container() {
return new NodeTypeCapabilities(NodeVisualRole.CONTAINER, false, true, true, true, true, true);
}
}

View File

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

View File

@ -36,6 +36,7 @@ import it.cnr.isti.workflow.manager.flows.model.FlowLane;
import it.cnr.isti.workflow.manager.flows.model.FlowNode;
import it.cnr.isti.workflow.manager.flows.model.bias.BlockBiasAnnotation;
import it.cnr.isti.workflow.manager.flows.model.bias.BiasActivationMode;
import it.cnr.isti.workflow.manager.flows.model.capabilities.NodeTypeCapabilities;
import it.cnr.isti.workflow.manager.executions.bias.runtime.BiasBehaviorAdapterRegistry;
import it.cnr.isti.workflow.manager.ios.IODescriptor;
import jakarta.validation.ConstraintValidator;
@ -187,6 +188,10 @@ public class FlowDataValidator implements ConstraintValidator<ValidFlowStructure
private void validateBiasAnnotations(FlowNode node) {
String entity = biasEntityType(node);
List<BlockBiasAnnotation> annotations = node.getBiasAnnotations();
if (!nodeTypeCapabilities(node).biasAnnotationsAllowed() && !annotations.isEmpty()) {
throw validationError(error(ValidationErrorCode.BIAS_ANNOTATIONS_NOT_ALLOWED, entity, node.getId(),
"biasAnnotations", "Bias annotations are not allowed for node type " + node.getClass().getSimpleName()));
}
if (annotations.size() > BlockBiasAnnotation.MAX_PER_NODE) {
throw validationError(error(ValidationErrorCode.TOO_MANY_BIAS_ANNOTATIONS, entity, node.getId(),
"biasAnnotations", "A node cannot contain more than " + BlockBiasAnnotation.MAX_PER_NODE + " bias annotations"));
@ -369,6 +374,16 @@ public class FlowDataValidator implements ConstraintValidator<ValidFlowStructure
return node instanceof Block<?> ? "block" : "container";
}
private NodeTypeCapabilities nodeTypeCapabilities(FlowNode node) {
if (node instanceof Block<?> block) {
return block.getType().getCapabilities();
}
if (node instanceof Container<?> container) {
return container.getType().getCapabilities();
}
return NodeTypeCapabilities.activity();
}
private void validateContainerSubFlow(Container<?> container, ContainerConfiguration<?> containerConfiguration,
String fieldName, FlowData subFlow) {
String fieldPath = "specificConfiguration." + fieldName;
@ -422,6 +437,18 @@ public class FlowDataValidator implements ConstraintValidator<ValidFlowStructure
throw validationError(error(ValidationErrorCode.CONNECTION_TARGET_NODE_NOT_FOUND, "connection", connection.getId(), "targetId", "Target node not found"));
}
if (!nodeTypeCapabilities(sourceNode).allowsOutgoingConnections()) {
throw validationError(error(ValidationErrorCode.NODE_OUTGOING_CONNECTION_NOT_ALLOWED,
biasEntityType(sourceNode), sourceNode.getId(), "outputs",
"Node type does not allow outgoing connections: " + sourceNode.getName()));
}
if (!nodeTypeCapabilities(targetNode).allowsIncomingConnections()) {
throw validationError(error(ValidationErrorCode.NODE_INCOMING_CONNECTION_NOT_ALLOWED,
biasEntityType(targetNode), targetNode.getId(), "inputs",
"Node type does not allow incoming connections: " + targetNode.getName()));
}
if (!hasIoNamed(sourceNode.getOutputs(), connection.getSourceName())) {
throw validationError(error(ValidationErrorCode.CONNECTION_SOURCE_OUTPUT_NOT_FOUND, "connection", connection.getId(), "sourceName", "Source output not found"));
}
@ -457,6 +484,18 @@ public class FlowDataValidator implements ConstraintValidator<ValidFlowStructure
if (!nodesById.containsKey(dependency.getTargetId())) {
throw validationError(error(ValidationErrorCode.DEPENDENCY_TARGET_NODE_NOT_FOUND, "dependency", dependency.getId(), "targetId", "Dependency target node not found"));
}
FlowNode sourceNode = nodesById.get(dependency.getSourceId());
FlowNode targetNode = nodesById.get(dependency.getTargetId());
if (!nodeTypeCapabilities(sourceNode).canHaveDependentNodes()) {
throw validationError(error(ValidationErrorCode.NODE_CANNOT_HAVE_DEPENDENT_NODES,
biasEntityType(sourceNode), sourceNode.getId(), "dependencies",
"Node type cannot be the source of a dependency: " + sourceNode.getName()));
}
if (!nodeTypeCapabilities(targetNode).canDependOnOtherNodes()) {
throw validationError(error(ValidationErrorCode.NODE_CANNOT_DEPEND_ON_OTHER_NODES,
biasEntityType(targetNode), targetNode.getId(), "dependencies",
"Node type cannot be the target of a dependency: " + targetNode.getName()));
}
if (dependency.getSourceId().equals(dependency.getTargetId())) {
throw validationError(error(ValidationErrorCode.DEPENDENCY_SELF_REFERENCE, "dependency", dependency.getId(), "targetId", "A node cannot depend on itself"));
}

View File

@ -9,6 +9,7 @@ public enum ValidationErrorCode {
BLOCK_TYPE_MISMATCH,
BLOCK_INPUTS_MISMATCH,
BLOCK_OUTPUTS_MISMATCH,
BIAS_ANNOTATIONS_NOT_ALLOWED,
TOO_MANY_BIAS_ANNOTATIONS,
NULL_BIAS_ANNOTATION,
DUPLICATE_BIAS_ANNOTATION_ID,
@ -50,6 +51,8 @@ public enum ValidationErrorCode {
CONNECTION_TARGET_NODE_NOT_FOUND,
CONNECTION_SOURCE_OUTPUT_NOT_FOUND,
CONNECTION_TARGET_INPUT_NOT_FOUND,
NODE_INCOMING_CONNECTION_NOT_ALLOWED,
NODE_OUTGOING_CONNECTION_NOT_ALLOWED,
NULL_DEPENDENCY,
DEPENDENCY_SOURCE_REQUIRED,
DEPENDENCY_TARGET_REQUIRED,
@ -57,6 +60,8 @@ public enum ValidationErrorCode {
DEPENDENCY_TARGET_NODE_NOT_FOUND,
DEPENDENCY_SELF_REFERENCE,
DUPLICATE_DEPENDENCY,
NODE_CANNOT_DEPEND_ON_OTHER_NODES,
NODE_CANNOT_HAVE_DEPENDENT_NODES,
EXCLUSIVE_BRANCH_MERGE,
EXCLUSIVE_MERGE_MIN_INPUTS,
EXCLUSIVE_MERGE_INPUT_NOT_CONNECTED,

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,6 @@ import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@ -62,45 +61,38 @@ public class FlowImportComponentTest {
Path tempDir;
@Test
public void bundledJensenHiringFlowsDeserializeAndValidate() throws Exception {
public void bundledTestBiasedFlowDeserializesAndValidates() throws Exception {
Path bundledFlows = Path.of("src/main/resources/workflow-editor-init/flows.json");
List<ImportedFlow> flows = ObjectMapperHolder.mapper.readValue(
bundledFlows.toFile(),
ObjectMapperHolder.mapper.getTypeFactory().constructCollectionType(List.class, ImportedFlow.class));
ImportedFlow fullRevised = flows.stream()
.filter(flow -> "Jensen Recruitment Process - Full Revised".equals(flow.name()))
.findFirst()
.orElseThrow();
ImportedFlow mitigated = flows.stream()
.filter(flow -> "Jensen Recruitment Process - Mitigated".equals(flow.name()))
assertFalse(flows.stream().anyMatch(flow -> "Jensen Recruitment Process - Full Revised".equals(flow.name())));
assertFalse(flows.stream().anyMatch(flow -> "Jensen Recruitment Process - Mitigated".equals(flow.name())));
ImportedFlow testBiased = flows.stream()
.filter(flow -> "test biased".equals(flow.name()))
.findFirst()
.orElseThrow();
for (ImportedFlow flow : List.of(fullRevised, mitigated)) {
Set<ConstraintViolation<ImportedFlow>> importErrors = validator.validate(flow);
Set<ConstraintViolation<FlowData>> structureErrors = validator.validate(flow.flow());
assertTrue(importErrors.isEmpty(), () -> "Import errors for " + flow.name() + ": " + importErrors);
assertTrue(structureErrors.isEmpty(), () -> "Structure errors for " + flow.name() + ": " + structureErrors);
assertTrue(flowExecutionValidator.isExecutable(flow.flow()),
() -> "Execution errors for " + flow.name() + ": " + flowExecutionValidator.collectErrors(flow.flow()));
}
Set<ConstraintViolation<ImportedFlow>> importErrors = validator.validate(testBiased);
Set<ConstraintViolation<FlowData>> structureErrors = validator.validate(testBiased.flow());
assertTrue(importErrors.isEmpty(), () -> "Import errors for test biased: " + importErrors);
assertTrue(structureErrors.isEmpty(), () -> "Structure errors for test biased: " + structureErrors);
assertTrue(flowExecutionValidator.isExecutable(testBiased.flow()),
() -> "Execution errors for test biased: " + flowExecutionValidator.collectErrors(testBiased.flow()));
assertEquals(2, fullRevised.flow().getContainers().size());
assertEquals(3, mitigated.flow().getContainers().size());
assertFalse(mitigated.flow().getContainers().stream()
.flatMap(container -> container.getBiasAnnotations().stream())
.toList()
.isEmpty());
Set<String> analysisIds = mitigated.flow().getNodes().stream()
assertEquals(6, testBiased.flow().getBlocks().size());
assertEquals(3, testBiased.flow().getNodes().stream()
.flatMap(node -> node.getBiasAnnotations().stream())
.map(BlockBiasAnnotation::analysisId)
.filter(id -> id != null && !id.isBlank())
.collect(Collectors.toSet());
Set<String> expectedAnalysisIds = java.util.stream.IntStream.rangeClosed(1, 16)
.mapToObj(index -> "FH-J" + index)
.collect(Collectors.toSet());
assertEquals(expectedAnalysisIds, analysisIds);
.count());
assertEquals(2, testBiased.flow().getNodes().stream()
.flatMap(node -> node.getBiasAnnotations().stream())
.map(BlockBiasAnnotation::behavioralProbe)
.filter(java.util.Objects::nonNull)
.count());
assertEquals(1, testBiased.flow().getBlocks().stream()
.filter(block -> block.getOutputs().size() == 2)
.count());
}
@Test
@ -112,7 +104,7 @@ public class FlowImportComponentTest {
List<ImportedFlow> jensenFlows = flows.stream()
.filter(flow -> flow.name().startsWith("Jensen Recruitment Process - "))
.toList();
assertEquals(4, jensenFlows.size());
assertEquals(2, jensenFlows.size());
for (ImportedFlow flow : jensenFlows) {
var firstBlock = flow.flow().getBlocks().stream()

View File

@ -0,0 +1,197 @@
package it.cnr.isti.workflow.manager.flows.validation;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
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.EndMode;
import it.cnr.isti.workflow.manager.blocks.configurations.HumanInteractiveBlockConfiguration;
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.HumanDecisionBlockType;
import it.cnr.isti.workflow.manager.blocks.types.HumanInteractionBlockType;
import it.cnr.isti.workflow.manager.containers.types.GenericContainerType;
import it.cnr.isti.workflow.manager.controllers.BlocksController;
import it.cnr.isti.workflow.manager.controllers.ContainersController;
import it.cnr.isti.workflow.manager.flows.model.Connection;
import it.cnr.isti.workflow.manager.flows.model.Dependency;
import it.cnr.isti.workflow.manager.flows.model.FlowData;
import it.cnr.isti.workflow.manager.flows.model.bias.BiasCategory;
import it.cnr.isti.workflow.manager.flows.model.bias.BiasSeverity;
import it.cnr.isti.workflow.manager.flows.model.bias.BlockBiasAnnotation;
import it.cnr.isti.workflow.manager.flows.model.capabilities.NodeVisualRole;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
class NodeTypeCapabilitiesIntegrationTest {
@Autowired
BlocksController blocksController;
@Autowired
ContainersController containersController;
@Autowired
EndBlockFactory endBlockFactory;
@Autowired
HumanInteractiveBlockFactory humanInteractiveBlockFactory;
@Autowired
Validator validator;
@Test
void catalogsExposeVisualAndStructuralCapabilities() {
var end = blocksController.getConfigurationDescriptorForType(EndBlockType.TYPE).capabilities();
assertEquals(NodeVisualRole.END, end.visualRole());
assertTrue(end.terminal());
assertFalse(end.biasAnnotationsAllowed());
assertTrue(end.allowsIncomingConnections());
assertFalse(end.allowsOutgoingConnections());
assertFalse(end.canDependOnOtherNodes());
assertFalse(end.canHaveDependentNodes());
var decision = blocksController.getConfigurationDescriptorForType(HumanDecisionBlockType.TYPE).capabilities();
assertEquals(NodeVisualRole.DECISION, decision.visualRole());
assertTrue(decision.biasAnnotationsAllowed());
var merge = blocksController.getConfigurationDescriptorForType(ExclusiveMergeBlockType.TYPE).capabilities();
assertEquals(NodeVisualRole.MERGE, merge.visualRole());
var container = containersController.getTypes().stream()
.filter(descriptor -> GenericContainerType.TYPE.equals(descriptor.type()))
.findFirst().orElseThrow().capabilities();
assertEquals(NodeVisualRole.CONTAINER, container.visualRole());
var compactEnd = blocksController.getTypeCatalog().descriptors().stream()
.filter(descriptor -> EndBlockType.TYPE.equals(descriptor.type()))
.findFirst().orElseThrow().capabilities();
assertEquals(end, compactEnd);
var compactContainer = containersController.getTypeCatalog().descriptors().stream()
.filter(descriptor -> GenericContainerType.TYPE.equals(descriptor.type()))
.findFirst().orElseThrow().capabilities();
assertEquals(container, compactContainer);
}
@Test
void endBlockRejectsBiasAnnotations() {
Block<EndBlockType> original = end();
Block<EndBlockType> annotated = Block.<EndBlockType>builder()
.specificConfiguration(original.getSpecificConfiguration())
.inputs(original.getInputs())
.outputs(original.getOutputs())
.type(original.getType())
.biasAnnotation(new BlockBiasAnnotation(
"end-risk", BiasCategory.TRANSPARENCY_RISK, BiasSeverity.LOW,
"Terminal marker risk", null, null, null, null, null, null))
.build();
assertEquals(List.of(ValidationErrorCode.BIAS_ANNOTATIONS_NOT_ALLOWED),
errorCodes(FlowData.builder().block(annotated).build()));
}
@Test
void endBlockCannotDependOnAnotherNode() {
Block<HumanInteractionBlockType> activity = activity();
Block<EndBlockType> end = end();
FlowData flow = FlowData.builder()
.block(activity)
.block(end)
.dependency(Dependency.builder().sourceId(activity.getId()).targetId(end.getId()).build())
.build();
assertEquals(List.of(ValidationErrorCode.NODE_CANNOT_DEPEND_ON_OTHER_NODES), errorCodes(flow));
}
@Test
void endBlockCannotHaveDependentNodes() {
Block<EndBlockType> end = end();
Block<HumanInteractionBlockType> activity = activity();
FlowData flow = FlowData.builder()
.block(end)
.block(activity)
.dependency(Dependency.builder().sourceId(end.getId()).targetId(activity.getId()).build())
.build();
assertEquals(List.of(ValidationErrorCode.NODE_CANNOT_HAVE_DEPENDENT_NODES), errorCodes(flow));
}
@Test
void endBlockStillAcceptsOneIncomingFlowConnection() {
Block<HumanInteractionBlockType> activity = activity();
Block<EndBlockType> end = end();
FlowData flow = FlowData.builder()
.block(activity)
.block(end)
.connection(Connection.builder()
.sourceId(activity.getId())
.sourceName("output")
.targetId(end.getId())
.targetName(EndBlockFactory.INPUT_NAME)
.build())
.build();
assertTrue(validate(flow).isEmpty(), () -> "Unexpected validation errors: " + validate(flow));
}
@Test
void endBlockRejectsOutgoingFlowConnections() {
Block<EndBlockType> end = end();
Block<HumanInteractionBlockType> activity = activity();
FlowData flow = FlowData.builder()
.block(end)
.block(activity)
.connection(Connection.builder()
.sourceId(end.getId())
.sourceName("output")
.targetId(activity.getId())
.targetName("input")
.build())
.build();
assertEquals(List.of(ValidationErrorCode.NODE_OUTGOING_CONNECTION_NOT_ALLOWED), errorCodes(flow));
}
private Block<EndBlockType> end() {
return endBlockFactory.create(EndBlockConfiguration.builder()
.name("Completed")
.outcomeCode("COMPLETED")
.outcomeLabel("Completed")
.mode(EndMode.PATH_END)
.build());
}
private Block<HumanInteractionBlockType> activity() {
return humanInteractiveBlockFactory.create(HumanInteractiveBlockConfiguration.builder()
.name("Review")
.actionDescription("Review the dossier")
.build());
}
private List<ValidationError> validate(FlowData flow) {
Set<ConstraintViolation<FlowData>> violations = validator.validate(flow);
if (violations.isEmpty()) {
return List.of();
}
return ValidationErrorCodec.decode(violations.iterator().next().getMessage());
}
private List<ValidationErrorCode> errorCodes(FlowData flow) {
return validate(flow).stream().map(ValidationError::code).toList();
}
}