From 93a253acf9eba0e25782343a3983f30b0c6096e5 Mon Sep 17 00:00:00 2001 From: "Xiaofei Cao (from Dev Box)" Date: Fri, 16 Oct 2026 10:57:32 +0800 Subject: [PATCH 1/5] Make Search retrieval stream wrappers SDK-created only Keep decoding in the converter and register typed package-private constructors from the base model. Generate the concrete base and SDK-only payload wrappers through Search customizations, and cover constructor visibility, payload identity, unknown events, malformed data, and cold initialization. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa365f59-c459-44fc-8076-1d59c78dc0b8 --- .../azure-search-documents/CHANGELOG.md | 2 + .../src/main/java/SearchCustomizations.java | 74 +++-- .../models/KnowledgeSourceFileCapacity.java | 12 +- ...edgeBaseRetrievalStreamEventConverter.java | 136 ++++++-- ...ledgeBaseActivityCompletedStreamEvent.java | 3 +- ...owledgeBaseActivityStartedStreamEvent.java | 3 +- ...owledgeBaseAnswerCompletedStreamEvent.java | 3 +- .../models/KnowledgeBaseErrorStreamEvent.java | 3 +- ...dgeBaseReferencesCompletedStreamEvent.java | 3 +- ...ledgeBaseResponseCompletedStreamEvent.java | 3 +- ...wledgeBaseRetrievalStartedStreamEvent.java | 3 +- .../KnowledgeBaseRetrievalStreamEvent.java | 64 +++- .../KnowledgeBaseRetrievalStreamTests.java | 163 ++------- ...nowledgeBaseRetrievalStreamEventTests.java | 313 ++++++++++++++++++ 14 files changed, 591 insertions(+), 194 deletions(-) create mode 100644 sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEventTests.java diff --git a/sdk/search/azure-search-documents/CHANGELOG.md b/sdk/search/azure-search-documents/CHANGELOG.md index fc096cfc1d713..188e014007c5f 100644 --- a/sdk/search/azure-search-documents/CHANGELOG.md +++ b/sdk/search/azure-search-documents/CHANGELOG.md @@ -34,6 +34,8 @@ The following changes apply when upgrading from the preceding `12.1.0` beta rele `KnowledgeBaseRetrievalStreamEvent.getEventName()`. Their payloads are not available. - Removed JSON serialization and deserialization APIs from retrieval stream event wrappers. Their typed payload models retain these APIs. +- Made retrieval stream event wrapper constructors package-private so only the SDK creates these envelopes. + `KnowledgeBaseRetrievalStreamEvent` is now concrete and represents unrecognized, nonterminal events by name only. - Removed preview-only image vector queries, `VectorFilterMode.STRICT_POST_FILTER`, Azure Machine Learning vectorizers, and `AIFoundryModelCatalogName`. - Moved `KnowledgeSourceFileCapacity` to `com.azure.search.documents.indexes.models`. diff --git a/sdk/search/azure-search-documents/customizations/src/main/java/SearchCustomizations.java b/sdk/search/azure-search-documents/customizations/src/main/java/SearchCustomizations.java index 8296bc71630dc..0a5a37ef88c05 100644 --- a/sdk/search/azure-search-documents/customizations/src/main/java/SearchCustomizations.java +++ b/sdk/search/azure-search-documents/customizations/src/main/java/SearchCustomizations.java @@ -293,32 +293,66 @@ private static String streamEventDocumentation() { return "Events are polymorphic: see {@link KnowledgeBaseRetrievalStreamEvent} for the known subtypes. " + "Use a known subtype's {@code getValue()} to access its typed payload, for example " + "{@link com.azure.search.documents.knowledgebases.models.KnowledgeBaseAnswerCompletedStreamEvent" - + "#getValue()}. " - + "Unrecognized events expose only their names through " + + "#getValue()}. " + "Unrecognized events expose only their names through " + "{@link KnowledgeBaseRetrievalStreamEvent#getEventName()}; their payloads are not available.\n\n"; } private static String baseEventSource() { return header("com.azure.search.documents.knowledgebases.models") - + "import com.azure.core.annotation.Generated;\n\n" + "/**\n" - + " * Abstract base for polymorphic events emitted by a streaming knowledge base retrieval.\n" + + "import com.azure.core.annotation.Generated;\n" + + "import com.azure.search.documents.knowledgebases.implementation." + + "KnowledgeBaseRetrievalStreamEventConverter;\n" + "import java.util.List;\n\n" + "/**\n" + + " * Base for polymorphic events emitted by a streaming knowledge base retrieval.\n" + + " * Instances are created by the SDK, not by applications.\n" + " * Known events expose typed payloads through their subtype's {@code getValue()} method.\n" - + " * Unrecognized events expose only their names through {@link #getEventName()}; " - + "their payloads are not available.\n" + + " * Unrecognized events use this concrete base class and expose only their names through\n" + + " * {@link #getEventName()}; their payloads are not available and they are not terminal.\n" + " * Stream events are envelopes, not JSON payload models; serialize the typed payload instead.\n" + " *\n" + " * @see KnowledgeBaseRetrievalStartedStreamEvent\n" + " * @see KnowledgeBaseActivityStartedStreamEvent\n" + " * @see KnowledgeBaseActivityCompletedStreamEvent\n" + " * @see KnowledgeBaseAnswerCompletedStreamEvent\n" + " * @see KnowledgeBaseReferencesCompletedStreamEvent\n" + " * @see KnowledgeBaseErrorStreamEvent\n" + " * @see KnowledgeBaseResponseCompletedStreamEvent\n" + " */\n" - + "public abstract class KnowledgeBaseRetrievalStreamEvent {\n" + " @Generated\n" + + "public class KnowledgeBaseRetrievalStreamEvent {\n" + " static {\n" + + " KnowledgeBaseRetrievalStreamEventConverter.setConstructorAccessor(\n" + + " new KnowledgeBaseRetrievalStreamEventConverter.ConstructorAccessor() {\n" + + " @Override\n" + + " public KnowledgeBaseRetrievalStreamEvent createEvent(String eventName) {\n" + + " return new KnowledgeBaseRetrievalStreamEvent(eventName);\n" + " }\n\n" + + " @Override\n" + + " public KnowledgeBaseRetrievalStartedStreamEvent createRetrievalStarted(\n" + + " KnowledgeBaseRetrievalStartedEvent value) {\n" + + " return new KnowledgeBaseRetrievalStartedStreamEvent(value);\n" + + " }\n\n" + " @Override\n" + + " public KnowledgeBaseActivityStartedStreamEvent createActivityStarted(\n" + + " KnowledgeBaseActivityStartedEvent value) {\n" + + " return new KnowledgeBaseActivityStartedStreamEvent(value);\n" + + " }\n\n" + " @Override\n" + + " public KnowledgeBaseActivityCompletedStreamEvent createActivityCompleted(\n" + + " KnowledgeBaseActivityRecord value) {\n" + + " return new KnowledgeBaseActivityCompletedStreamEvent(value);\n" + + " }\n\n" + " @Override\n" + + " public KnowledgeBaseAnswerCompletedStreamEvent createAnswerCompleted(\n" + + " KnowledgeBaseAnswerCompletedEvent value) {\n" + + " return new KnowledgeBaseAnswerCompletedStreamEvent(value);\n" + + " }\n\n" + " @Override\n" + + " public KnowledgeBaseReferencesCompletedStreamEvent createReferencesCompleted(\n" + + " List value) {\n" + + " return new KnowledgeBaseReferencesCompletedStreamEvent(value);\n" + + " }\n\n" + " @Override\n" + + " public KnowledgeBaseErrorStreamEvent createError(KnowledgeBaseStreamErrorEvent value) {\n" + + " return new KnowledgeBaseErrorStreamEvent(value);\n" + " }\n\n" + + " @Override\n" + + " public KnowledgeBaseResponseCompletedStreamEvent createResponseCompleted(\n" + + " KnowledgeBaseResponseCompletedEvent value) {\n" + + " return new KnowledgeBaseResponseCompletedStreamEvent(value);\n" + + " }\n" + " });\n" + " }\n\n" + " @Generated\n" + " private final String eventName;\n\n" + " /**\n" + " * Creates a stream event.\n" + " *\n" + " * @param eventName The server-sent event name.\n" + " */\n" + " @Generated\n" - + " protected KnowledgeBaseRetrievalStreamEvent(String eventName) {\n" - + " this.eventName = eventName;\n" + " }\n\n" + " /**\n" - + " * Gets the server-sent event name.\n" + " *\n" + " * @return The event name.\n" - + " */\n" + " @Generated\n" + " public final String getEventName() {\n" - + " return eventName;\n" + " }\n\n" + " /**\n" + + " KnowledgeBaseRetrievalStreamEvent(String eventName) {\n" + " this.eventName = eventName;\n" + + " }\n\n" + " /**\n" + " * Gets the server-sent event name.\n" + " *\n" + + " * @return The event name.\n" + " */\n" + " @Generated\n" + + " public final String getEventName() {\n" + " return eventName;\n" + " }\n\n" + " /**\n" + " * Gets whether this event terminates the retrieval stream.\n" + " *\n" + " * @return {@code true} if this is a terminal event; otherwise {@code false}.\n" + " */\n" + " @Generated\n" + " public boolean isTerminal() {\n" + " return false;\n" + " }\n" @@ -337,15 +371,17 @@ private static String wrapperSource(String className, String payloadType, String return header("com.azure.search.documents.knowledgebases.models") + "import com.azure.core.annotation.Generated;\n" + "import com.azure.core.annotation.Immutable;\n" + listImports + "\n" + "/**\n" + " * Represents the {@code " + eventName - + "} knowledge base retrieval stream event.\n" + " * Access the typed payload with {@link #getValue()}.\n" - + " *\n" + " * @see KnowledgeBaseRetrievalStreamEvent\n" + " */\n" + "@Immutable\n" + "public final class " + + "} knowledge base retrieval stream event.\n" + + " * Instances are created by the SDK, not by applications.\n" + + " * Access the typed payload with {@link #getValue()}.\n" + " *\n" + + " * @see KnowledgeBaseRetrievalStreamEvent\n" + " */\n" + "@Immutable\n" + "public final class " + className + " extends KnowledgeBaseRetrievalStreamEvent {\n" + " @Generated\n" + " private final " + valueType + " value;\n\n" + " /**\n" + " * Creates an event wrapper.\n" + " *\n" - + " * @param value The event payload.\n" + " */\n" + " @Generated\n" + " public " + className - + "(" + valueType + " value) {\n" + " super(\"" + eventName + "\");\n" - + " this.value = value;\n" + " }\n\n" + " /**\n" + " * Gets the event payload.\n" - + " *\n" + " * @return The event payload.\n" + " */\n" + " @Generated\n" + " public " - + valueType + " getValue() {\n" + " return value;\n" + " }\n" + terminalOverride + "}\n"; + + " * @param value The event payload.\n" + " */\n" + " @Generated\n" + " " + className + "(" + + valueType + " value) {\n" + " super(\"" + eventName + "\");\n" + " this.value = value;\n" + + " }\n\n" + " /**\n" + " * Gets the event payload.\n" + " *\n" + + " * @return The event payload.\n" + " */\n" + " @Generated\n" + " public " + valueType + + " getValue() {\n" + " return value;\n" + " }\n" + terminalOverride + "}\n"; } private static String header(String packageName) { diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/models/KnowledgeSourceFileCapacity.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/models/KnowledgeSourceFileCapacity.java index f96f8b9bcee3f..ad77e34d6d469 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/models/KnowledgeSourceFileCapacity.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/indexes/models/KnowledgeSourceFileCapacity.java @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // Code generated by Microsoft (R) TypeSpec Code Generator. - package com.azure.search.documents.indexes.models; import com.azure.core.annotation.Generated; @@ -17,6 +16,7 @@ */ @Immutable public final class KnowledgeSourceFileCapacity implements JsonSerializable { + /* * The maximum number of files allowed in the knowledge source. */ @@ -44,7 +44,7 @@ private KnowledgeSourceFileCapacity() { /** * Get the maxFileCount property: The maximum number of files allowed in the knowledge source. - * + * * @return the maxFileCount value. */ @Generated @@ -55,7 +55,7 @@ public int getMaxFileCount() { /** * Get the remainingFileCount property: The number of additional files that can be uploaded at the time of the * request. - * + * * @return the remainingFileCount value. */ @Generated @@ -65,7 +65,7 @@ public int getRemainingFileCount() { /** * Get the maxFileSizeBytes property: The maximum size in bytes of an individual uploaded file. - * + * * @return the maxFileSizeBytes value. */ @Generated @@ -85,7 +85,7 @@ public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { /** * Reads an instance of KnowledgeSourceFileCapacity from the JsonReader. - * + * * @param jsonReader The JsonReader being read. * @return An instance of KnowledgeSourceFileCapacity if the JsonReader was pointing to an instance of it, or null * if it was pointing to JSON null. @@ -99,7 +99,6 @@ public static KnowledgeSourceFileCapacity fromJson(JsonReader jsonReader) throws while (reader.nextToken() != JsonToken.END_OBJECT) { String fieldName = reader.getFieldName(); reader.nextToken(); - if ("maxFileCount".equals(fieldName)) { deserializedKnowledgeSourceFileCapacity.maxFileCount = reader.getInt(); } else if ("remainingFileCount".equals(fieldName)) { @@ -110,7 +109,6 @@ public static KnowledgeSourceFileCapacity fromJson(JsonReader jsonReader) throws reader.skipChildren(); } } - return deserializedKnowledgeSourceFileCapacity; }); } diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/implementation/KnowledgeBaseRetrievalStreamEventConverter.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/implementation/KnowledgeBaseRetrievalStreamEventConverter.java index 83b1898930dbf..f657be60a4448 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/implementation/KnowledgeBaseRetrievalStreamEventConverter.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/implementation/KnowledgeBaseRetrievalStreamEventConverter.java @@ -3,6 +3,7 @@ package com.azure.search.documents.knowledgebases.implementation; +import com.azure.core.util.logging.ClientLogger; import com.azure.json.JsonProviders; import com.azure.json.JsonReader; import com.azure.json.ReadValueCallback; @@ -23,14 +24,116 @@ import com.azure.search.documents.knowledgebases.models.KnowledgeBaseStreamErrorEvent; import java.io.IOException; import java.io.UncheckedIOException; +import java.util.List; +import java.util.Objects; /** * Converts knowledge base retrieval stream event payloads to typed event models. */ public final class KnowledgeBaseRetrievalStreamEventConverter { + private static final ClientLogger LOGGER = new ClientLogger(KnowledgeBaseRetrievalStreamEventConverter.class); + private static volatile ConstructorAccessor constructorAccessor; + private KnowledgeBaseRetrievalStreamEventConverter() { } + /** + * Provides access to the SDK-only event constructors. + */ + public interface ConstructorAccessor { + /** + * Creates a name-only event. + * + * @param eventName The event name. + * @return The event. + */ + KnowledgeBaseRetrievalStreamEvent createEvent(String eventName); + + /** + * Creates a retrieval-started event. + * + * @param value The decoded payload. + * @return The event. + */ + KnowledgeBaseRetrievalStartedStreamEvent createRetrievalStarted(KnowledgeBaseRetrievalStartedEvent value); + + /** + * Creates an activity-started event. + * + * @param value The decoded payload. + * @return The event. + */ + KnowledgeBaseActivityStartedStreamEvent createActivityStarted(KnowledgeBaseActivityStartedEvent value); + + /** + * Creates an activity-completed event. + * + * @param value The decoded payload. + * @return The event. + */ + KnowledgeBaseActivityCompletedStreamEvent createActivityCompleted(KnowledgeBaseActivityRecord value); + + /** + * Creates an answer-completed event. + * + * @param value The decoded payload. + * @return The event. + */ + KnowledgeBaseAnswerCompletedStreamEvent createAnswerCompleted(KnowledgeBaseAnswerCompletedEvent value); + + /** + * Creates a references-completed event. + * + * @param value The decoded payload. + * @return The event. + */ + KnowledgeBaseReferencesCompletedStreamEvent createReferencesCompleted(List value); + + /** + * Creates an error event. + * + * @param value The decoded payload. + * @return The event. + */ + KnowledgeBaseErrorStreamEvent createError(KnowledgeBaseStreamErrorEvent value); + + /** + * Creates a response-completed event. + * + * @param value The decoded payload. + * @return The event. + */ + KnowledgeBaseResponseCompletedStreamEvent createResponseCompleted(KnowledgeBaseResponseCompletedEvent value); + } + + /** + * Registers the event constructors from the models package. + * + * @param accessor The constructor accessor. + */ + public static void setConstructorAccessor(ConstructorAccessor accessor) { + constructorAccessor = Objects.requireNonNull(accessor, "'accessor' cannot be null."); + } + + private static ConstructorAccessor getConstructorAccessor() { + ConstructorAccessor accessor = constructorAccessor; + if (accessor == null) { + try { + Class.forName(KnowledgeBaseRetrievalStreamEvent.class.getName(), true, + KnowledgeBaseRetrievalStreamEvent.class.getClassLoader()); + } catch (ClassNotFoundException | LinkageError exception) { + throw LOGGER.logExceptionAsError(new IllegalStateException( + "Failed to initialize knowledge base retrieval stream event constructors.", exception)); + } + accessor = constructorAccessor; + if (accessor == null) { + throw LOGGER.logExceptionAsError(new IllegalStateException( + "Knowledge base retrieval stream event constructors were not registered.")); + } + } + return accessor; + } + /** * Converts a stream event payload. * @@ -39,37 +142,36 @@ private KnowledgeBaseRetrievalStreamEventConverter() { * @return The typed stream event. */ public static KnowledgeBaseRetrievalStreamEvent convert(String eventName, String data) { + ConstructorAccessor accessor = getConstructorAccessor(); switch (eventName) { case "retrieval.started": - return new KnowledgeBaseRetrievalStartedStreamEvent( - read(eventName, data, KnowledgeBaseRetrievalStartedEvent::fromJson)); + return accessor + .createRetrievalStarted(read(eventName, data, KnowledgeBaseRetrievalStartedEvent::fromJson)); case "activity.started": - return new KnowledgeBaseActivityStartedStreamEvent( - read(eventName, data, KnowledgeBaseActivityStartedEvent::fromJson)); + return accessor + .createActivityStarted(read(eventName, data, KnowledgeBaseActivityStartedEvent::fromJson)); case "activity.completed": - return new KnowledgeBaseActivityCompletedStreamEvent( - read(eventName, data, KnowledgeBaseActivityRecord::fromJson)); + return accessor.createActivityCompleted(read(eventName, data, KnowledgeBaseActivityRecord::fromJson)); case "answer.completed": - return new KnowledgeBaseAnswerCompletedStreamEvent( - read(eventName, data, KnowledgeBaseAnswerCompletedEvent::fromJson)); + return accessor + .createAnswerCompleted(read(eventName, data, KnowledgeBaseAnswerCompletedEvent::fromJson)); case "references.completed": - return new KnowledgeBaseReferencesCompletedStreamEvent( + return accessor.createReferencesCompleted( read(eventName, data, reader -> reader.readArray(KnowledgeBaseReference::fromJson))); case "error": - return new KnowledgeBaseErrorStreamEvent( - read(eventName, data, KnowledgeBaseStreamErrorEvent::fromJson)); + return accessor.createError(read(eventName, data, KnowledgeBaseStreamErrorEvent::fromJson)); case "response.completed": - return new KnowledgeBaseResponseCompletedStreamEvent( - read(eventName, data, KnowledgeBaseResponseCompletedEvent::fromJson)); + return accessor + .createResponseCompleted(read(eventName, data, KnowledgeBaseResponseCompletedEvent::fromJson)); default: - return new UnrecognizedStreamEvent(eventName); + return accessor.createEvent(eventName); } } @@ -80,12 +182,6 @@ private static T read(String eventName, String data, ReadValueCallback value) { + KnowledgeBaseReferencesCompletedStreamEvent(List value) { super("references.completed"); this.value = value; } diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseResponseCompletedStreamEvent.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseResponseCompletedStreamEvent.java index 7e8f0cc42f2c6..850d1f7989f45 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseResponseCompletedStreamEvent.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseResponseCompletedStreamEvent.java @@ -7,6 +7,7 @@ /** * Represents the {@code response.completed} knowledge base retrieval stream event. + * Instances are created by the SDK, not by applications. * Access the typed payload with {@link #getValue()}. * * @see KnowledgeBaseRetrievalStreamEvent @@ -23,7 +24,7 @@ public final class KnowledgeBaseResponseCompletedStreamEvent extends KnowledgeBa * @param value The event payload. */ @Generated - public KnowledgeBaseResponseCompletedStreamEvent(KnowledgeBaseResponseCompletedEvent value) { + KnowledgeBaseResponseCompletedStreamEvent(KnowledgeBaseResponseCompletedEvent value) { super("response.completed"); this.value = value; } diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStartedStreamEvent.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStartedStreamEvent.java index 9c2be7f80e0cc..e050780b8982e 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStartedStreamEvent.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStartedStreamEvent.java @@ -7,6 +7,7 @@ /** * Represents the {@code retrieval.started} knowledge base retrieval stream event. + * Instances are created by the SDK, not by applications. * Access the typed payload with {@link #getValue()}. * * @see KnowledgeBaseRetrievalStreamEvent @@ -23,7 +24,7 @@ public final class KnowledgeBaseRetrievalStartedStreamEvent extends KnowledgeBas * @param value The event payload. */ @Generated - public KnowledgeBaseRetrievalStartedStreamEvent(KnowledgeBaseRetrievalStartedEvent value) { + KnowledgeBaseRetrievalStartedStreamEvent(KnowledgeBaseRetrievalStartedEvent value) { super("retrieval.started"); this.value = value; } diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEvent.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEvent.java index 12fc8ba8dfd01..8bea1b09ac775 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEvent.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEvent.java @@ -3,11 +3,15 @@ package com.azure.search.documents.knowledgebases.models; import com.azure.core.annotation.Generated; +import com.azure.search.documents.knowledgebases.implementation.KnowledgeBaseRetrievalStreamEventConverter; +import java.util.List; /** - * Abstract base for polymorphic events emitted by a streaming knowledge base retrieval. + * Base for polymorphic events emitted by a streaming knowledge base retrieval. + * Instances are created by the SDK, not by applications. * Known events expose typed payloads through their subtype's {@code getValue()} method. - * Unrecognized events expose only their names through {@link #getEventName()}; their payloads are not available. + * Unrecognized events use this concrete base class and expose only their names through + * {@link #getEventName()}; their payloads are not available and they are not terminal. * Stream events are envelopes, not JSON payload models; serialize the typed payload instead. * * @see KnowledgeBaseRetrievalStartedStreamEvent @@ -18,7 +22,7 @@ * @see KnowledgeBaseErrorStreamEvent * @see KnowledgeBaseResponseCompletedStreamEvent */ -public abstract class KnowledgeBaseRetrievalStreamEvent { +public class KnowledgeBaseRetrievalStreamEvent { @Generated private final String eventName; @@ -29,7 +33,7 @@ public abstract class KnowledgeBaseRetrievalStreamEvent { * @param eventName The server-sent event name. */ @Generated - protected KnowledgeBaseRetrievalStreamEvent(String eventName) { + KnowledgeBaseRetrievalStreamEvent(String eventName) { this.eventName = eventName; } @@ -52,4 +56,56 @@ public final String getEventName() { public boolean isTerminal() { return false; } + + static { + KnowledgeBaseRetrievalStreamEventConverter + .setConstructorAccessor(new KnowledgeBaseRetrievalStreamEventConverter.ConstructorAccessor() { + + @Override + public KnowledgeBaseRetrievalStreamEvent createEvent(String eventName) { + return new KnowledgeBaseRetrievalStreamEvent(eventName); + } + + @Override + public KnowledgeBaseRetrievalStartedStreamEvent + createRetrievalStarted(KnowledgeBaseRetrievalStartedEvent value) { + return new KnowledgeBaseRetrievalStartedStreamEvent(value); + } + + @Override + public KnowledgeBaseActivityStartedStreamEvent + createActivityStarted(KnowledgeBaseActivityStartedEvent value) { + return new KnowledgeBaseActivityStartedStreamEvent(value); + } + + @Override + public KnowledgeBaseActivityCompletedStreamEvent + createActivityCompleted(KnowledgeBaseActivityRecord value) { + return new KnowledgeBaseActivityCompletedStreamEvent(value); + } + + @Override + public KnowledgeBaseAnswerCompletedStreamEvent + createAnswerCompleted(KnowledgeBaseAnswerCompletedEvent value) { + return new KnowledgeBaseAnswerCompletedStreamEvent(value); + } + + @Override + public KnowledgeBaseReferencesCompletedStreamEvent + createReferencesCompleted(List value) { + return new KnowledgeBaseReferencesCompletedStreamEvent(value); + } + + @Override + public KnowledgeBaseErrorStreamEvent createError(KnowledgeBaseStreamErrorEvent value) { + return new KnowledgeBaseErrorStreamEvent(value); + } + + @Override + public KnowledgeBaseResponseCompletedStreamEvent + createResponseCompleted(KnowledgeBaseResponseCompletedEvent value) { + return new KnowledgeBaseResponseCompletedStreamEvent(value); + } + }); + } } diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/KnowledgeBaseRetrievalStreamTests.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/KnowledgeBaseRetrievalStreamTests.java index 75a0266ecfda8..7f4f0453b550f 100644 --- a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/KnowledgeBaseRetrievalStreamTests.java +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/KnowledgeBaseRetrievalStreamTests.java @@ -10,7 +10,6 @@ import com.azure.core.test.http.MockHttpResponse; import com.azure.core.util.CloseableIterableStream; import com.azure.json.JsonSerializable; -import com.azure.json.JsonWriter; import com.azure.search.documents.knowledgebases.implementation.KnowledgeBaseRetrievalStreamEventConverter; import com.azure.search.documents.knowledgebases.models.KnowledgeBaseActivityCompletedStreamEvent; import com.azure.search.documents.knowledgebases.models.KnowledgeBaseActivityRecord; @@ -43,12 +42,10 @@ import reactor.test.StepVerifier; import java.io.IOException; -import java.lang.reflect.Modifier; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -59,7 +56,6 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -133,17 +129,11 @@ public void preservesUnknownEventsAndRejectsMalformedKnownEvents() { = KnowledgeBaseRetrievalStreamEventConverter.convert("future.event", "not json\nsecond line"); assertEvent(event, "future.event", false); - assertTrue(Modifier.isPrivate(event.getClass().getModifiers())); - assertTrue(Arrays.stream(event.getClass().getDeclaredFields()).allMatch(field -> field.isSynthetic())); - assertEquals(1, - Arrays.stream(event.getClass().getDeclaredConstructors()) - .filter(constructor -> !constructor.isSynthetic()) - .count()); - assertTrue(Arrays.stream(event.getClass().getDeclaredConstructors()) - .filter(constructor -> !constructor.isSynthetic()) - .allMatch(constructor -> constructor.getParameterCount() == 1 - && constructor.getParameterTypes()[0] == String.class)); - assertEvent(KnowledgeBaseRetrievalStreamEventConverter.convert("future.event", null), "future.event", false); + assertEquals(KnowledgeBaseRetrievalStreamEvent.class, event.getClass()); + KnowledgeBaseRetrievalStreamEvent nullPayload + = KnowledgeBaseRetrievalStreamEventConverter.convert("future.event", null); + assertEquals(KnowledgeBaseRetrievalStreamEvent.class, nullPayload.getClass()); + assertEvent(nullPayload, "future.event", false); assertThrows(RuntimeException.class, () -> KnowledgeBaseRetrievalStreamEventConverter.convert("response.completed", "{")); } @@ -170,6 +160,13 @@ public void bothClientsDecodeTypedPayloadsForEveryKnownVariant(String eventName, KnowledgeBaseRetrievalStreamEvent converted = KnowledgeBaseRetrievalStreamEventConverter.convert(eventName, data); assertEquals("error".equals(eventName) || "response.completed".equals(eventName), converted.isTerminal()); + if ("activity.completed".equals(eventName)) { + assertInstanceOf(KnowledgeBaseSearchIndexActivityRecord.class, payload(converted)); + } + if ("references.completed".equals(eventName)) { + assertInstanceOf(KnowledgeBaseSearchIndexReference.class, + ((KnowledgeBaseReferencesCompletedStreamEvent) converted).getValue().get(0)); + } try (CloseableIterableStream stream = createBuilder(body, null).buildClient().retrieveStream(new KnowledgeBaseRetrievalOptions())) { @@ -231,106 +228,30 @@ public void bothClientsPreserveAgenticReasoningInProgressAndCompletedResponse() @ParameterizedTest @ValueSource(strings = { "", " \t ", "not json", "not json\n second line ", " {\"future\":true}\n " }) public void unknownPayloadsAreDiscardedWithoutDisplacingLaterTerminalEvents(String data) throws IOException { - assertEvent(KnowledgeBaseRetrievalStreamEventConverter.convert("future.event", data), "future.event", false); + KnowledgeBaseRetrievalStreamEvent converted + = KnowledgeBaseRetrievalStreamEventConverter.convert("future.event", data); + assertEquals(KnowledgeBaseRetrievalStreamEvent.class, converted.getClass()); + assertEvent(converted, "future.event", false); String body = frame("future.event", data) + frame("response.completed", RESPONSE_COMPLETED_JSON); try (CloseableIterableStream stream = createBuilder(body, null).buildClient().retrieveStream(new KnowledgeBaseRetrievalOptions())) { Iterator iterator = stream.iterator(); KnowledgeBaseRetrievalStreamEvent event = iterator.next(); + assertEquals(KnowledgeBaseRetrievalStreamEvent.class, event.getClass()); assertEvent(event, "future.event", false); assertEvent(iterator.next(), "response.completed", true); assertFalse(iterator.hasNext()); } StepVerifier .create(createBuilder(body, null).buildAsyncClient().retrieveStream(new KnowledgeBaseRetrievalOptions())) - .assertNext(event -> assertEvent(event, "future.event", false)) + .assertNext(event -> { + assertEquals(KnowledgeBaseRetrievalStreamEvent.class, event.getClass()); + assertEvent(event, "future.event", false); + }) .assertNext(event -> assertEvent(event, "response.completed", true)) .verifyComplete(); } - @ParameterizedTest - @MethodSource("knownEvents") - public void payloadOnlyConstructorsRetainTypedPayloadsWithoutRawOrJsonApis(String eventName, String json) - throws ReflectiveOperationException { - KnowledgeBaseRetrievalStreamEvent converted - = KnowledgeBaseRetrievalStreamEventConverter.convert(eventName, json); - KnowledgeBaseRetrievalStreamEvent event = withPayload(converted); - Object value = payload(event); - assertSame(value, payload(converted)); - assertEvent(event, eventName, converted.isTerminal()); - assertTrue(Modifier.isAbstract(KnowledgeBaseRetrievalStreamEvent.class.getModifiers())); - assertFalse(JsonSerializable.class.isAssignableFrom(event.getClass())); - assertFalse(Arrays.stream(event.getClass().getMethods()) - .anyMatch(method -> "fromJson".equals(method.getName()) || method.getName().startsWith("toJson"))); - assertThrows(NoSuchMethodException.class, () -> event.getClass().getMethod("getRawValue")); - assertThrows(NoSuchMethodException.class, - () -> KnowledgeBaseRetrievalStreamEvent.class.getMethod("getRawValue")); - assertEquals(1, event.getClass().getDeclaredConstructors().length); - assertEquals(1, event.getClass().getDeclaredConstructors()[0].getParameterCount()); - assertEquals(1, KnowledgeBaseRetrievalStreamEvent.class.getDeclaredConstructors().length); - assertNotNull(KnowledgeBaseRetrievalStreamEvent.class.getDeclaredConstructor(String.class)); - assertThrows(NoSuchMethodException.class, - () -> KnowledgeBaseRetrievalStreamEvent.class.getDeclaredConstructor(String.class, String.class)); - assertEquals(1, - Arrays.stream(KnowledgeBaseRetrievalStreamEvent.class.getDeclaredFields()) - .filter(field -> !field.isSynthetic()) - .count()); - assertNotNull(KnowledgeBaseRetrievalStreamEvent.class.getDeclaredField("eventName")); - assertThrows(NoSuchFieldException.class, - () -> KnowledgeBaseRetrievalStreamEvent.class.getDeclaredField("rawValue")); - if ("activity.completed".equals(eventName)) { - assertInstanceOf(KnowledgeBaseSearchIndexActivityRecord.class, value); - } - if ("references.completed".equals(eventName)) { - assertInstanceOf(KnowledgeBaseSearchIndexReference.class, - ((KnowledgeBaseReferencesCompletedStreamEvent) event).getValue().get(0)); - } - } - - @Test - public void nullPayloadsAndEmptyReferenceListsRemainAccessible() { - List absent = Arrays.asList( - new KnowledgeBaseRetrievalStartedStreamEvent(null), new KnowledgeBaseActivityStartedStreamEvent(null), - new KnowledgeBaseActivityCompletedStreamEvent(null), new KnowledgeBaseAnswerCompletedStreamEvent(null), - new KnowledgeBaseReferencesCompletedStreamEvent(null), new KnowledgeBaseErrorStreamEvent(null), - new KnowledgeBaseResponseCompletedStreamEvent(null)); - absent.forEach(event -> assertNull(payload(event))); - List empty = Collections.emptyList(); - assertSame(empty, new KnowledgeBaseReferencesCompletedStreamEvent(empty).getValue()); - assertEvent(new KnowledgeBaseRetrievalStreamEvent("custom") { - }, "custom", false); - } - - @Test - public void constructorsAndPayloadAccessNeverSerialize() { - AtomicInteger writes = new AtomicInteger(); - KnowledgeBaseActivityRecord activity = new KnowledgeBaseActivityRecord(0) { - @Override - public JsonWriter toJson(JsonWriter writer) throws IOException { - writes.incrementAndGet(); - throw new IOException("The event wrapper must not serialize its payload."); - } - }; - KnowledgeBaseActivityCompletedStreamEvent event = new KnowledgeBaseActivityCompletedStreamEvent(activity); - assertSame(activity, event.getValue()); - assertSame(activity, event.getValue()); - assertEquals(0, writes.get()); - - AtomicInteger referenceWrites = new AtomicInteger(); - KnowledgeBaseReference reference = new KnowledgeBaseReference("reference", 0) { - @Override - public JsonWriter toJson(JsonWriter writer) throws IOException { - referenceWrites.incrementAndGet(); - throw new IOException("The event wrapper must not serialize its payload."); - } - }; - List references = Collections.singletonList(reference); - KnowledgeBaseReferencesCompletedStreamEvent array = new KnowledgeBaseReferencesCompletedStreamEvent(references); - assertSame(references, array.getValue()); - assertSame(reference, array.getValue().get(0)); - assertEquals(0, referenceWrites.get()); - } - @ParameterizedTest @MethodSource("knownEvents") public void everyKnownVariantRejectsMalformedData(String eventName, String json) { @@ -450,9 +371,10 @@ public void clientsEmitErrorEventInclusivelyWithoutParsingFollowingMalformedPayl .verifyComplete(); } - @Test - public void clientsPropagateMalformedKnownPayload() throws IOException { - String body = "event: response.completed\ndata: {\n\n"; + @ParameterizedTest + @MethodSource("knownEvents") + public void clientsPropagateMalformedKnownPayload(String eventName, String json) throws IOException { + String body = frame(eventName, "references.completed".equals(eventName) ? "[{" : "{"); try (CloseableIterableStream stream = createBuilder(body, null).buildClient().retrieveStream(new KnowledgeBaseRetrievalOptions())) { @@ -507,6 +429,7 @@ private static void assertStreamEvents(List e assertEquals(3, events.size()); assertInstanceOf(KnowledgeBaseRetrievalStartedStreamEvent.class, events.get(0)); assertEvent(events.get(0), "retrieval.started", false); + assertEquals(KnowledgeBaseRetrievalStreamEvent.class, events.get(1).getClass()); assertEvent(events.get(1), "future.event", false); assertInstanceOf(KnowledgeBaseResponseCompletedStreamEvent.class, events.get(2)); assertEvent(events.get(2), "response.completed", true); @@ -567,40 +490,6 @@ private static Object payload(KnowledgeBaseRetrievalStreamEvent event) { } } - private static KnowledgeBaseRetrievalStreamEvent withPayload(KnowledgeBaseRetrievalStreamEvent event) { - switch (event.getEventName()) { - case "retrieval.started": - return new KnowledgeBaseRetrievalStartedStreamEvent( - ((KnowledgeBaseRetrievalStartedStreamEvent) event).getValue()); - - case "activity.started": - return new KnowledgeBaseActivityStartedStreamEvent( - ((KnowledgeBaseActivityStartedStreamEvent) event).getValue()); - - case "activity.completed": - return new KnowledgeBaseActivityCompletedStreamEvent( - ((KnowledgeBaseActivityCompletedStreamEvent) event).getValue()); - - case "answer.completed": - return new KnowledgeBaseAnswerCompletedStreamEvent( - ((KnowledgeBaseAnswerCompletedStreamEvent) event).getValue()); - - case "references.completed": - return new KnowledgeBaseReferencesCompletedStreamEvent( - ((KnowledgeBaseReferencesCompletedStreamEvent) event).getValue()); - - case "error": - return new KnowledgeBaseErrorStreamEvent(((KnowledgeBaseErrorStreamEvent) event).getValue()); - - case "response.completed": - return new KnowledgeBaseResponseCompletedStreamEvent( - ((KnowledgeBaseResponseCompletedStreamEvent) event).getValue()); - - default: - throw new IllegalArgumentException("Expected a known event: " + event.getEventName()); - } - } - private static KnowledgeBaseRetrievalClientBuilder createBuilder(Flux responseBody) { HttpHeaders headers = new HttpHeaders().set(HttpHeaderName.CONTENT_TYPE, "text/event-stream"); return new KnowledgeBaseRetrievalClientBuilder().endpoint("https://example.search.windows.net") diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEventTests.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEventTests.java new file mode 100644 index 0000000000000..3cef60c9b73a4 --- /dev/null +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEventTests.java @@ -0,0 +1,313 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.search.documents.knowledgebases.models; + +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; +import com.azure.search.documents.knowledgebases.implementation.KnowledgeBaseRetrievalStreamEventConverter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class KnowledgeBaseRetrievalStreamEventTests { + private static final String CONVERTER_NAME = KnowledgeBaseRetrievalStreamEventConverter.class.getName(); + + @Test + public void baseIsConcreteAndNonFinal() { + assertTrue(Modifier.isPublic(KnowledgeBaseRetrievalStreamEvent.class.getModifiers())); + assertFalse(Modifier.isAbstract(KnowledgeBaseRetrievalStreamEvent.class.getModifiers())); + assertFalse(Modifier.isFinal(KnowledgeBaseRetrievalStreamEvent.class.getModifiers())); + KnowledgeBaseRetrievalStreamEvent event = new KnowledgeBaseRetrievalStreamEvent("future.event"); + assertEquals(KnowledgeBaseRetrievalStreamEvent.class, event.getClass()); + assertEquals("future.event", event.getEventName()); + assertFalse(event.isTerminal()); + } + + private static Stream wrapperConstructors() { + return Stream.of(Arguments.of(KnowledgeBaseRetrievalStreamEvent.class, String.class), + Arguments.of(KnowledgeBaseRetrievalStartedStreamEvent.class, KnowledgeBaseRetrievalStartedEvent.class), + Arguments.of(KnowledgeBaseActivityStartedStreamEvent.class, KnowledgeBaseActivityStartedEvent.class), + Arguments.of(KnowledgeBaseActivityCompletedStreamEvent.class, KnowledgeBaseActivityRecord.class), + Arguments.of(KnowledgeBaseAnswerCompletedStreamEvent.class, KnowledgeBaseAnswerCompletedEvent.class), + Arguments.of(KnowledgeBaseReferencesCompletedStreamEvent.class, List.class), + Arguments.of(KnowledgeBaseErrorStreamEvent.class, KnowledgeBaseStreamErrorEvent.class), + Arguments.of(KnowledgeBaseResponseCompletedStreamEvent.class, KnowledgeBaseResponseCompletedEvent.class)); + } + + @ParameterizedTest + @MethodSource("wrapperConstructors") + public void wrappersHaveOnlyExactPackagePrivateConstructorsAndEnvelopeApis(Class wrapper, Class payloadType) + throws NoSuchMethodException { + assertTrue(Modifier.isPublic(wrapper.getModifiers())); + assertEquals(1, wrapper.getDeclaredConstructors().length); + assertEquals(0, wrapper.getConstructors().length); + Constructor constructor = wrapper.getDeclaredConstructor(payloadType); + assertEquals(0, constructor.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE)); + assertFalse(JsonSerializable.class.isAssignableFrom(wrapper)); + List methods = Arrays.stream(wrapper.getDeclaredMethods()) + .filter(method -> !method.isSynthetic() && Modifier.isPublic(method.getModifiers())) + .map(Method::getName) + .sorted() + .collect(Collectors.toList()); + if (wrapper == KnowledgeBaseRetrievalStreamEvent.class) { + assertEquals(Arrays.asList("getEventName", "isTerminal"), methods); + } else { + assertTrue(Modifier.isFinal(wrapper.getModifiers())); + assertEquals(wrapper == KnowledgeBaseErrorStreamEvent.class + || wrapper == KnowledgeBaseResponseCompletedStreamEvent.class + ? Arrays.asList("getValue", "isTerminal") + : Collections.singletonList("getValue"), + methods); + } + assertEquals( + Collections.singletonList(wrapper == KnowledgeBaseRetrievalStreamEvent.class ? "eventName" : "value"), + Arrays.stream(wrapper.getDeclaredFields()) + .filter(field -> !field.isSynthetic()) + .map(field -> field.getName()) + .collect(Collectors.toList())); + } + + private static Stream knownEvents() { + return Stream.of(Arguments.of("retrieval.started", "{\"requestId\":\"request\",\"knowledgeBaseName\":\"kb\"}"), + Arguments.of("activity.started", + "{\"id\":0,\"type\":\"searchIndex\",\"startedAt\":\"2025-01-01T00:00:00Z\"}"), + Arguments.of("activity.completed", + "{\"type\":\"searchIndex\",\"id\":0,\"knowledgeSourceName\":\"source\"}"), + Arguments.of("answer.completed", + "{\"messageIndex\":0,\"message\":{\"content\":[{\"type\":\"text\",\"text\":\"answer\"}]}}"), + Arguments.of("references.completed", + "[{\"type\":\"searchIndex\",\"id\":\"reference\",\"activitySource\":0,\"docKey\":\"document\"}]"), + Arguments.of("error", "{\"error\":{\"code\":\"BadRequest\",\"message\":\"bad request\"}}"), + Arguments.of("response.completed", "{\"statusCode\":200,\"response\":{}}")); + } + + @ParameterizedTest + @MethodSource("knownEvents") + public void payloadOnlyConstructorsRetainTypedPayloads(String eventName, String json) { + KnowledgeBaseRetrievalStreamEvent converted + = KnowledgeBaseRetrievalStreamEventConverter.convert(eventName, json); + KnowledgeBaseRetrievalStreamEvent event = withPayload(converted); + assertSame(payload(converted), payload(event)); + assertEquals(converted.getClass(), event.getClass()); + assertEquals(eventName, event.getEventName()); + assertEquals("error".equals(eventName) || "response.completed".equals(eventName), event.isTerminal()); + if ("activity.completed".equals(eventName)) { + assertInstanceOf(KnowledgeBaseSearchIndexActivityRecord.class, payload(event)); + } + if ("references.completed".equals(eventName)) { + assertInstanceOf(KnowledgeBaseSearchIndexReference.class, + ((KnowledgeBaseReferencesCompletedStreamEvent) event).getValue().get(0)); + } + } + + @Test + public void nullPayloadsAndEmptyReferenceListsRemainAccessible() { + List absent = Arrays.asList( + new KnowledgeBaseRetrievalStartedStreamEvent(null), new KnowledgeBaseActivityStartedStreamEvent(null), + new KnowledgeBaseActivityCompletedStreamEvent(null), new KnowledgeBaseAnswerCompletedStreamEvent(null), + new KnowledgeBaseReferencesCompletedStreamEvent(null), new KnowledgeBaseErrorStreamEvent(null), + new KnowledgeBaseResponseCompletedStreamEvent(null)); + absent.forEach(event -> assertNull(payload(event))); + List empty = Collections.emptyList(); + assertSame(empty, new KnowledgeBaseReferencesCompletedStreamEvent(empty).getValue()); + } + + @Test + public void constructorsAndPayloadAccessNeverSerialize() { + AtomicInteger writes = new AtomicInteger(); + KnowledgeBaseActivityRecord activity = new KnowledgeBaseActivityRecord(0) { + @Override + public JsonWriter toJson(JsonWriter writer) throws IOException { + writes.incrementAndGet(); + throw new IOException("The event wrapper must not serialize its payload."); + } + }; + KnowledgeBaseActivityCompletedStreamEvent event = new KnowledgeBaseActivityCompletedStreamEvent(activity); + assertSame(activity, event.getValue()); + assertSame(activity, event.getValue()); + assertEquals(0, writes.get()); + + AtomicInteger referenceWrites = new AtomicInteger(); + KnowledgeBaseReference reference = new KnowledgeBaseReference("reference", 0) { + @Override + public JsonWriter toJson(JsonWriter writer) throws IOException { + referenceWrites.incrementAndGet(); + throw new IOException("The event wrapper must not serialize its payload."); + } + }; + List references = Collections.singletonList(reference); + KnowledgeBaseReferencesCompletedStreamEvent array = new KnowledgeBaseReferencesCompletedStreamEvent(references); + assertSame(references, array.getValue()); + assertSame(reference, array.getValue().get(0)); + assertEquals(0, referenceWrites.get()); + } + + @ParameterizedTest + @ValueSource(strings = { "future.event", "retrieval.started" }) + public void converterInitializesConstructorsOnFirstUse(String eventName) throws Exception { + try (URLClassLoader loader = createEventClassLoader()) { + Class converter = Class.forName(CONVERTER_NAME, true, loader); + Object event = converter.getMethod("convert", String.class, String.class) + .invoke(null, eventName, "retrieval.started".equals(eventName) ? "{}" : "not json"); + assertEquals(loader, event.getClass().getClassLoader()); + assertEquals("retrieval.started".equals(eventName) + ? KnowledgeBaseRetrievalStartedStreamEvent.class.getName() + : KnowledgeBaseRetrievalStreamEvent.class.getName(), event.getClass().getName()); + assertEquals(eventName, event.getClass().getMethod("getEventName").invoke(event)); + assertEquals(false, event.getClass().getMethod("isTerminal").invoke(event)); + } + } + + @Test + public void modelInitializationRegistersConstructorsBeforeConverterUse() throws Exception { + try (URLClassLoader loader = createEventClassLoader()) { + Class base = Class.forName(KnowledgeBaseRetrievalStreamEvent.class.getName(), true, loader); + Class converter = Class.forName(CONVERTER_NAME, true, loader); + Object event + = converter.getMethod("convert", String.class, String.class).invoke(null, "future.event", "not json"); + assertSame(base, event.getClass()); + assertEquals("future.event", base.getMethod("getEventName").invoke(event)); + assertEquals(false, base.getMethod("isTerminal").invoke(event)); + } + } + + @Test + public void converterInitializesConstructorsOnConcurrentFirstUse() throws Exception { + try (URLClassLoader loader = createEventClassLoader()) { + Class converter = Class.forName(CONVERTER_NAME, true, loader); + Method convert = converter.getMethod("convert", String.class, String.class); + ExecutorService executor = Executors.newFixedThreadPool(8); + CountDownLatch ready = new CountDownLatch(8); + CountDownLatch start = new CountDownLatch(1); + try { + List> results = new ArrayList<>(); + for (int i = 0; i < 8; i++) { + String eventName = i % 2 == 0 ? "future.event" : "retrieval.started"; + results.add(executor.submit(() -> { + ready.countDown(); + assertTrue(start.await(30, TimeUnit.SECONDS)); + return convert.invoke(null, eventName, "{}"); + })); + } + assertTrue(ready.await(30, TimeUnit.SECONDS)); + start.countDown(); + for (int i = 0; i < results.size(); i++) { + Object event = results.get(i).get(30, TimeUnit.SECONDS); + assertSame(loader, event.getClass().getClassLoader()); + assertEquals(i % 2 == 0 + ? KnowledgeBaseRetrievalStreamEvent.class.getName() + : KnowledgeBaseRetrievalStartedStreamEvent.class.getName(), event.getClass().getName()); + assertEquals(false, event.getClass().getMethod("isTerminal").invoke(event)); + } + } finally { + start.countDown(); + executor.shutdownNow(); + assertTrue(executor.awaitTermination(30, TimeUnit.SECONDS)); + } + } + } + + private static URLClassLoader createEventClassLoader() throws IOException { + List urls = new ArrayList<>(); + urls.add(KnowledgeBaseRetrievalStreamEvent.class.getProtectionDomain().getCodeSource().getLocation()); + String classPath = System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")) + + File.pathSeparator + System.getProperty("jdk.module.path", ""); + for (String entry : classPath.split(File.pathSeparator)) { + urls.add(new File(entry).toURI().toURL()); + } + // A platform parent can route named SDK modules back to the already initialized application loader. + return URLClassLoader.newInstance(urls.toArray(new URL[0]), null); + } + + private static Object payload(KnowledgeBaseRetrievalStreamEvent event) { + switch (event.getEventName()) { + case "retrieval.started": + return ((KnowledgeBaseRetrievalStartedStreamEvent) event).getValue(); + + case "activity.started": + return ((KnowledgeBaseActivityStartedStreamEvent) event).getValue(); + + case "activity.completed": + return ((KnowledgeBaseActivityCompletedStreamEvent) event).getValue(); + + case "answer.completed": + return ((KnowledgeBaseAnswerCompletedStreamEvent) event).getValue(); + + case "references.completed": + return ((KnowledgeBaseReferencesCompletedStreamEvent) event).getValue(); + + case "error": + return ((KnowledgeBaseErrorStreamEvent) event).getValue(); + + case "response.completed": + return ((KnowledgeBaseResponseCompletedStreamEvent) event).getValue(); + + default: + throw new IllegalArgumentException("Expected a known event: " + event.getEventName()); + } + } + + private static KnowledgeBaseRetrievalStreamEvent withPayload(KnowledgeBaseRetrievalStreamEvent event) { + switch (event.getEventName()) { + case "retrieval.started": + return new KnowledgeBaseRetrievalStartedStreamEvent( + ((KnowledgeBaseRetrievalStartedStreamEvent) event).getValue()); + + case "activity.started": + return new KnowledgeBaseActivityStartedStreamEvent( + ((KnowledgeBaseActivityStartedStreamEvent) event).getValue()); + + case "activity.completed": + return new KnowledgeBaseActivityCompletedStreamEvent( + ((KnowledgeBaseActivityCompletedStreamEvent) event).getValue()); + + case "answer.completed": + return new KnowledgeBaseAnswerCompletedStreamEvent( + ((KnowledgeBaseAnswerCompletedStreamEvent) event).getValue()); + + case "references.completed": + return new KnowledgeBaseReferencesCompletedStreamEvent( + ((KnowledgeBaseReferencesCompletedStreamEvent) event).getValue()); + + case "error": + return new KnowledgeBaseErrorStreamEvent(((KnowledgeBaseErrorStreamEvent) event).getValue()); + + case "response.completed": + return new KnowledgeBaseResponseCompletedStreamEvent( + ((KnowledgeBaseResponseCompletedStreamEvent) event).getValue()); + + default: + throw new IllegalArgumentException("Expected a known event: " + event.getEventName()); + } + } +} From 86155781152dac0a97b05fa2f2df680127b0ad7e Mon Sep 17 00:00:00 2001 From: "Xiaofei Cao (from Dev Box)" Date: Fri, 16 Oct 2026 11:21:44 +0800 Subject: [PATCH 2/5] Remove added retrieval stream event model tests Delete the newly added model test file as requested without changing existing tests or production code. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa365f59-c459-44fc-8076-1d59c78dc0b8 --- ...nowledgeBaseRetrievalStreamEventTests.java | 313 ------------------ 1 file changed, 313 deletions(-) delete mode 100644 sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEventTests.java diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEventTests.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEventTests.java deleted file mode 100644 index 3cef60c9b73a4..0000000000000 --- a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEventTests.java +++ /dev/null @@ -1,313 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.search.documents.knowledgebases.models; - -import com.azure.json.JsonSerializable; -import com.azure.json.JsonWriter; -import com.azure.search.documents.knowledgebases.implementation.KnowledgeBaseRetrievalStreamEventConverter; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class KnowledgeBaseRetrievalStreamEventTests { - private static final String CONVERTER_NAME = KnowledgeBaseRetrievalStreamEventConverter.class.getName(); - - @Test - public void baseIsConcreteAndNonFinal() { - assertTrue(Modifier.isPublic(KnowledgeBaseRetrievalStreamEvent.class.getModifiers())); - assertFalse(Modifier.isAbstract(KnowledgeBaseRetrievalStreamEvent.class.getModifiers())); - assertFalse(Modifier.isFinal(KnowledgeBaseRetrievalStreamEvent.class.getModifiers())); - KnowledgeBaseRetrievalStreamEvent event = new KnowledgeBaseRetrievalStreamEvent("future.event"); - assertEquals(KnowledgeBaseRetrievalStreamEvent.class, event.getClass()); - assertEquals("future.event", event.getEventName()); - assertFalse(event.isTerminal()); - } - - private static Stream wrapperConstructors() { - return Stream.of(Arguments.of(KnowledgeBaseRetrievalStreamEvent.class, String.class), - Arguments.of(KnowledgeBaseRetrievalStartedStreamEvent.class, KnowledgeBaseRetrievalStartedEvent.class), - Arguments.of(KnowledgeBaseActivityStartedStreamEvent.class, KnowledgeBaseActivityStartedEvent.class), - Arguments.of(KnowledgeBaseActivityCompletedStreamEvent.class, KnowledgeBaseActivityRecord.class), - Arguments.of(KnowledgeBaseAnswerCompletedStreamEvent.class, KnowledgeBaseAnswerCompletedEvent.class), - Arguments.of(KnowledgeBaseReferencesCompletedStreamEvent.class, List.class), - Arguments.of(KnowledgeBaseErrorStreamEvent.class, KnowledgeBaseStreamErrorEvent.class), - Arguments.of(KnowledgeBaseResponseCompletedStreamEvent.class, KnowledgeBaseResponseCompletedEvent.class)); - } - - @ParameterizedTest - @MethodSource("wrapperConstructors") - public void wrappersHaveOnlyExactPackagePrivateConstructorsAndEnvelopeApis(Class wrapper, Class payloadType) - throws NoSuchMethodException { - assertTrue(Modifier.isPublic(wrapper.getModifiers())); - assertEquals(1, wrapper.getDeclaredConstructors().length); - assertEquals(0, wrapper.getConstructors().length); - Constructor constructor = wrapper.getDeclaredConstructor(payloadType); - assertEquals(0, constructor.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE)); - assertFalse(JsonSerializable.class.isAssignableFrom(wrapper)); - List methods = Arrays.stream(wrapper.getDeclaredMethods()) - .filter(method -> !method.isSynthetic() && Modifier.isPublic(method.getModifiers())) - .map(Method::getName) - .sorted() - .collect(Collectors.toList()); - if (wrapper == KnowledgeBaseRetrievalStreamEvent.class) { - assertEquals(Arrays.asList("getEventName", "isTerminal"), methods); - } else { - assertTrue(Modifier.isFinal(wrapper.getModifiers())); - assertEquals(wrapper == KnowledgeBaseErrorStreamEvent.class - || wrapper == KnowledgeBaseResponseCompletedStreamEvent.class - ? Arrays.asList("getValue", "isTerminal") - : Collections.singletonList("getValue"), - methods); - } - assertEquals( - Collections.singletonList(wrapper == KnowledgeBaseRetrievalStreamEvent.class ? "eventName" : "value"), - Arrays.stream(wrapper.getDeclaredFields()) - .filter(field -> !field.isSynthetic()) - .map(field -> field.getName()) - .collect(Collectors.toList())); - } - - private static Stream knownEvents() { - return Stream.of(Arguments.of("retrieval.started", "{\"requestId\":\"request\",\"knowledgeBaseName\":\"kb\"}"), - Arguments.of("activity.started", - "{\"id\":0,\"type\":\"searchIndex\",\"startedAt\":\"2025-01-01T00:00:00Z\"}"), - Arguments.of("activity.completed", - "{\"type\":\"searchIndex\",\"id\":0,\"knowledgeSourceName\":\"source\"}"), - Arguments.of("answer.completed", - "{\"messageIndex\":0,\"message\":{\"content\":[{\"type\":\"text\",\"text\":\"answer\"}]}}"), - Arguments.of("references.completed", - "[{\"type\":\"searchIndex\",\"id\":\"reference\",\"activitySource\":0,\"docKey\":\"document\"}]"), - Arguments.of("error", "{\"error\":{\"code\":\"BadRequest\",\"message\":\"bad request\"}}"), - Arguments.of("response.completed", "{\"statusCode\":200,\"response\":{}}")); - } - - @ParameterizedTest - @MethodSource("knownEvents") - public void payloadOnlyConstructorsRetainTypedPayloads(String eventName, String json) { - KnowledgeBaseRetrievalStreamEvent converted - = KnowledgeBaseRetrievalStreamEventConverter.convert(eventName, json); - KnowledgeBaseRetrievalStreamEvent event = withPayload(converted); - assertSame(payload(converted), payload(event)); - assertEquals(converted.getClass(), event.getClass()); - assertEquals(eventName, event.getEventName()); - assertEquals("error".equals(eventName) || "response.completed".equals(eventName), event.isTerminal()); - if ("activity.completed".equals(eventName)) { - assertInstanceOf(KnowledgeBaseSearchIndexActivityRecord.class, payload(event)); - } - if ("references.completed".equals(eventName)) { - assertInstanceOf(KnowledgeBaseSearchIndexReference.class, - ((KnowledgeBaseReferencesCompletedStreamEvent) event).getValue().get(0)); - } - } - - @Test - public void nullPayloadsAndEmptyReferenceListsRemainAccessible() { - List absent = Arrays.asList( - new KnowledgeBaseRetrievalStartedStreamEvent(null), new KnowledgeBaseActivityStartedStreamEvent(null), - new KnowledgeBaseActivityCompletedStreamEvent(null), new KnowledgeBaseAnswerCompletedStreamEvent(null), - new KnowledgeBaseReferencesCompletedStreamEvent(null), new KnowledgeBaseErrorStreamEvent(null), - new KnowledgeBaseResponseCompletedStreamEvent(null)); - absent.forEach(event -> assertNull(payload(event))); - List empty = Collections.emptyList(); - assertSame(empty, new KnowledgeBaseReferencesCompletedStreamEvent(empty).getValue()); - } - - @Test - public void constructorsAndPayloadAccessNeverSerialize() { - AtomicInteger writes = new AtomicInteger(); - KnowledgeBaseActivityRecord activity = new KnowledgeBaseActivityRecord(0) { - @Override - public JsonWriter toJson(JsonWriter writer) throws IOException { - writes.incrementAndGet(); - throw new IOException("The event wrapper must not serialize its payload."); - } - }; - KnowledgeBaseActivityCompletedStreamEvent event = new KnowledgeBaseActivityCompletedStreamEvent(activity); - assertSame(activity, event.getValue()); - assertSame(activity, event.getValue()); - assertEquals(0, writes.get()); - - AtomicInteger referenceWrites = new AtomicInteger(); - KnowledgeBaseReference reference = new KnowledgeBaseReference("reference", 0) { - @Override - public JsonWriter toJson(JsonWriter writer) throws IOException { - referenceWrites.incrementAndGet(); - throw new IOException("The event wrapper must not serialize its payload."); - } - }; - List references = Collections.singletonList(reference); - KnowledgeBaseReferencesCompletedStreamEvent array = new KnowledgeBaseReferencesCompletedStreamEvent(references); - assertSame(references, array.getValue()); - assertSame(reference, array.getValue().get(0)); - assertEquals(0, referenceWrites.get()); - } - - @ParameterizedTest - @ValueSource(strings = { "future.event", "retrieval.started" }) - public void converterInitializesConstructorsOnFirstUse(String eventName) throws Exception { - try (URLClassLoader loader = createEventClassLoader()) { - Class converter = Class.forName(CONVERTER_NAME, true, loader); - Object event = converter.getMethod("convert", String.class, String.class) - .invoke(null, eventName, "retrieval.started".equals(eventName) ? "{}" : "not json"); - assertEquals(loader, event.getClass().getClassLoader()); - assertEquals("retrieval.started".equals(eventName) - ? KnowledgeBaseRetrievalStartedStreamEvent.class.getName() - : KnowledgeBaseRetrievalStreamEvent.class.getName(), event.getClass().getName()); - assertEquals(eventName, event.getClass().getMethod("getEventName").invoke(event)); - assertEquals(false, event.getClass().getMethod("isTerminal").invoke(event)); - } - } - - @Test - public void modelInitializationRegistersConstructorsBeforeConverterUse() throws Exception { - try (URLClassLoader loader = createEventClassLoader()) { - Class base = Class.forName(KnowledgeBaseRetrievalStreamEvent.class.getName(), true, loader); - Class converter = Class.forName(CONVERTER_NAME, true, loader); - Object event - = converter.getMethod("convert", String.class, String.class).invoke(null, "future.event", "not json"); - assertSame(base, event.getClass()); - assertEquals("future.event", base.getMethod("getEventName").invoke(event)); - assertEquals(false, base.getMethod("isTerminal").invoke(event)); - } - } - - @Test - public void converterInitializesConstructorsOnConcurrentFirstUse() throws Exception { - try (URLClassLoader loader = createEventClassLoader()) { - Class converter = Class.forName(CONVERTER_NAME, true, loader); - Method convert = converter.getMethod("convert", String.class, String.class); - ExecutorService executor = Executors.newFixedThreadPool(8); - CountDownLatch ready = new CountDownLatch(8); - CountDownLatch start = new CountDownLatch(1); - try { - List> results = new ArrayList<>(); - for (int i = 0; i < 8; i++) { - String eventName = i % 2 == 0 ? "future.event" : "retrieval.started"; - results.add(executor.submit(() -> { - ready.countDown(); - assertTrue(start.await(30, TimeUnit.SECONDS)); - return convert.invoke(null, eventName, "{}"); - })); - } - assertTrue(ready.await(30, TimeUnit.SECONDS)); - start.countDown(); - for (int i = 0; i < results.size(); i++) { - Object event = results.get(i).get(30, TimeUnit.SECONDS); - assertSame(loader, event.getClass().getClassLoader()); - assertEquals(i % 2 == 0 - ? KnowledgeBaseRetrievalStreamEvent.class.getName() - : KnowledgeBaseRetrievalStartedStreamEvent.class.getName(), event.getClass().getName()); - assertEquals(false, event.getClass().getMethod("isTerminal").invoke(event)); - } - } finally { - start.countDown(); - executor.shutdownNow(); - assertTrue(executor.awaitTermination(30, TimeUnit.SECONDS)); - } - } - } - - private static URLClassLoader createEventClassLoader() throws IOException { - List urls = new ArrayList<>(); - urls.add(KnowledgeBaseRetrievalStreamEvent.class.getProtectionDomain().getCodeSource().getLocation()); - String classPath = System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")) - + File.pathSeparator + System.getProperty("jdk.module.path", ""); - for (String entry : classPath.split(File.pathSeparator)) { - urls.add(new File(entry).toURI().toURL()); - } - // A platform parent can route named SDK modules back to the already initialized application loader. - return URLClassLoader.newInstance(urls.toArray(new URL[0]), null); - } - - private static Object payload(KnowledgeBaseRetrievalStreamEvent event) { - switch (event.getEventName()) { - case "retrieval.started": - return ((KnowledgeBaseRetrievalStartedStreamEvent) event).getValue(); - - case "activity.started": - return ((KnowledgeBaseActivityStartedStreamEvent) event).getValue(); - - case "activity.completed": - return ((KnowledgeBaseActivityCompletedStreamEvent) event).getValue(); - - case "answer.completed": - return ((KnowledgeBaseAnswerCompletedStreamEvent) event).getValue(); - - case "references.completed": - return ((KnowledgeBaseReferencesCompletedStreamEvent) event).getValue(); - - case "error": - return ((KnowledgeBaseErrorStreamEvent) event).getValue(); - - case "response.completed": - return ((KnowledgeBaseResponseCompletedStreamEvent) event).getValue(); - - default: - throw new IllegalArgumentException("Expected a known event: " + event.getEventName()); - } - } - - private static KnowledgeBaseRetrievalStreamEvent withPayload(KnowledgeBaseRetrievalStreamEvent event) { - switch (event.getEventName()) { - case "retrieval.started": - return new KnowledgeBaseRetrievalStartedStreamEvent( - ((KnowledgeBaseRetrievalStartedStreamEvent) event).getValue()); - - case "activity.started": - return new KnowledgeBaseActivityStartedStreamEvent( - ((KnowledgeBaseActivityStartedStreamEvent) event).getValue()); - - case "activity.completed": - return new KnowledgeBaseActivityCompletedStreamEvent( - ((KnowledgeBaseActivityCompletedStreamEvent) event).getValue()); - - case "answer.completed": - return new KnowledgeBaseAnswerCompletedStreamEvent( - ((KnowledgeBaseAnswerCompletedStreamEvent) event).getValue()); - - case "references.completed": - return new KnowledgeBaseReferencesCompletedStreamEvent( - ((KnowledgeBaseReferencesCompletedStreamEvent) event).getValue()); - - case "error": - return new KnowledgeBaseErrorStreamEvent(((KnowledgeBaseErrorStreamEvent) event).getValue()); - - case "response.completed": - return new KnowledgeBaseResponseCompletedStreamEvent( - ((KnowledgeBaseResponseCompletedStreamEvent) event).getValue()); - - default: - throw new IllegalArgumentException("Expected a known event: " + event.getEventName()); - } - } -} From 2b8d81ceb3b05a237d5e83d4c5baa12391507108 Mon Sep 17 00:00:00 2001 From: "Xiaofei Cao (from Dev Box)" Date: Fri, 16 Oct 2026 12:18:24 +0800 Subject: [PATCH 3/5] Log exhausted SSE frame iterator exceptions Route the existing NoSuchElementException through ClientLogger to satisfy Checkstyle without changing iterator exhaustion semantics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa365f59-c459-44fc-8076-1d59c78dc0b8 --- .../models/implementation/sse/ServerSentEventStream.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/implementation/sse/ServerSentEventStream.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/implementation/sse/ServerSentEventStream.java index 6aff6eb7a3a09..b96df5a7953e6 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/implementation/sse/ServerSentEventStream.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/models/implementation/sse/ServerSentEventStream.java @@ -7,6 +7,7 @@ import com.azure.core.util.BinaryData; import com.azure.core.util.CloseableIterableStream; import com.azure.core.util.FluxUtil; +import com.azure.core.util.logging.ClientLogger; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; @@ -30,6 +31,7 @@ * Parses one server-sent event response. */ final class ServerSentEventStream { + private static final ClientLogger LOGGER = new ClientLogger(ServerSentEventStream.class); private static final String DEFAULT_EVENT = "message"; private ServerSentEventStream() { @@ -161,7 +163,7 @@ public boolean hasNext() { @Override public ServerSentEventFrame next() { if (!hasNext()) { - throw new NoSuchElementException(); + throw LOGGER.logExceptionAsError(new NoSuchElementException()); } ServerSentEventFrame frame = next; next = null; From 6cb4df8713a84c08039b882580ad21519e862fbc Mon Sep 17 00:00:00 2001 From: "Xiaofei Cao (from Dev Box)" Date: Fri, 16 Oct 2026 12:39:51 +0800 Subject: [PATCH 4/5] Remove redundant event wrapper documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa365f59-c459-44fc-8076-1d59c78dc0b8 --- sdk/search/azure-search-documents/CHANGELOG.md | 2 -- .../customizations/src/main/java/SearchCustomizations.java | 7 ++----- .../models/KnowledgeBaseActivityCompletedStreamEvent.java | 1 - .../models/KnowledgeBaseActivityStartedStreamEvent.java | 1 - .../models/KnowledgeBaseAnswerCompletedStreamEvent.java | 1 - .../models/KnowledgeBaseErrorStreamEvent.java | 1 - .../KnowledgeBaseReferencesCompletedStreamEvent.java | 1 - .../models/KnowledgeBaseResponseCompletedStreamEvent.java | 1 - .../models/KnowledgeBaseRetrievalStartedStreamEvent.java | 1 - .../models/KnowledgeBaseRetrievalStreamEvent.java | 1 - 10 files changed, 2 insertions(+), 15 deletions(-) diff --git a/sdk/search/azure-search-documents/CHANGELOG.md b/sdk/search/azure-search-documents/CHANGELOG.md index 188e014007c5f..fc096cfc1d713 100644 --- a/sdk/search/azure-search-documents/CHANGELOG.md +++ b/sdk/search/azure-search-documents/CHANGELOG.md @@ -34,8 +34,6 @@ The following changes apply when upgrading from the preceding `12.1.0` beta rele `KnowledgeBaseRetrievalStreamEvent.getEventName()`. Their payloads are not available. - Removed JSON serialization and deserialization APIs from retrieval stream event wrappers. Their typed payload models retain these APIs. -- Made retrieval stream event wrapper constructors package-private so only the SDK creates these envelopes. - `KnowledgeBaseRetrievalStreamEvent` is now concrete and represents unrecognized, nonterminal events by name only. - Removed preview-only image vector queries, `VectorFilterMode.STRICT_POST_FILTER`, Azure Machine Learning vectorizers, and `AIFoundryModelCatalogName`. - Moved `KnowledgeSourceFileCapacity` to `com.azure.search.documents.indexes.models`. diff --git a/sdk/search/azure-search-documents/customizations/src/main/java/SearchCustomizations.java b/sdk/search/azure-search-documents/customizations/src/main/java/SearchCustomizations.java index 0a5a37ef88c05..64faa09e64247 100644 --- a/sdk/search/azure-search-documents/customizations/src/main/java/SearchCustomizations.java +++ b/sdk/search/azure-search-documents/customizations/src/main/java/SearchCustomizations.java @@ -303,7 +303,6 @@ private static String baseEventSource() { + "import com.azure.search.documents.knowledgebases.implementation." + "KnowledgeBaseRetrievalStreamEventConverter;\n" + "import java.util.List;\n\n" + "/**\n" + " * Base for polymorphic events emitted by a streaming knowledge base retrieval.\n" - + " * Instances are created by the SDK, not by applications.\n" + " * Known events expose typed payloads through their subtype's {@code getValue()} method.\n" + " * Unrecognized events use this concrete base class and expose only their names through\n" + " * {@link #getEventName()}; their payloads are not available and they are not terminal.\n" @@ -371,10 +370,8 @@ private static String wrapperSource(String className, String payloadType, String return header("com.azure.search.documents.knowledgebases.models") + "import com.azure.core.annotation.Generated;\n" + "import com.azure.core.annotation.Immutable;\n" + listImports + "\n" + "/**\n" + " * Represents the {@code " + eventName - + "} knowledge base retrieval stream event.\n" - + " * Instances are created by the SDK, not by applications.\n" - + " * Access the typed payload with {@link #getValue()}.\n" + " *\n" - + " * @see KnowledgeBaseRetrievalStreamEvent\n" + " */\n" + "@Immutable\n" + "public final class " + + "} knowledge base retrieval stream event.\n" + " * Access the typed payload with {@link #getValue()}.\n" + + " *\n" + " * @see KnowledgeBaseRetrievalStreamEvent\n" + " */\n" + "@Immutable\n" + "public final class " + className + " extends KnowledgeBaseRetrievalStreamEvent {\n" + " @Generated\n" + " private final " + valueType + " value;\n\n" + " /**\n" + " * Creates an event wrapper.\n" + " *\n" + " * @param value The event payload.\n" + " */\n" + " @Generated\n" + " " + className + "(" diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseActivityCompletedStreamEvent.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseActivityCompletedStreamEvent.java index 132d3600445e2..6094ad98ac9cb 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseActivityCompletedStreamEvent.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseActivityCompletedStreamEvent.java @@ -7,7 +7,6 @@ /** * Represents the {@code activity.completed} knowledge base retrieval stream event. - * Instances are created by the SDK, not by applications. * Access the typed payload with {@link #getValue()}. * * @see KnowledgeBaseRetrievalStreamEvent diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseActivityStartedStreamEvent.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseActivityStartedStreamEvent.java index dfe854a16a3ba..fb8e412e16db2 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseActivityStartedStreamEvent.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseActivityStartedStreamEvent.java @@ -7,7 +7,6 @@ /** * Represents the {@code activity.started} knowledge base retrieval stream event. - * Instances are created by the SDK, not by applications. * Access the typed payload with {@link #getValue()}. * * @see KnowledgeBaseRetrievalStreamEvent diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseAnswerCompletedStreamEvent.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseAnswerCompletedStreamEvent.java index e7d328aa80fc7..936804251290c 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseAnswerCompletedStreamEvent.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseAnswerCompletedStreamEvent.java @@ -7,7 +7,6 @@ /** * Represents the {@code answer.completed} knowledge base retrieval stream event. - * Instances are created by the SDK, not by applications. * Access the typed payload with {@link #getValue()}. * * @see KnowledgeBaseRetrievalStreamEvent diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseErrorStreamEvent.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseErrorStreamEvent.java index b9cc16b574248..4f552b8681fac 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseErrorStreamEvent.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseErrorStreamEvent.java @@ -7,7 +7,6 @@ /** * Represents the {@code error} knowledge base retrieval stream event. - * Instances are created by the SDK, not by applications. * Access the typed payload with {@link #getValue()}. * * @see KnowledgeBaseRetrievalStreamEvent diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseReferencesCompletedStreamEvent.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseReferencesCompletedStreamEvent.java index 0990599fd0b04..4ba6e51a0815c 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseReferencesCompletedStreamEvent.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseReferencesCompletedStreamEvent.java @@ -8,7 +8,6 @@ /** * Represents the {@code references.completed} knowledge base retrieval stream event. - * Instances are created by the SDK, not by applications. * Access the typed payload with {@link #getValue()}. * * @see KnowledgeBaseRetrievalStreamEvent diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseResponseCompletedStreamEvent.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseResponseCompletedStreamEvent.java index 850d1f7989f45..438c3bc039694 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseResponseCompletedStreamEvent.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseResponseCompletedStreamEvent.java @@ -7,7 +7,6 @@ /** * Represents the {@code response.completed} knowledge base retrieval stream event. - * Instances are created by the SDK, not by applications. * Access the typed payload with {@link #getValue()}. * * @see KnowledgeBaseRetrievalStreamEvent diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStartedStreamEvent.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStartedStreamEvent.java index e050780b8982e..a47db8de65ebf 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStartedStreamEvent.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStartedStreamEvent.java @@ -7,7 +7,6 @@ /** * Represents the {@code retrieval.started} knowledge base retrieval stream event. - * Instances are created by the SDK, not by applications. * Access the typed payload with {@link #getValue()}. * * @see KnowledgeBaseRetrievalStreamEvent diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEvent.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEvent.java index 8bea1b09ac775..bc562c8a72069 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEvent.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEvent.java @@ -8,7 +8,6 @@ /** * Base for polymorphic events emitted by a streaming knowledge base retrieval. - * Instances are created by the SDK, not by applications. * Known events expose typed payloads through their subtype's {@code getValue()} method. * Unrecognized events use this concrete base class and expose only their names through * {@link #getEventName()}; their payloads are not available and they are not terminal. From a5eeac0a7bd1a9bcb4888b76cde8da6cf6b993fc Mon Sep 17 00:00:00 2001 From: "Xiaofei Cao (from Dev Box)" Date: Fri, 16 Oct 2026 16:09:34 +0800 Subject: [PATCH 5/5] Restore abstract retrieval stream event base Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa365f59-c459-44fc-8076-1d59c78dc0b8 --- .../src/main/java/SearchCustomizations.java | 8 ++--- .../KnowledgeBaseRetrievalStreamEvent.java | 7 ++-- .../KnowledgeBaseRetrievalStreamTests.java | 33 +++++++++++-------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/sdk/search/azure-search-documents/customizations/src/main/java/SearchCustomizations.java b/sdk/search/azure-search-documents/customizations/src/main/java/SearchCustomizations.java index 64faa09e64247..e3b05500e0704 100644 --- a/sdk/search/azure-search-documents/customizations/src/main/java/SearchCustomizations.java +++ b/sdk/search/azure-search-documents/customizations/src/main/java/SearchCustomizations.java @@ -304,7 +304,7 @@ private static String baseEventSource() { + "KnowledgeBaseRetrievalStreamEventConverter;\n" + "import java.util.List;\n\n" + "/**\n" + " * Base for polymorphic events emitted by a streaming knowledge base retrieval.\n" + " * Known events expose typed payloads through their subtype's {@code getValue()} method.\n" - + " * Unrecognized events use this concrete base class and expose only their names through\n" + + " * Unrecognized events expose only their names through\n" + " * {@link #getEventName()}; their payloads are not available and they are not terminal.\n" + " * Stream events are envelopes, not JSON payload models; serialize the typed payload instead.\n" + " *\n" + " * @see KnowledgeBaseRetrievalStartedStreamEvent\n" + " * @see KnowledgeBaseActivityStartedStreamEvent\n" @@ -312,13 +312,13 @@ private static String baseEventSource() { + " * @see KnowledgeBaseAnswerCompletedStreamEvent\n" + " * @see KnowledgeBaseReferencesCompletedStreamEvent\n" + " * @see KnowledgeBaseErrorStreamEvent\n" + " * @see KnowledgeBaseResponseCompletedStreamEvent\n" + " */\n" - + "public class KnowledgeBaseRetrievalStreamEvent {\n" + " static {\n" + + "public abstract class KnowledgeBaseRetrievalStreamEvent {\n" + " static {\n" + " KnowledgeBaseRetrievalStreamEventConverter.setConstructorAccessor(\n" + " new KnowledgeBaseRetrievalStreamEventConverter.ConstructorAccessor() {\n" + " @Override\n" + " public KnowledgeBaseRetrievalStreamEvent createEvent(String eventName) {\n" - + " return new KnowledgeBaseRetrievalStreamEvent(eventName);\n" + " }\n\n" - + " @Override\n" + + " return new KnowledgeBaseRetrievalStreamEvent(eventName) { };\n" + + " }\n\n" + " @Override\n" + " public KnowledgeBaseRetrievalStartedStreamEvent createRetrievalStarted(\n" + " KnowledgeBaseRetrievalStartedEvent value) {\n" + " return new KnowledgeBaseRetrievalStartedStreamEvent(value);\n" diff --git a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEvent.java b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEvent.java index bc562c8a72069..41a8838398b71 100644 --- a/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEvent.java +++ b/sdk/search/azure-search-documents/src/main/java/com/azure/search/documents/knowledgebases/models/KnowledgeBaseRetrievalStreamEvent.java @@ -9,7 +9,7 @@ /** * Base for polymorphic events emitted by a streaming knowledge base retrieval. * Known events expose typed payloads through their subtype's {@code getValue()} method. - * Unrecognized events use this concrete base class and expose only their names through + * Unrecognized events expose only their names through * {@link #getEventName()}; their payloads are not available and they are not terminal. * Stream events are envelopes, not JSON payload models; serialize the typed payload instead. * @@ -21,7 +21,7 @@ * @see KnowledgeBaseErrorStreamEvent * @see KnowledgeBaseResponseCompletedStreamEvent */ -public class KnowledgeBaseRetrievalStreamEvent { +public abstract class KnowledgeBaseRetrievalStreamEvent { @Generated private final String eventName; @@ -62,7 +62,8 @@ public boolean isTerminal() { @Override public KnowledgeBaseRetrievalStreamEvent createEvent(String eventName) { - return new KnowledgeBaseRetrievalStreamEvent(eventName); + return new KnowledgeBaseRetrievalStreamEvent(eventName) { + }; } @Override diff --git a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/KnowledgeBaseRetrievalStreamTests.java b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/KnowledgeBaseRetrievalStreamTests.java index 7f4f0453b550f..44ce071e03dd0 100644 --- a/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/KnowledgeBaseRetrievalStreamTests.java +++ b/sdk/search/azure-search-documents/src/test/java/com/azure/search/documents/knowledgebases/KnowledgeBaseRetrievalStreamTests.java @@ -42,6 +42,7 @@ import reactor.test.StepVerifier; import java.io.IOException; +import java.lang.reflect.Modifier; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -70,6 +71,11 @@ public class KnowledgeBaseRetrievalStreamTests { + "\"reasoningEffort\":{\"kind\":\"low\"}}"; private static final String RESPONSE_COMPLETED_JSON = "{\"statusCode\":200,\"response\":{}}"; + @Test + public void baseEventRemainsAbstract() { + assertTrue(Modifier.isAbstract(KnowledgeBaseRetrievalStreamEvent.class.getModifiers())); + } + @Test public void convertsAllStreamEventVariants() { KnowledgeBaseRetrievalStreamEvent retrievalStarted @@ -128,12 +134,10 @@ public void preservesUnknownEventsAndRejectsMalformedKnownEvents() { KnowledgeBaseRetrievalStreamEvent event = KnowledgeBaseRetrievalStreamEventConverter.convert("future.event", "not json\nsecond line"); - assertEvent(event, "future.event", false); - assertEquals(KnowledgeBaseRetrievalStreamEvent.class, event.getClass()); + assertUnknownEvent(event, "future.event"); KnowledgeBaseRetrievalStreamEvent nullPayload = KnowledgeBaseRetrievalStreamEventConverter.convert("future.event", null); - assertEquals(KnowledgeBaseRetrievalStreamEvent.class, nullPayload.getClass()); - assertEvent(nullPayload, "future.event", false); + assertUnknownEvent(nullPayload, "future.event"); assertThrows(RuntimeException.class, () -> KnowledgeBaseRetrievalStreamEventConverter.convert("response.completed", "{")); } @@ -230,24 +234,19 @@ public void bothClientsPreserveAgenticReasoningInProgressAndCompletedResponse() public void unknownPayloadsAreDiscardedWithoutDisplacingLaterTerminalEvents(String data) throws IOException { KnowledgeBaseRetrievalStreamEvent converted = KnowledgeBaseRetrievalStreamEventConverter.convert("future.event", data); - assertEquals(KnowledgeBaseRetrievalStreamEvent.class, converted.getClass()); - assertEvent(converted, "future.event", false); + assertUnknownEvent(converted, "future.event"); String body = frame("future.event", data) + frame("response.completed", RESPONSE_COMPLETED_JSON); try (CloseableIterableStream stream = createBuilder(body, null).buildClient().retrieveStream(new KnowledgeBaseRetrievalOptions())) { Iterator iterator = stream.iterator(); KnowledgeBaseRetrievalStreamEvent event = iterator.next(); - assertEquals(KnowledgeBaseRetrievalStreamEvent.class, event.getClass()); - assertEvent(event, "future.event", false); + assertUnknownEvent(event, "future.event"); assertEvent(iterator.next(), "response.completed", true); assertFalse(iterator.hasNext()); } StepVerifier .create(createBuilder(body, null).buildAsyncClient().retrieveStream(new KnowledgeBaseRetrievalOptions())) - .assertNext(event -> { - assertEquals(KnowledgeBaseRetrievalStreamEvent.class, event.getClass()); - assertEvent(event, "future.event", false); - }) + .assertNext(event -> assertUnknownEvent(event, "future.event")) .assertNext(event -> assertEvent(event, "response.completed", true)) .verifyComplete(); } @@ -429,8 +428,7 @@ private static void assertStreamEvents(List e assertEquals(3, events.size()); assertInstanceOf(KnowledgeBaseRetrievalStartedStreamEvent.class, events.get(0)); assertEvent(events.get(0), "retrieval.started", false); - assertEquals(KnowledgeBaseRetrievalStreamEvent.class, events.get(1).getClass()); - assertEvent(events.get(1), "future.event", false); + assertUnknownEvent(events.get(1), "future.event"); assertInstanceOf(KnowledgeBaseResponseCompletedStreamEvent.class, events.get(2)); assertEvent(events.get(2), "response.completed", true); } @@ -524,6 +522,13 @@ private static String streamWithUnknownEvent() { + "data: " + RESPONSE_COMPLETED_JSON + "\n\n" + "event: response.completed\ndata: {\n\n"; } + private static void assertUnknownEvent(KnowledgeBaseRetrievalStreamEvent event, String eventName) { + assertEquals(KnowledgeBaseRetrievalStreamEvent.class, event.getClass().getSuperclass()); + assertTrue(event.getClass().isAnonymousClass()); + assertFalse(Modifier.isPublic(event.getClass().getModifiers())); + assertEvent(event, eventName, false); + } + private static void assertEvent(KnowledgeBaseRetrievalStreamEvent event, String eventName, boolean terminal) { assertEquals(eventName, event.getEventName()); assertEquals(terminal, event.isTerminal());