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/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()); + } + } +}