From 337fc5a768e132185a6f9410e9f943c20b496547 Mon Sep 17 00:00:00 2001 From: Liang Xu Date: Mon, 26 Oct 2026 14:50:59 +0800 Subject: [PATCH 1/3] [ai-agents] Handle replaced telephony roots during customization --- sdk/ai/azure-ai-agents/CHANGELOG.md | 2 + sdk/ai/azure-ai-agents/customizations/pom.xml | 15 +++- .../src/main/java/AgentsCustomizations.java | 36 +++++---- .../AgentsCustomizationsConstructorTests.java | 76 +++++++++++++++++++ 4 files changed, 112 insertions(+), 17 deletions(-) create mode 100644 sdk/ai/azure-ai-agents/customizations/src/test/java/AgentsCustomizationsConstructorTests.java diff --git a/sdk/ai/azure-ai-agents/CHANGELOG.md b/sdk/ai/azure-ai-agents/CHANGELOG.md index 6239e1bfd771f..ab2eca64009a5 100644 --- a/sdk/ai/azure-ai-agents/CHANGELOG.md +++ b/sdk/ai/azure-ai-agents/CHANGELOG.md @@ -8,6 +8,8 @@ ### Bugs Fixed +- Fixed code generation when a TypeSpec revision replaces telephony polymorphic base models. Constructor customization now applies only to emitted models, including the shared telephony address and binding roots. + ### Other Changes ## 2.8.0 (2026-10-19) diff --git a/sdk/ai/azure-ai-agents/customizations/pom.xml b/sdk/ai/azure-ai-agents/customizations/pom.xml index 3ba56b51d6c01..9715a05b45be2 100644 --- a/sdk/ai/azure-ai-agents/customizations/pom.xml +++ b/sdk/ai/azure-ai-agents/customizations/pom.xml @@ -15,4 +15,17 @@ azure-ai-agents-customizations 1.2.0-beta.1 jar - \ No newline at end of file + + + ../../../.. + + + + + org.junit.jupiter + junit-jupiter + 5.13.4 + test + + + diff --git a/sdk/ai/azure-ai-agents/customizations/src/main/java/AgentsCustomizations.java b/sdk/ai/azure-ai-agents/customizations/src/main/java/AgentsCustomizations.java index 5b80c6d17c8d9..ef6de02c3b1bc 100644 --- a/sdk/ai/azure-ai-agents/customizations/src/main/java/AgentsCustomizations.java +++ b/sdk/ai/azure-ai-agents/customizations/src/main/java/AgentsCustomizations.java @@ -147,22 +147,26 @@ private void protectPolymorphicBaseConstructors(LibraryCustomization customizati List classNames = Arrays.asList("AgentHarness", "AgentOptimizationConfigurationBase", "AgentOptimizationEvaluationSet", "AgentOptimizationTargetCompletionDataSource", "AgentOptimizationTargetConfiguration", "AgentOptimizationUserConversationSimulationDataSource", - "CreateTelephonyBindingInput", "EvaluationVoiceModelConfiguration", "OptimizationContext", - "MemoryItem", "MemoryStoreDefinition", - "RealtimeAudioFormat", "RealtimeClientEvent", "RealtimeConversationItem", - "RealtimeConversationItemMessage", "RealtimeMcpError", "RealtimeSessionConfigurationBase", - "RealtimeTurnDetection", "TelephonyOutboundRetryPolicy", "TelephonyTransferDestination", - "UserConversationSimulationInterruptionConfiguration", "VoiceAgentGreetingConfiguration", - "VoiceAgentInterimResponseConfiguration", "VoiceAgentSystemTool", "VoiceAgentTool", - "VoiceAgentTurnDetectionConfiguration", "VoiceConversationEngine"); - - for (String className : classNames) { - ClassCustomization classCustomization = customization.getClass(MODELS_PACKAGE, className); - classCustomization.customizeAst(ast -> ast.getClassByName(className).ifPresent(clazz -> clazz - .getConstructors() - .stream() - .filter(constructor -> constructor.isPublic()) - .forEach(constructor -> constructor.setModifiers(Modifier.Keyword.PROTECTED)))); + "CreateTelephonyBindingInput", "TelephonyBinding", "TelephonyAddress", "EvaluationVoiceModelConfiguration", + "OptimizationContext", "MemoryItem", "MemoryStoreDefinition", "RealtimeAudioFormat", "RealtimeClientEvent", + "RealtimeConversationItem", "RealtimeConversationItemMessage", "RealtimeMcpError", + "RealtimeSessionConfigurationBase", "RealtimeTurnDetection", "TelephonyOutboundRetryPolicy", + "TelephonyTransferDestination", "UserConversationSimulationInterruptionConfiguration", + "VoiceAgentGreetingConfiguration", "VoiceAgentInterimResponseConfiguration", "VoiceAgentSystemTool", + "VoiceAgentTool", "VoiceAgentTurnDetectionConfiguration", "VoiceConversationEngine"); + + // TypeSpec can remove or replace a polymorphic root. Protect every root that is + // actually emitted, without requiring retired models to remain in the package. + for (ClassCustomization classCustomization : customization.getPackage(MODELS_PACKAGE).listClasses()) { + String className = classCustomization.getClassName(); + if (!classNames.contains(className)) { + continue; + } + classCustomization.customizeAst(ast -> ast.getClassByName(className) + .ifPresent(clazz -> clazz.getConstructors() + .stream() + .filter(constructor -> constructor.isPublic()) + .forEach(constructor -> constructor.setModifiers(Modifier.Keyword.PROTECTED)))); } } diff --git a/sdk/ai/azure-ai-agents/customizations/src/test/java/AgentsCustomizationsConstructorTests.java b/sdk/ai/azure-ai-agents/customizations/src/test/java/AgentsCustomizationsConstructorTests.java new file mode 100644 index 0000000000000..a177adcda1b1a --- /dev/null +++ b/sdk/ai/azure-ai-agents/customizations/src/test/java/AgentsCustomizationsConstructorTests.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.azure.autorest.customization.Customization; +import com.azure.autorest.customization.LibraryCustomization; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.helpers.NOPLogger; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AgentsCustomizationsConstructorTests { + @Test + void protectsRetainedAndReplacementRootsWithoutRequiringRemovedModels() throws Exception { + Map models = new HashMap<>(); + for (String name : new String[] { + "AgentHarness", + "TelephonyBinding", + "TelephonyAddress", + "TelephonyOutboundRetryPolicy", + "UnrelatedModel" }) { + models.put(path(name), model(name)); + } + + Map result = constructorCustomization().run(models, NOPLogger.NOP_LOGGER); + + for (String name : new String[] { + "AgentHarness", + "TelephonyBinding", + "TelephonyAddress", + "TelephonyOutboundRetryPolicy" }) { + assertTrue(result.get(path(name)).contains("protected " + name + "()"), name); + } + assertTrue(result.get(path("UnrelatedModel")).contains("public UnrelatedModel()")); + } + + @Test + void protectsLegacyRootsWhenTheyAreStillEmitted() throws Exception { + Map models = new HashMap<>(); + models.put(path("CreateTelephonyBindingInput"), model("CreateTelephonyBindingInput")); + models.put(path("TelephonyTransferDestination"), model("TelephonyTransferDestination")); + + Map result = constructorCustomization().run(models, NOPLogger.NOP_LOGGER); + + assertTrue(result.get(path("CreateTelephonyBindingInput")).contains("protected CreateTelephonyBindingInput()")); + assertTrue( + result.get(path("TelephonyTransferDestination")).contains("protected TelephonyTransferDestination()")); + } + + private static Customization constructorCustomization() throws Exception { + Method method = AgentsCustomizations.class.getDeclaredMethod("protectPolymorphicBaseConstructors", + LibraryCustomization.class); + method.setAccessible(true); + return new Customization() { + @Override + public void customize(LibraryCustomization customization, Logger logger) { + try { + method.invoke(new AgentsCustomizations(), customization); + } catch (ReflectiveOperationException exception) { + throw new AssertionError(exception); + } + } + }; + } + + private static String path(String name) { + return "src/main/java/com/azure/ai/agents/models/" + name + ".java"; + } + + private static String model(String name) { + return "package com.azure.ai.agents.models;\npublic class " + name + " { public " + name + "() {} }\n"; + } +} From fc820aaa40a4136ad866b7f4774e6a584ac22710 Mon Sep 17 00:00:00 2001 From: Liang Xu Date: Tue, 27 Oct 2026 18:48:03 +0800 Subject: [PATCH 2/3] Remove changelog entry from customization-only fix --- sdk/ai/azure-ai-agents/CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/ai/azure-ai-agents/CHANGELOG.md b/sdk/ai/azure-ai-agents/CHANGELOG.md index ab2eca64009a5..6239e1bfd771f 100644 --- a/sdk/ai/azure-ai-agents/CHANGELOG.md +++ b/sdk/ai/azure-ai-agents/CHANGELOG.md @@ -8,8 +8,6 @@ ### Bugs Fixed -- Fixed code generation when a TypeSpec revision replaces telephony polymorphic base models. Constructor customization now applies only to emitted models, including the shared telephony address and binding roots. - ### Other Changes ## 2.8.0 (2026-10-19) From 6e7f6895557c2f6fd829234c79caef6eb0c089a6 Mon Sep 17 00:00:00 2001 From: Liang Xu Date: Tue, 27 Oct 2026 19:13:25 +0800 Subject: [PATCH 3/3] [ai-agents] Run constructor regressions with existing SDK tests --- sdk/ai/azure-ai-agents/customizations/pom.xml | 15 +--- .../AgentsCustomizationsConstructorTests.java | 76 ------------------- .../PolymorphicModelConstructorTests.java | 71 +++++++++++++++++ 3 files changed, 72 insertions(+), 90 deletions(-) delete mode 100644 sdk/ai/azure-ai-agents/customizations/src/test/java/AgentsCustomizationsConstructorTests.java create mode 100644 sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/models/PolymorphicModelConstructorTests.java diff --git a/sdk/ai/azure-ai-agents/customizations/pom.xml b/sdk/ai/azure-ai-agents/customizations/pom.xml index 9715a05b45be2..3ba56b51d6c01 100644 --- a/sdk/ai/azure-ai-agents/customizations/pom.xml +++ b/sdk/ai/azure-ai-agents/customizations/pom.xml @@ -15,17 +15,4 @@ azure-ai-agents-customizations 1.2.0-beta.1 jar - - - ../../../.. - - - - - org.junit.jupiter - junit-jupiter - 5.13.4 - test - - - + \ No newline at end of file diff --git a/sdk/ai/azure-ai-agents/customizations/src/test/java/AgentsCustomizationsConstructorTests.java b/sdk/ai/azure-ai-agents/customizations/src/test/java/AgentsCustomizationsConstructorTests.java deleted file mode 100644 index a177adcda1b1a..0000000000000 --- a/sdk/ai/azure-ai-agents/customizations/src/test/java/AgentsCustomizationsConstructorTests.java +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import com.azure.autorest.customization.Customization; -import com.azure.autorest.customization.LibraryCustomization; -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.helpers.NOPLogger; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -class AgentsCustomizationsConstructorTests { - @Test - void protectsRetainedAndReplacementRootsWithoutRequiringRemovedModels() throws Exception { - Map models = new HashMap<>(); - for (String name : new String[] { - "AgentHarness", - "TelephonyBinding", - "TelephonyAddress", - "TelephonyOutboundRetryPolicy", - "UnrelatedModel" }) { - models.put(path(name), model(name)); - } - - Map result = constructorCustomization().run(models, NOPLogger.NOP_LOGGER); - - for (String name : new String[] { - "AgentHarness", - "TelephonyBinding", - "TelephonyAddress", - "TelephonyOutboundRetryPolicy" }) { - assertTrue(result.get(path(name)).contains("protected " + name + "()"), name); - } - assertTrue(result.get(path("UnrelatedModel")).contains("public UnrelatedModel()")); - } - - @Test - void protectsLegacyRootsWhenTheyAreStillEmitted() throws Exception { - Map models = new HashMap<>(); - models.put(path("CreateTelephonyBindingInput"), model("CreateTelephonyBindingInput")); - models.put(path("TelephonyTransferDestination"), model("TelephonyTransferDestination")); - - Map result = constructorCustomization().run(models, NOPLogger.NOP_LOGGER); - - assertTrue(result.get(path("CreateTelephonyBindingInput")).contains("protected CreateTelephonyBindingInput()")); - assertTrue( - result.get(path("TelephonyTransferDestination")).contains("protected TelephonyTransferDestination()")); - } - - private static Customization constructorCustomization() throws Exception { - Method method = AgentsCustomizations.class.getDeclaredMethod("protectPolymorphicBaseConstructors", - LibraryCustomization.class); - method.setAccessible(true); - return new Customization() { - @Override - public void customize(LibraryCustomization customization, Logger logger) { - try { - method.invoke(new AgentsCustomizations(), customization); - } catch (ReflectiveOperationException exception) { - throw new AssertionError(exception); - } - } - }; - } - - private static String path(String name) { - return "src/main/java/com/azure/ai/agents/models/" + name + ".java"; - } - - private static String model(String name) { - return "package com.azure.ai.agents.models;\npublic class " + name + " { public " + name + "() {} }\n"; - } -} diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/models/PolymorphicModelConstructorTests.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/models/PolymorphicModelConstructorTests.java new file mode 100644 index 0000000000000..aa1ba18c2f754 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/models/PolymorphicModelConstructorTests.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.models; + +import com.azure.ai.agents.BetaVoiceAgentsTelephonyClient; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PolymorphicModelConstructorTests { + @ParameterizedTest + @ValueSource(classes = { AgentHarness.class, TelephonyBinding.class, TelephonyOutboundRetryPolicy.class }) + public void retainedRootsHaveProtectedConstructors(Class modelClass) { + assertProtectedConstructors(modelClass); + } + + @Test + public void bindingInputRootHasProtectedConstructors() { + Method createBinding = Arrays.stream(BetaVoiceAgentsTelephonyClient.class.getMethods()) + .filter(method -> method.getName().equals("createTelephonyBinding") + && method.getParameterCount() == 2 + && method.getParameterTypes()[0] == String.class) + .findFirst() + .orElseThrow(() -> new AssertionError("The typed createTelephonyBinding method must be present")); + + assertEquals(TelephonyBinding.class, createBinding.getReturnType()); + // Follow the generated API so both the legacy request root and its shared-model replacement are tested. + Class inputClass = createBinding.getParameterTypes()[1]; + assertTrue( + inputClass.getSimpleName().equals("CreateTelephonyBindingInput") || inputClass == TelephonyBinding.class, + inputClass.getName()); + assertProtectedConstructors(inputClass); + } + + @Test + public void transferDestinationRootHasProtectedConstructors() throws NoSuchMethodException { + // This is TelephonyTransferDestination before regeneration and TelephonyAddress afterwards. + Class destinationClass = TelephonyTransferTarget.class.getMethod("getDestination").getReturnType(); + assertTrue(destinationClass.getSimpleName().equals("TelephonyTransferDestination") + || destinationClass.getSimpleName().equals("TelephonyAddress"), destinationClass.getName()); + assertProtectedConstructors(destinationClass); + } + + @Test + public void unrelatedModelKeepsPublicConstructors() { + Constructor[] constructors = TelephonyTransferTarget.class.getDeclaredConstructors(); + assertTrue(constructors.length > 0); + for (Constructor constructor : constructors) { + assertTrue(Modifier.isPublic(constructor.getModifiers()), constructor.toString()); + } + } + + private static void assertProtectedConstructors(Class modelClass) { + assertTrue(Modifier.isPublic(modelClass.getModifiers()), modelClass.getName()); + assertFalse(Modifier.isAbstract(modelClass.getModifiers()), modelClass.getName()); + Constructor[] constructors = modelClass.getDeclaredConstructors(); + assertTrue(constructors.length > 0, modelClass.getName()); + for (Constructor constructor : constructors) { + assertTrue(Modifier.isProtected(constructor.getModifiers()), constructor.toString()); + } + } +}