feat: add node capabilities and biased seed flow
This commit is contained in:
parent
007bca14fb
commit
a0ed4874d8
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package it.cnr.isti.workflow.manager.flows.model.capabilities;
|
||||
|
||||
public enum NodeVisualRole {
|
||||
ACTIVITY,
|
||||
DECISION,
|
||||
MERGE,
|
||||
END,
|
||||
CONTAINER
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue