Keep isFromGlobalInput out of the payload it is not part of

Jackson reads an is-prefixed no-arg boolean as a property, so the helper
added with the field put a "fromGlobalInput" into every block payload and
every persisted flow - a key the generated schema never declares, sitting
next to the one it is derived from. ModelParameters.isEmpty carries the
same guard for the same reason.

Also cover the two steps between this field and the code that reads it:
the generated schema has to carry it, and a configuration posted back by
the editor has to survive deserialization into a block whose ports reflect
the choice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-14 14:11:54 +02:00
parent f4693ac6ab
commit 1d5d1eb78b
3 changed files with 111 additions and 1 deletions

View File

@ -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.
*
* <p>{@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();
}

View File

@ -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<MCPAgentBlockType> block = blocksController.create(configuration, null, null);
assertNotNull(block);
assertTrue(block.getInputs().stream().noneMatch(input -> input.getName().equals("planDoc")));
}
}

View File

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