diff --git a/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/MCPAgentUploadInput.java b/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/MCPAgentUploadInput.java index 4fa9852..da27aa0 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/MCPAgentUploadInput.java +++ b/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/MCPAgentUploadInput.java @@ -1,5 +1,6 @@ package it.cnr.isti.workflow.manager.blocks.configurations; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import it.cnr.isti.workflow.manager.configurations.annotations.FieldRetriever; @@ -40,7 +41,15 @@ public record MCPAgentUploadInput( this(name, kind, multiple, null); } - /** Whether this attachment comes from a global input rather than a port of its own. */ + /** + * Whether this attachment comes from a global input rather than a port of its own. + * + *

{@code @JsonIgnore} because Jackson reads an is-prefixed no-arg boolean as a property: + * without it a "fromGlobalInput" field appeared in every API payload and every persisted flow, + * in a shape the schema does not declare - the same trap {@code ModelParameters.isEmpty} carries + * the same guard for. + */ + @JsonIgnore public boolean isFromGlobalInput() { return globalInput != null && !globalInput.isBlank(); } diff --git a/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/MCPAgentUploadGlobalSchemaTest.java b/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/MCPAgentUploadGlobalSchemaTest.java new file mode 100644 index 0000000..88e44ba --- /dev/null +++ b/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/MCPAgentUploadGlobalSchemaTest.java @@ -0,0 +1,55 @@ +package it.cnr.isti.workflow.manager.blocks.configurations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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 tools.jackson.databind.JsonNode; + +import it.cnr.isti.workflow.manager.app.ObjectMapperHolder; +import it.cnr.isti.workflow.manager.blocks.Block; +import it.cnr.isti.workflow.manager.blocks.types.MCPAgentBlockType; +import it.cnr.isti.workflow.manager.controllers.BlocksController; + +@SpringBootTest +@TestPropertySource(locations = "classpath:test.properties") +public class MCPAgentUploadGlobalSchemaTest { + + @Autowired + private JsonSchemaProducer schemaProducer; + + @Autowired + private BlocksController blocksController; + + @Test + public void theEditorIsToldAnUploadCanNameAGlobal() { + JsonNode schema = schemaProducer.generateSchemaNode(MCPAgentBlockConfiguration.class); + assertNotNull(schema); + String asText = schema.toString(); + assertTrue(asText.contains("globalInput"), "the upload input schema must carry the new field: " + asText); + } + + @Test + public void aBlockWhoseUploadNamesAGlobalSurvivesTheRoundTripTheEditorMakes() { + // Exactly what the editor posts back when the field is filled from the dropdown. + String payload = """ + {"type":"MCPAgentBlockConfiguration","name":"Read the plan","model":"llama3.1:8b", + "prompt":"Summarize the attached plan","mcpServers":[], + "uploadInputs":[{"name":"planDoc","kind":"DOCUMENT","multiple":false,"globalInput":"document"}], + "shareSession":false,"useSharedSession":false} + """; + MCPAgentBlockConfiguration configuration = + ObjectMapperHolder.mapper.readValue(payload, MCPAgentBlockConfiguration.class); + assertEquals("document", configuration.getUploadInputs().getFirst().globalInput()); + + Block block = blocksController.create(configuration, null, null); + + assertNotNull(block); + assertTrue(block.getInputs().stream().noneMatch(input -> input.getName().equals("planDoc"))); + } +} diff --git a/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/MCPAgentUploadInputJsonTest.java b/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/MCPAgentUploadInputJsonTest.java new file mode 100644 index 0000000..4f08df8 --- /dev/null +++ b/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/MCPAgentUploadInputJsonTest.java @@ -0,0 +1,46 @@ +package it.cnr.isti.workflow.manager.blocks.configurations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; + +public class MCPAgentUploadInputJsonTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + public void readsAnUploadInputThatNamesAGlobal() { + MCPAgentUploadInput upload = mapper.readValue( + "{\"name\":\"planDoc\",\"kind\":\"DOCUMENT\",\"multiple\":false,\"globalInput\":\"document\"}", + MCPAgentUploadInput.class); + + assertEquals("planDoc", upload.name()); + assertEquals("document", upload.globalInput()); + } + + @Test + public void readsAnUploadInputSavedBeforeGlobalsWereAnOption() { + MCPAgentUploadInput upload = mapper.readValue( + "{\"name\":\"planDoc\",\"kind\":\"DOCUMENT\",\"multiple\":false}", + MCPAgentUploadInput.class); + + assertEquals("planDoc", upload.name()); + assertNull(upload.globalInput()); + } + + @Test + public void doesNotInventAFieldTheSchemaNeverDeclared() { + // Jackson reads an is-prefixed no-arg boolean as a property, so isFromGlobalInput() would + // otherwise put a "fromGlobalInput" into every payload and every persisted flow. + String written = mapper.writeValueAsString( + new MCPAgentUploadInput("planDoc", MCPAgentUploadInput.MCPAgentUploadKind.DOCUMENT, false, "document")); + + assertFalse(written.contains("fromGlobalInput"), written); + assertTrue(written.contains("globalInput"), written); + } +}