From b29712eed86b574a3ee3bfb854b08a5614ae2ec8 Mon Sep 17 00:00:00 2001 From: LautaroPetaccio Date: Sun, 16 Aug 2026 18:09:40 -0300 Subject: [PATCH 1/7] AsyncAPI 3.x: the driver contract for publishing a message Everything so far is in core and reads a document. This is the other side: what a driver must provide so that the core can drive an AsyncAPI service without knowing anything about brokers. AsyncApiProblem declares that the SUT is driven by messages. It carries only the document -- a location to fetch it from, or the text itself. The connection to the broker stays in the driver and never crosses, which is what keeps the core free of any broker library. Note it may be declared with the document inline, which REST has no real need for. A REST service usually serves its own contract over HTTP; a service that speaks only Kafka has no endpoint to serve anything from, so its document is far more likely to be a file shipped beside it. SutController.executeAsyncApiAction is where a driver publishes one message and, when a reply is expected, waits for the one that answers it. It is the counterpart of executeAction for RPC. It has a default that throws rather than being abstract, so that adding it does not break every existing driver; the message says what to override. Only publish and await are protocol-specific, and they never leave the driver. The driver is asked to report what happened, not to judge it: AsyncApiReplyDto distinguishes published-with-no-reply-expected, a reply that arrived, silence within the window, and a failure to publish at all. The last of those is a broken setup rather than a finding about the service, which is why it is kept apart from silence. Deciding what an outcome means is the core's job, so that it means the same thing whatever the transport. Also carried in the reply is whether the correlation id came back. Whether correlation works cannot be read off a contract, since echoing the id is the service's own behaviour, so it is established by watching for it. Wiring: a field on SutInfoDto, a field on ActionDto, and two branches in EMController. --- .../java/controller/api/dto/ActionDto.java | 7 + .../java/controller/api/dto/SutInfoDto.java | 7 + .../api/dto/problem/AsyncApiProblemDto.java | 26 ++++ .../problem/asyncapi/AsyncApiActionDto.java | 93 +++++++++++ .../problem/asyncapi/AsyncApiReplyDto.java | 75 +++++++++ .../controller/internal/EMController.java | 30 ++++ .../controller/internal/SutController.java | 37 +++++ .../controller/problem/AsyncApiProblem.java | 69 +++++++++ .../problem/AsyncApiProblemTest.java | 144 ++++++++++++++++++ 9 files changed, 488 insertions(+) create mode 100644 client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/AsyncApiProblemDto.java create mode 100644 client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java create mode 100644 client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/problem/AsyncApiProblem.java create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/problem/AsyncApiProblemTest.java diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/ActionDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/ActionDto.java index 9f8f1c202d..7b8f00168e 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/ActionDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/ActionDto.java @@ -1,6 +1,7 @@ package org.evomaster.client.java.controller.api.dto; import org.evomaster.client.java.controller.api.dto.problem.ExternalServiceDto; +import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiActionDto; import org.evomaster.client.java.controller.api.dto.problem.rpc.RPCActionDto; import org.evomaster.client.java.controller.api.dto.problem.rpc.ScheduleTaskInvocationDto; @@ -39,6 +40,12 @@ public class ActionDto { * note that this is only used when handling RPC problem */ public RPCActionDto rpcCall; + + /** + * info to publish a message. + * note that this is only used when handling an AsyncAPI problem + */ + public AsyncApiActionDto asyncApiCall; /** diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/SutInfoDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/SutInfoDto.java index ea50070eea..b305be4331 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/SutInfoDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/SutInfoDto.java @@ -6,6 +6,7 @@ import org.evomaster.client.java.controller.api.dto.problem.RestProblemDto; import org.evomaster.client.java.controller.api.dto.problem.GraphQLProblemDto; import org.evomaster.client.java.controller.api.dto.problem.WebProblemDto; +import org.evomaster.client.java.controller.api.dto.problem.AsyncApiProblemDto; import java.util.List; @@ -35,6 +36,12 @@ public class SutInfoDto { */ public WebProblemDto webProblem; + /** + * If the SUT is a service driven by messages and described by an AsyncAPI document, + * here there will be the info on how to interact with it + */ + public AsyncApiProblemDto asyncApiProblem; + /** * Whether the SUT is running or not */ diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/AsyncApiProblemDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/AsyncApiProblemDto.java new file mode 100644 index 0000000000..ee34b8b079 --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/AsyncApiProblemDto.java @@ -0,0 +1,26 @@ +package org.evomaster.client.java.controller.api.dto.problem; + +/** + * Info the driver gives about an AsyncAPI service, so that the core can learn what the service + * consumes and what shape those messages have. + * + * Note the transport client is not here, and never crosses: it is an open connection to a + * broker, held by the driver. Only the document travels. + */ +public class AsyncApiProblemDto extends ProblemInfoDto { + + /** + * Where the AsyncAPI document can be fetched from: a URL, or a path on the machine running + * the driver. Null when the document is given inline instead. + */ + public String schemaLocation; + + /** + * The AsyncAPI document itself. Null when a location is given instead. + * + * Useful when the document is packaged with the service rather than served by it, which is + * the common case: unlike OpenAPI, an AsyncAPI service rarely exposes its own contract over + * HTTP, since it may not speak HTTP at all. + */ + public String schemaText; +} diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java new file mode 100644 index 0000000000..06158e24b2 --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java @@ -0,0 +1,93 @@ +package org.evomaster.client.java.controller.api.dto.problem.asyncapi; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * One message for the driver to publish, and what to do about a reply. + * + * Everything here is decided by the core: which operation, where it goes, what it says. The + * driver's job is to put it on the wire and, when a reply is expected, wait for the one that + * answers it. + */ +public class AsyncApiActionDto { + + /** + * Key of the operation in the AsyncAPI document. Sent along so the driver can report and + * log in terms the user will recognise from their own contract. + */ + public String operationId; + + /** + * Key of the channel the message is published on. + */ + public String channelName; + + /** + * Where the message actually goes on the wire: a topic, a queue, a routing key. Already + * resolved by the core, including any binding that overrides the channel's address. + */ + public String address; + + /** + * Id of the message being published, as the document names it. + */ + public String messageId; + + /** + * The message body, serialised. Its content type is in {@link #contentType}. + */ + public String payload; + + /** + * What the document declares the payload is encoded as, eg "application/json". + */ + public String contentType; + + /** + * Headers to publish alongside the body, for a transport that has them. + */ + public Map headers = new LinkedHashMap<>(); + + /** + * The value stamped into this message so that a reply can be recognised as answering it. + * + * It is minted fresh by the core for every execution rather than being part of the message + * the search varies: pairing needs a value unique to the execution, and the service only + * echoes it back. + */ + public String correlationId; + + /** + * Where the correlation id has to be written, as the document declares it. One of + * {@link #CORRELATION_IN_HEADER} or {@link #CORRELATION_IN_PAYLOAD}, or null when the + * document says nothing, in which case it is up to the driver to decide -- a transport with + * native correlation should use it. + */ + public String correlationLocation; + + /** + * JSON Pointer to the field the correlation id goes in, within whatever + * {@link #correlationLocation} names. Null when there is no declared location. + */ + public String correlationPointer; + + /** + * Where a reply is expected to arrive, when the operation declares one. Null for a + * fire-and-forget operation, in which case the driver publishes and returns. + */ + public String replyAddress; + + /** + * How long to wait for a reply before giving up, in milliseconds. + * + * There is no right answer here: a slow service and a stuck one look the same from outside, + * so this is a tuning parameter with no equivalent in a synchronous protocol. It is set + * generously and reported with the result. + */ + public Long replyTimeoutMs; + + public static final String CORRELATION_IN_HEADER = "HEADER"; + + public static final String CORRELATION_IN_PAYLOAD = "PAYLOAD"; +} diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java new file mode 100644 index 0000000000..5130169ba9 --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java @@ -0,0 +1,75 @@ +package org.evomaster.client.java.controller.api.dto.problem.asyncapi; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * What came of publishing one message. + * + * The four outcomes are deliberately distinguished, because they mean different things and only + * one of them is a fault: + * + * + */ +public class AsyncApiReplyDto { + + /** + * The index of the action this answers, echoing what was asked. + */ + public Integer index; + + /** + * Whether the message reached the broker. False means the driver could not publish, and + * {@link #errorMessage} says why. + */ + public boolean published; + + /** + * Whether a reply arrived and was recognised as answering this message. + */ + public boolean replyReceived; + + /** + * Whether the driver waited for a reply at all. False for a fire-and-forget operation, so + * that the absence of a reply is not mistaken for silence in answer to a promise. + */ + public boolean replyExpected; + + /** + * The reply body, as it arrived. + */ + public String replyPayload; + + /** + * The reply's headers, for a transport that has them. + */ + public Map replyHeaders = new LinkedHashMap<>(); + + /** + * Whether the reply carried back the correlation id that was stamped on the request. + * + * This is the honest answer to "did correlation work", which cannot be read off a contract: + * echoing the id is the service's own behaviour. A reply that arrives without it is + * recorded rather than treated as a fault, since from outside there is no telling a defect + * from a service that correlates by some business key instead. + */ + public boolean correlationMatched; + + /** + * How long the driver waited, in milliseconds, whether or not anything arrived. Reported + * because the verdict on silence is only meaningful alongside how long it was waited for. + */ + public Long waitedMs; + + /** + * Why publishing failed, when it did. + */ + public String errorMessage; +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/EMController.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/EMController.java index 5951d369d9..97c789e324 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/EMController.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/EMController.java @@ -5,6 +5,7 @@ import org.evomaster.client.java.controller.api.dto.*; import org.evomaster.client.java.controller.api.dto.database.operations.*; import org.evomaster.client.java.controller.api.dto.problem.*; +import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiReplyDto; import org.evomaster.client.java.controller.api.dto.problem.param.DeriveParamResponseDto; import org.evomaster.client.java.controller.api.dto.problem.param.DerivedParamChangeReqDto; import org.evomaster.client.java.controller.api.dto.problem.param.RestDerivedParamDto; @@ -235,6 +236,13 @@ public Response getSutInfo(@Context HttpServletRequest httpServletRequest) { SimpleLogger.error(msg, e); return Response.status(500).entity(WrappedResponseDto.withError(msg)).build(); } + } else if (info instanceof AsyncApiProblem) { + AsyncApiProblem p = (AsyncApiProblem) info; + dto.asyncApiProblem = new AsyncApiProblemDto(); + dto.asyncApiProblem.schemaLocation = p.getSchemaLocation(); + dto.asyncApiProblem.schemaText = p.getSchemaText(); + dto.asyncApiProblem.servicesToNotMock = servicesToNotMock; + } else if(info instanceof WebProblem){ WebProblem p = (WebProblem) info; dto.webProblem = new WebProblemDto(); @@ -830,6 +838,28 @@ public Response newAction( } } + + if (dto.asyncApiCall != null) { + + AsyncApiReplyDto replyDto = new AsyncApiReplyDto(); + replyDto.index = index; + + try { + sutController.executeAsyncApiAction(dto.asyncApiCall, replyDto); + return Response.status(200).entity(WrappedResponseDto.withData(replyDto)).build(); + } catch (Exception e) { + /* + Failing to publish is not a finding about the service, it is a broken + setup, so it is reported as such rather than as silence in answer to a + promised reply. + */ + String msg = "Thrown exception when publishing a message: " + e.getMessage(); + SimpleLogger.error(msg, e); + replyDto.published = false; + replyDto.errorMessage = msg; + return Response.status(500).entity(WrappedResponseDto.withData(replyDto)).build(); + } + } } return Response.status(204).entity(WrappedResponseDto.withNoData()).build(); diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/SutController.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/SutController.java index 053b1a8e8b..684dd9f878 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/SutController.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/SutController.java @@ -23,6 +23,8 @@ import org.evomaster.client.java.controller.api.dto.MockDatabaseDto; import org.evomaster.client.java.controller.api.dto.database.schema.TableIdDto; import org.evomaster.client.java.controller.api.dto.problem.RPCProblemDto; +import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiActionDto; +import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiReplyDto; import org.evomaster.client.java.controller.api.dto.problem.rpc.*; import org.evomaster.client.java.controller.api.dto.problem.rpc.RPCTestDto; import org.evomaster.client.java.controller.internal.db.OpenSearchHandler; @@ -1929,6 +1931,41 @@ public boolean isScheduleTaskCompleted(ScheduleTaskInvocationResultDto invocatio return false; } + /** + * Publish one message, and wait for the reply that answers it when one is expected. + * + * This is where a driver for an AsyncAPI service does its work, and it is the counterpart + * of {@link #executeAction(RPCActionDto, ActionResponseDto)} for RPC: the core decides + * what to send and reads what comes back, while everything that knows about a broker lives + * on this side. Only publish and await are protocol-specific, and they never leave here. + * + * A driver that does not test an AsyncAPI service has no reason to override this. + * + * Sketch of what an implementation does, for a transport whose correlation rides in + * metadata: + * + *
+     * publish(dto.address, dto.payload, dto.headers + {correlationId: dto.correlationId});
+     * reply.published = true;
+     * if (dto.replyAddress != null) {
+     *     reply.replyExpected = true;
+     *     awaitOn(dto.replyAddress, matching dto.correlationId, within dto.replyTimeoutMs);
+     * }
+     * 
+ * + * Note what is not asked of the driver: it does not judge the reply, only reports it. + * Deciding what an outcome means is the core's job, so that it means the same thing + * whatever the transport. + * + * @param dto what to publish, and where a reply is expected + * @param reply to be filled in with what happened + */ + public void executeAsyncApiAction(AsyncApiActionDto dto, AsyncApiReplyDto reply) { + throw new IllegalStateException( + "Trying to publish a message, but this driver does not implement" + + " executeAsyncApiAction. It must be overridden to test an AsyncAPI service."); + } + @Override public void resetDatabase(List tablesToClean) { diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/problem/AsyncApiProblem.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/problem/AsyncApiProblem.java new file mode 100644 index 0000000000..a1ad115354 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/problem/AsyncApiProblem.java @@ -0,0 +1,69 @@ +package org.evomaster.client.java.controller.problem; + +import java.util.List; +import java.util.Objects; + +/** + * Declares that the SUT is a service driven by messages, described by an AsyncAPI document. + * + * AsyncAPI is the event-driven counterpart of OpenAPI: it describes services that talk over a + * broker or a socket rather than over HTTP. That difference is why a driver is needed even for + * black-box testing, which is not so for REST or GraphQL. There is no universal wire to point + * at -- Kafka, AMQP, MQTT and WebSocket share nothing at the API level -- so something has to + * hold a client and move the bytes, and that something is the driver. + * + * The document is the only thing that crosses to the core. The connection to the broker stays + * here, in the driver, which is what keeps the core from depending on any broker library. + */ +public class AsyncApiProblem extends ProblemInfo { + + private final String schemaLocation; + + private final String schemaText; + + /** + * @param schemaLocation where the AsyncAPI document can be fetched from: a URL, or a path + * on the machine running the driver + */ + public AsyncApiProblem(String schemaLocation) { + this(schemaLocation, null); + } + + private AsyncApiProblem(String schemaLocation, String schemaText) { + + if ((schemaLocation == null) == (schemaText == null)) { + throw new IllegalArgumentException( + "An AsyncAPI problem needs exactly one of a schema location and a schema text"); + } + + this.schemaLocation = schemaLocation; + this.schemaText = schemaText; + } + + /** + * Declare the problem with the document itself rather than somewhere to fetch it from. + * + * This is the common case, and the difference from OpenAPI is worth stating: a REST service + * usually serves its own contract over HTTP, whereas a service that speaks only Kafka has + * no endpoint to serve anything from. Its document is far more likely to be a file shipped + * beside it. + */ + public static AsyncApiProblem fromSchemaText(String schemaText) { + return new AsyncApiProblem(null, Objects.requireNonNull(schemaText)); + } + + public String getSchemaLocation() { + return schemaLocation; + } + + public String getSchemaText() { + return schemaText; + } + + @Override + public AsyncApiProblem withServicesToNotMock(List servicesToNotMock) { + AsyncApiProblem p = new AsyncApiProblem(this.schemaLocation, this.schemaText); + p.servicesToNotMock.addAll(servicesToNotMock); + return p; + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/problem/AsyncApiProblemTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/problem/AsyncApiProblemTest.java new file mode 100644 index 0000000000..7e2fbe2fdc --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/problem/AsyncApiProblemTest.java @@ -0,0 +1,144 @@ +package org.evomaster.client.java.controller.problem; + +import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiActionDto; +import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiReplyDto; +import org.evomaster.client.java.controller.EmbeddedSutController; +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto; +import org.evomaster.client.java.controller.api.dto.SutInfoDto; +import org.evomaster.client.java.controller.internal.SutController; +import org.evomaster.client.java.sql.DbSpecification; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class AsyncApiProblemTest { + + @Test + public void testDeclaredWithSomewhereToFetchTheDocumentFrom() { + + AsyncApiProblem p = new AsyncApiProblem("http://localhost:8080/asyncapi.yaml"); + + assertEquals("http://localhost:8080/asyncapi.yaml", p.getSchemaLocation()); + assertNull(p.getSchemaText()); + } + + @Test + public void testDeclaredWithTheDocumentItself() { + + /* + The common case, and the difference from OpenAPI: a service that speaks only Kafka + has no HTTP endpoint to serve its own contract from, so the document is far more + likely to be a file shipped beside it than a URL. + */ + AsyncApiProblem p = AsyncApiProblem.fromSchemaText("asyncapi: 3.0.0"); + + assertEquals("asyncapi: 3.0.0", p.getSchemaText()); + assertNull(p.getSchemaLocation()); + } + + @Test + public void testTheDocumentHasToComeFromExactlyOnePlace() { + + //neither + assertThrows(IllegalArgumentException.class, () -> new AsyncApiProblem(null)); + + //and null is not a document + assertThrows(NullPointerException.class, () -> AsyncApiProblem.fromSchemaText(null)); + } + + @Test + public void testServicesToNotMockAreCarriedOver() { + + List services = + Arrays.asList(new ExternalService("foo.com", 80), new ExternalService("bar.com", 443)); + + AsyncApiProblem p = AsyncApiProblem.fromSchemaText("asyncapi: 3.0.0") + .withServicesToNotMock(services); + + assertEquals(2, p.getServicesToNotMock().size()); + assertEquals("asyncapi: 3.0.0", p.getSchemaText()); + } + + @Test + public void testServicesToNotMockCannotBeChangedFromOutside() { + + AsyncApiProblem p = AsyncApiProblem.fromSchemaText("asyncapi: 3.0.0") + .withServicesToNotMock(Collections.singletonList(new ExternalService("foo.com", 80))); + + assertThrows( + UnsupportedOperationException.class, + () -> p.getServicesToNotMock().add(new ExternalService("bar.com", 80)) + ); + } + + @Test + public void testADriverThatDoesNotPublishSaysSoClearly() { + + /* + The hook has a default rather than being abstract, so that adding it does not break + every existing driver. A driver that is pointed at an AsyncAPI service without + implementing it should fail with something that explains what to do. + */ + SutController controller = new AsyncApiControllerWithoutPublishing(); + + IllegalStateException e = assertThrows( + IllegalStateException.class, + () -> controller.executeAsyncApiAction(new AsyncApiActionDto(), new AsyncApiReplyDto()) + ); + + assertTrue(e.getMessage().contains("executeAsyncApiAction"), e.getMessage()); + } + + /** + * A driver that says its SUT is an AsyncAPI service, but never implements the publishing. + */ + private static class AsyncApiControllerWithoutPublishing extends EmbeddedSutController { + + @Override + public String startSut() { + return null; + } + + @Override + public boolean isSutRunning() { + return false; + } + + @Override + public void stopSut() { + } + + @Override + public String getPackagePrefixesToCover() { + return null; + } + + @Override + public void resetStateOfSUT() { + } + + @Override + public List getInfoForAuthentication() { + return null; + } + + @Override + public List getDbSpecifications() { + return null; + } + + @Override + public ProblemInfo getProblemInfo() { + return AsyncApiProblem.fromSchemaText("asyncapi: 3.0.0"); + } + + @Override + public SutInfoDto.OutputFormat getPreferredOutputFormat() { + return SutInfoDto.OutputFormat.JAVA_JUNIT_5; + } + } +} From d7d3e9b2299138d3db070ccf300af39cba5c4756 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Tue, 8 Sep 2026 13:28:12 -0300 Subject: [PATCH 2/7] AsyncAPI 3.x: constants before fields in the action DTO Review feedback: CORRELATION_IN_HEADER and CORRELATION_IN_PAYLOAD closed the class, after every instance field. docs/for_developers.md puts constants first. --- .../api/dto/problem/asyncapi/AsyncApiActionDto.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java index 06158e24b2..958cd25503 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java @@ -12,6 +12,13 @@ */ public class AsyncApiActionDto { + /** + * The two places a correlation id can travel, as {@link #correlationLocation} names them. + */ + public static final String CORRELATION_IN_HEADER = "HEADER"; + + public static final String CORRELATION_IN_PAYLOAD = "PAYLOAD"; + /** * Key of the operation in the AsyncAPI document. Sent along so the driver can report and * log in terms the user will recognise from their own contract. @@ -86,8 +93,4 @@ public class AsyncApiActionDto { * generously and reported with the result. */ public Long replyTimeoutMs; - - public static final String CORRELATION_IN_HEADER = "HEADER"; - - public static final String CORRELATION_IN_PAYLOAD = "PAYLOAD"; } From 5f3a2c74fea516ada0435b1d2b57290742736b13 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Tue, 8 Sep 2026 14:00:41 -0300 Subject: [PATCH 3/7] AsyncAPI 3.x: say what the header maps hold Review feedback, and docs/for_developers.md: a Map field says what its key and value are. Both header maps go from header name to its text. --- .../controller/api/dto/problem/asyncapi/AsyncApiActionDto.java | 1 + .../controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java | 1 + 2 files changed, 2 insertions(+) diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java index 958cd25503..66ecfeb70d 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java @@ -53,6 +53,7 @@ public class AsyncApiActionDto { /** * Headers to publish alongside the body, for a transport that has them. + * Key is the header name as the document declares it, value is what to send under it, as text. */ public Map headers = new LinkedHashMap<>(); diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java index 5130169ba9..4921b4b7ac 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java @@ -49,6 +49,7 @@ public class AsyncApiReplyDto { /** * The reply's headers, for a transport that has them. + * Key is the header name, value is what arrived under it, as text. */ public Map replyHeaders = new LinkedHashMap<>(); From 2e1304d58607c7993bfb13162fed9cc6d2cf7c4c Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Tue, 8 Sep 2026 14:00:41 -0300 Subject: [PATCH 4/7] AsyncAPI 3.x: withServicesToNotMock states its pre-condition A null list failed inside addAll anyway; the check now sits where the method starts, named, as docs/for_developers.md asks of public methods. --- .../client/java/controller/problem/AsyncApiProblem.java | 1 + .../java/controller/problem/AsyncApiProblemTest.java | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/problem/AsyncApiProblem.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/problem/AsyncApiProblem.java index a1ad115354..3b2328bf81 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/problem/AsyncApiProblem.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/problem/AsyncApiProblem.java @@ -62,6 +62,7 @@ public String getSchemaText() { @Override public AsyncApiProblem withServicesToNotMock(List servicesToNotMock) { + Objects.requireNonNull(servicesToNotMock, "servicesToNotMock"); AsyncApiProblem p = new AsyncApiProblem(this.schemaLocation, this.schemaText); p.servicesToNotMock.addAll(servicesToNotMock); return p; diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/problem/AsyncApiProblemTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/problem/AsyncApiProblemTest.java index 7e2fbe2fdc..4b48e53b6b 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/problem/AsyncApiProblemTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/problem/AsyncApiProblemTest.java @@ -63,6 +63,14 @@ public void testServicesToNotMockAreCarriedOver() { assertEquals("asyncapi: 3.0.0", p.getSchemaText()); } + @Test + public void testServicesToNotMockCannotBeNull() { + + AsyncApiProblem p = AsyncApiProblem.fromSchemaText("asyncapi: 3.0.0"); + + assertThrows(NullPointerException.class, () -> p.withServicesToNotMock(null)); + } + @Test public void testServicesToNotMockCannotBeChangedFromOutside() { From 69860be0a5a815509a8893c82ca5f19185243ea6 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Tue, 8 Sep 2026 19:08:17 -0300 Subject: [PATCH 5/7] AsyncAPI 3.x: say when the two durations are null Review feedback: replyTimeoutMs is null when no reply is expected, and waitedMs when the driver did not wait at all, which the javadoc left unsaid. --- .../controller/api/dto/problem/asyncapi/AsyncApiActionDto.java | 2 ++ .../controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java index 66ecfeb70d..5b1e5b441a 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiActionDto.java @@ -92,6 +92,8 @@ public class AsyncApiActionDto { * There is no right answer here: a slow service and a stuck one look the same from outside, * so this is a tuning parameter with no equivalent in a synchronous protocol. It is set * generously and reported with the result. + * + * Null when no reply is expected, as there is then nothing to wait for. */ public Long replyTimeoutMs; } diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java index 4921b4b7ac..3d878f414e 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java @@ -66,6 +66,8 @@ public class AsyncApiReplyDto { /** * How long the driver waited, in milliseconds, whether or not anything arrived. Reported * because the verdict on silence is only meaningful alongside how long it was waited for. + * + * Null when the driver did not wait at all: no reply was expected, or nothing was published. */ public Long waitedMs; From 0e2c12e844a7086a0f01e0e203fefce75f0bfc81 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Fri, 11 Sep 2026 17:25:04 -0300 Subject: [PATCH 6/7] AsyncAPI 3.x: the reply's index cannot be null Review feedback: it echoes ActionDto.index, which newAction has already dereferenced by the time the reply is built, so an int says what the Integer could only promise. --- .../controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java index 3d878f414e..82ff7b8582 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java @@ -23,7 +23,7 @@ public class AsyncApiReplyDto { /** * The index of the action this answers, echoing what was asked. */ - public Integer index; + public int index; /** * Whether the message reached the broker. False means the driver could not publish, and From 94b1a3631dcebca93a1af081ac724ce1fff79607 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Thu, 17 Sep 2026 09:44:01 -0300 Subject: [PATCH 7/7] AsyncAPI 3.x: no primitives in the reply DTO A primitive turns a field the driver never set into a default, with no way to tell the two apart. index, published, replyReceived, replyExpected and correlationMatched are boxed, and each says what null means. The distinction earns its keep on correlationMatched: a driver that does not track correlation at all said false before, which reads as the service having failed to echo the id back. That is a claim about the SUT, and it was not one the driver ever made. The core reads these, so it decides what an absent value means rather than inheriting a default; that half comes with the fitness. Also: the check for exactly one of a location and a text is written out, rather than comparing two null-checks for equality. --- .../problem/asyncapi/AsyncApiReplyDto.java | 21 ++++++++++++++----- .../controller/problem/AsyncApiProblem.java | 3 ++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java index 82ff7b8582..c36e0e0703 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/problem/asyncapi/AsyncApiReplyDto.java @@ -23,24 +23,32 @@ public class AsyncApiReplyDto { /** * The index of the action this answers, echoing what was asked. */ - public int index; + public Integer index; /** * Whether the message reached the broker. False means the driver could not publish, and * {@link #errorMessage} says why. + * + * Null means the driver did not say, which is read the same way as false: without an answer + * here there is no knowing whether the message went out, and the rest of the test would + * mean nothing. */ - public boolean published; + public Boolean published; /** * Whether a reply arrived and was recognised as answering this message. + * + * Null means the driver did not say, and is read as no reply having arrived. */ - public boolean replyReceived; + public Boolean replyReceived; /** * Whether the driver waited for a reply at all. False for a fire-and-forget operation, so * that the absence of a reply is not mistaken for silence in answer to a promise. + * + * Null means the driver did not say, and is read as not having waited. */ - public boolean replyExpected; + public Boolean replyExpected; /** * The reply body, as it arrived. @@ -60,8 +68,11 @@ public class AsyncApiReplyDto { * echoing the id is the service's own behaviour. A reply that arrives without it is * recorded rather than treated as a fault, since from outside there is no telling a defect * from a service that correlates by some business key instead. + * + * Null means the driver does not track correlation at all, which is not the same as having + * checked and found the id missing, and is recorded as neither. */ - public boolean correlationMatched; + public Boolean correlationMatched; /** * How long the driver waited, in milliseconds, whether or not anything arrived. Reported diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/problem/AsyncApiProblem.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/problem/AsyncApiProblem.java index 3b2328bf81..4d3582dede 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/problem/AsyncApiProblem.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/problem/AsyncApiProblem.java @@ -31,7 +31,8 @@ public AsyncApiProblem(String schemaLocation) { private AsyncApiProblem(String schemaLocation, String schemaText) { - if ((schemaLocation == null) == (schemaText == null)) { + if ((schemaLocation == null && schemaText == null) + || (schemaLocation != null && schemaText != null)) { throw new IllegalArgumentException( "An AsyncAPI problem needs exactly one of a schema location and a schema text"); }