From 49546ba0943add4ec9dbc2561274347081174480 Mon Sep 17 00:00:00 2001 From: Marimuthu Gurusamy Date: Thu, 20 Nov 2025 09:51:47 -0800 Subject: [PATCH 1/4] Adding support for configuring Messaging Endpoint --- .../Commands/SetupCommand.cs | 31 +++++- .../Models/Agent365Config.cs | 17 +++- .../Services/ConfigurationWizardService.cs | 82 +++++++++++++-- ...nfigCommandStaticDynamicSeparationTests.cs | 70 +++++++++++++ .../Commands/SetupCommandTests.cs | 32 ++++-- .../Models/Agent365ConfigTests.cs | 99 ++++++++++++++++++- 6 files changed, 307 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs index 5170e6b1..48234997 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs @@ -408,14 +408,16 @@ private static async Task RegisterBlueprintMessagingEndpointAsync( throw new InvalidOperationException("Agent Blueprint ID is required for messaging endpoint registration"); } - if (string.IsNullOrEmpty(setupConfig.WebAppName)) + if (string.IsNullOrEmpty(setupConfig.WebAppName) && string.IsNullOrEmpty(setupConfig.MessagingEndpoint)) { logger.LogError("Web App Name not configured in a365.config.json"); throw new InvalidOperationException("Web App Name is required for messaging endpoint registration"); } // Generate endpoint name with Azure Bot Service constraints (4-42 chars) - var baseEndpointName = $"{setupConfig.WebAppName}-endpoint"; + var baseEndpointName = !string.IsNullOrEmpty(setupConfig.WebAppName) + ? $"{setupConfig.WebAppName}-endpoint" + : $"{ExtractWebAppNameFromUrl(setupConfig.MessagingEndpoint)}-endpoint"; var endpointName = EndpointHelper.GetEndpointName(baseEndpointName); if (endpointName.Length < 4) { @@ -424,7 +426,7 @@ private static async Task RegisterBlueprintMessagingEndpointAsync( } // Register messaging endpoint using agent blueprint identity and deployed web app URL - var messagingEndpoint = $"https://{setupConfig.WebAppName}.azurewebsites.net/api/messages"; + var messagingEndpoint = !string.IsNullOrEmpty(setupConfig.WebAppName) ? $"https://{setupConfig.WebAppName}.azurewebsites.net/api/messages" : setupConfig.MessagingEndpoint; logger.LogInformation(" - Registering blueprint messaging endpoint"); logger.LogInformation(" * Endpoint Name: {EndpointName}", endpointName); @@ -604,6 +606,29 @@ private static void DisplaySetupSummary(SetupResults results, ILogger logger) logger.LogInformation("=========================================="); } + + /// + /// Extract web app name from Azure Web App URL (e.g., "SampleAgent" from "https://SampleAgent.azurewebsites.net/api/messages") + /// + private static string? ExtractWebAppNameFromUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) + return null; + + try + { + var host = new Uri(url).Host; + + // Split by '.' and take the first segment + var segments = host.Split('.'); + return segments.Length > 0 ? segments[0] : null; + } + catch + { + // If URL parsing fails, return null + return null; + } + } } /// diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Models/Agent365Config.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Models/Agent365Config.cs index ea4e4680..52d97188 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Models/Agent365Config.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Models/Agent365Config.cs @@ -28,8 +28,13 @@ public List Validate() if (string.IsNullOrWhiteSpace(SubscriptionId)) errors.Add("subscriptionId is required."); if (string.IsNullOrWhiteSpace(ResourceGroup)) errors.Add("resourceGroup is required."); if (string.IsNullOrWhiteSpace(Location)) errors.Add("location is required."); - if (string.IsNullOrWhiteSpace(AppServicePlanName)) errors.Add("appServicePlanName is required."); - if (string.IsNullOrWhiteSpace(WebAppName)) errors.Add("webAppName is required."); + + if (string.IsNullOrWhiteSpace(MessagingEndpoint)) + { + if (string.IsNullOrWhiteSpace(AppServicePlanName)) errors.Add("appServicePlanName is required."); + if (string.IsNullOrWhiteSpace(WebAppName)) errors.Add("webAppName is required."); + } + if (string.IsNullOrWhiteSpace(AgentIdentityDisplayName)) errors.Add("agentIdentityDisplayName is required."); if (string.IsNullOrWhiteSpace(DeploymentProjectPath)) errors.Add("deploymentProjectPath is required."); // agentIdentityScopes and agentApplicationScopes are now hardcoded defaults @@ -90,7 +95,7 @@ public List Validate() /// App Service Plan SKU/pricing tier (e.g., "B1", "S1", "P1v2"). /// [JsonPropertyName("appServicePlanSku")] - public string AppServicePlanSku { get; init; } = "B1"; + public string AppServicePlanSku { get; init; } = string.Empty; /// /// Name of the Azure Web App (must be globally unique). @@ -98,6 +103,12 @@ public List Validate() [JsonPropertyName("webAppName")] public string WebAppName { get; init; } = string.Empty; + /// + /// URL of the Messaging Endpoint (e.g., https://SampleAgent.azurewebsites.net/api/messages). + /// + [JsonPropertyName("messagingEndpoint")] + public string MessagingEndpoint { get; init; } = string.Empty; + #endregion #region Agent Configuration diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigurationWizardService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigurationWizardService.cs index c4b1df23..ba44dc00 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigurationWizardService.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigurationWizardService.cs @@ -107,12 +107,27 @@ private static string ExtractDomainFromAccount(AzureAccountInfo accountInfo) return null; } - // Step 6: Select App Service Plan - var appServicePlan = await PromptForAppServicePlanAsync(existingConfig, resourceGroup); - if (string.IsNullOrWhiteSpace(appServicePlan)) + // Step 6: Select Web App Service Plan or Messaging endpoint + string appServicePlan = string.Empty; + string messagingEndpoint = string.Empty; + + if (await PromptForWebAppCreateAsync(existingConfig, derivedNames)) { - _logger.LogError("Configuration wizard cancelled: App Service Plan not selected"); - return null; + appServicePlan = await PromptForAppServicePlanAsync(existingConfig, resourceGroup); + if (string.IsNullOrWhiteSpace(appServicePlan)) + { + _logger.LogError("Configuration wizard cancelled: App Service Plan not selected"); + return null; + } + } + else + { + messagingEndpoint = await PromptForMessagingEndpointAsync(existingConfig); + if (string.IsNullOrWhiteSpace(messagingEndpoint)) + { + _logger.LogError("Configuration wizard cancelled: Messaging Endpoint not provided"); + return null; + } } // Step 7: Get manager email (required for agent creation) @@ -132,7 +147,17 @@ private static string ExtractDomainFromAccount(AzureAccountInfo accountInfo) Console.WriteLine(" Configuration Summary"); Console.WriteLine("================================================================="); Console.WriteLine($"Agent Name : {agentName}"); - Console.WriteLine($"Web App Name : {derivedNames.WebAppName}"); + + if (string.IsNullOrWhiteSpace(messagingEndpoint)) + { + Console.WriteLine($"Web App Name : {derivedNames.WebAppName}"); + Console.WriteLine($"App Service Plan : {appServicePlan}"); + } + else + { + Console.WriteLine($"Messaging Endpoint : {messagingEndpoint}"); + } + Console.WriteLine($"Agent Identity Name : {derivedNames.AgentIdentityDisplayName}"); Console.WriteLine($"Agent Blueprint Name : {derivedNames.AgentBlueprintDisplayName}"); Console.WriteLine($"Agent UPN : {derivedNames.AgentUserPrincipalName}"); @@ -140,7 +165,6 @@ private static string ExtractDomainFromAccount(AzureAccountInfo accountInfo) Console.WriteLine($"Manager Email : {managerEmail}"); Console.WriteLine($"Deployment Path : {deploymentPath}"); Console.WriteLine($"Resource Group : {resourceGroup}"); - Console.WriteLine($"App Service Plan : {appServicePlan}"); Console.WriteLine($"Location : {location}"); Console.WriteLine($"Subscription : {accountInfo.Name} ({accountInfo.Id})"); Console.WriteLine($"Tenant : {accountInfo.TenantId}"); @@ -169,8 +193,9 @@ private static string ExtractDomainFromAccount(AzureAccountInfo accountInfo) Location = location, Environment = existingConfig?.Environment ?? "prod", // Default to prod, not asking for this AppServicePlanName = appServicePlan, - AppServicePlanSku = existingConfig?.AppServicePlanSku ?? "B1", // Default to B1, not asking - WebAppName = customizedNames.WebAppName, + AppServicePlanSku = string.IsNullOrWhiteSpace(appServicePlan) ? string.Empty : (existingConfig?.AppServicePlanSku ?? "B1"), // Default to B1, if appServicePlan is selected + WebAppName = string.IsNullOrWhiteSpace(appServicePlan) ? string.Empty : customizedNames.WebAppName, + MessagingEndpoint = messagingEndpoint, AgentIdentityDisplayName = customizedNames.AgentIdentityDisplayName, AgentBlueprintDisplayName = customizedNames.AgentBlueprintDisplayName, AgentUserPrincipalName = customizedNames.AgentUserPrincipalName, @@ -389,6 +414,31 @@ private string PromptForManagerEmail(Agent365Config? existingConfig, AzureAccoun ); } + private async Task PromptForWebAppCreateAsync(Agent365Config? existingConfig, ConfigDerivedNames? configDerivedNames) + { + Console.WriteLine(); + Console.Write($"Would you like to create a Web App [https://{configDerivedNames?.WebAppName}.azurewebsites.net] in Azure for this Agent? (Y/n): "); + var response = Console.ReadLine()?.Trim().ToLowerInvariant(); + + await Task.CompletedTask; // Satisfy async requirement + + // Default to Yes - only return false if explicitly "n" or "no" + return response != "n" && response != "no"; + } + + private async Task PromptForMessagingEndpointAsync(Agent365Config? existingConfig) + { + Console.WriteLine("Provide the messaging endpoint URL where your Agent will receive messages."); + Console.WriteLine("[Example: https://SampleAgent.azurewebsites.net/api/messages]"); + + await Task.CompletedTask; // Satisfy async requirement + return PromptWithDefault( + "Messaging endpoint URL", + existingConfig?.MessagingEndpoint ?? "", + ValidateUrl + ); + } + private async Task PromptForLocationAsync(Agent365Config? existingConfig, AzureAccountInfo accountInfo) { // Try to get a smart default location @@ -554,6 +604,20 @@ private static (bool isValid, string error) ValidateEmail(string input) return (true, ""); } + private static (bool isValid, string error) ValidateUrl(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return (false, "URL cannot be empty"); + + if (!Uri.TryCreate(input, UriKind.Absolute, out Uri? uri)) + return (false, "Must be a valid URL format"); + + if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) + return (false, "URL must use HTTP or HTTPS protocol"); + + return (true, ""); + } + private string GetUsageLocationFromAccount(AzureAccountInfo accountInfo) { // Default to US for now - could be enhanced to detect from account location diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/ConfigCommandStaticDynamicSeparationTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/ConfigCommandStaticDynamicSeparationTests.cs index d582ff21..253db0bc 100644 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/ConfigCommandStaticDynamicSeparationTests.cs +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/ConfigCommandStaticDynamicSeparationTests.cs @@ -354,6 +354,76 @@ public void GetGeneratedConfig_OnlyReturnsMutableProperties() "static property should NOT be included in GetGeneratedConfig()"); } + [Fact] + public async Task ConfigInit_WithWizard_MessagingEndpoint() + { + // Arrange + var logger = _loggerFactory.CreateLogger("Test"); + var configDir = GetTestConfigDir(); + var localConfigPath = Path.Combine(configDir, "a365.config.json"); + + // Create a mock wizard that returns a complete config object + var mockWizard = Substitute.For(); + var wizardResult = new Agent365Config + { + // Static properties (should be saved) + TenantId = "12345678-1234-1234-1234-123456789012", + SubscriptionId = "87654321-4321-4321-4321-210987654321", + ResourceGroup = "test-rg", + Location = "eastus", + MessagingEndpoint = "https://custom-endpoint.contoso.com/api/messages", + AgentIdentityDisplayName = "Test Agent", + AgentBlueprintDisplayName = "Test Blueprint", + AgentUserPrincipalName = "agent.test@contoso.com", + AgentUserDisplayName = "Test Agent User", + ManagerEmail = "manager@contoso.com", + AgentUserUsageLocation = "US", + DeploymentProjectPath = configDir, + AgentDescription = "Test Agent Description" + }; + + mockWizard.RunWizardAsync(Arg.Any()).Returns(wizardResult); + + var originalDir = Environment.CurrentDirectory; + try + { + Environment.CurrentDirectory = configDir; + + // Act - Run config init + var root = new RootCommand(); + root.AddCommand(ConfigCommand.CreateCommand(logger, configDir, mockWizard)); + var result = await root.InvokeAsync("config init"); + + // Assert + result.Should().Be(0, "command should succeed"); + File.Exists(localConfigPath).Should().BeTrue("config file should be created"); + + // Read the saved config file + var savedJson = await File.ReadAllTextAsync(localConfigPath); + var savedDoc = JsonDocument.Parse(savedJson); + var rootElement = savedDoc.RootElement; + + // Verify STATIC properties ARE present + rootElement.TryGetProperty("tenantId", out _).Should().BeTrue("static property tenantId should be saved"); + rootElement.TryGetProperty("subscriptionId", out _).Should().BeTrue("static property subscriptionId should be saved"); + rootElement.TryGetProperty("resourceGroup", out _).Should().BeTrue("static property resourceGroup should be saved"); + rootElement.TryGetProperty("location", out _).Should().BeTrue("static property location should be saved"); + rootElement.TryGetProperty("appServicePlanName", out _).Should().BeFalse("static property appServicePlanName should not be saved"); + rootElement.TryGetProperty("webAppName", out _).Should().BeFalse("static property webAppName should not be saved"); + rootElement.TryGetProperty("messagingEndpoint", out _).Should().BeTrue("static property messagingEndpoint should be saved"); + rootElement.TryGetProperty("agentIdentityDisplayName", out _).Should().BeTrue("static property agentIdentityDisplayName should be saved"); + rootElement.TryGetProperty("deploymentProjectPath", out _).Should().BeTrue("static property deploymentProjectPath should be saved"); + } + finally + { + Environment.CurrentDirectory = originalDir; + if (Directory.Exists(configDir)) + { + await CleanupTestDirectoryAsync(configDir); + } + } + } + /// /// Helper method to clean up test directories with retry logic /// diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/SetupCommandTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/SetupCommandTests.cs index 6585152b..a8c9fb64 100644 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/SetupCommandTests.cs +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/SetupCommandTests.cs @@ -1,19 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.IO; -using System.CommandLine.Parsing; -using Microsoft.Extensions.Logging; +using FluentAssertions; using Microsoft.Agents.A365.DevTools.Cli.Commands; using Microsoft.Agents.A365.DevTools.Cli.Models; using Microsoft.Agents.A365.DevTools.Cli.Services; +using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.IO; +using System.CommandLine.Parsing; using System.IO; +using System.Reflection; using System.Threading.Tasks; -using FluentAssertions; +using Xunit; namespace Microsoft.Agents.A365.DevTools.Cli.Tests.Commands; @@ -391,5 +392,22 @@ args[0] is LogLevel level && }); infoLogCount.Should().BeGreaterThan(0, "Setup should show success message when all steps complete"); } + + [Theory] + [InlineData("https://test.azurewebsites.net:8080/api/messages", "test")] + [InlineData("https://agent.azurewebsites.net:443/api/messages", "agent")] + public void ExtractWebAppNameFromUrl_HandlesPortNumbers(string input, string expectedName) + { + // Arrange + var method = typeof(SetupCommand) + .GetMethod("ExtractWebAppNameFromUrl", BindingFlags.NonPublic | BindingFlags.Static); + method.Should().NotBeNull(); + + // Act + var result = method!.Invoke(null, new object[] { input }) as string; + + // Assert + result.Should().Be(expectedName, "should extract subdomain correctly even with port number"); + } } diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Models/Agent365ConfigTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Models/Agent365ConfigTests.cs index f58c1beb..24d3ba23 100644 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Models/Agent365ConfigTests.cs +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Models/Agent365ConfigTests.cs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json; +using FluentAssertions; using Microsoft.Agents.A365.DevTools.Cli.Models; +using System.Text.Json; using Xunit; namespace Microsoft.Agents.A365.DevTools.Cli.Tests.Models; @@ -59,7 +60,6 @@ public void StaticProperties_HaveDefaultValues() }; // Assert - check default values - Assert.Equal("B1", config.AppServicePlanSku); // Has default Assert.NotNull(config.AgentIdentityScopes); // Hardcoded defaults Assert.NotEmpty(config.AgentIdentityScopes); // Should contain default scopes } @@ -350,4 +350,99 @@ public void Agent365Config_CanContainMcpServerConfigs() } #endregion + + #region MessagingEndpoint Tests + + [Fact] + public void Validate_WithMessagingEndpoint_DoesNotRequireAppServiceFields() + { + // Arrange + var config = new Agent365Config + { + TenantId = "00000000-0000-0000-0000-000000000000", + SubscriptionId = "11111111-1111-1111-1111-111111111111", + ResourceGroup = "test-rg", + Location = "eastus", + MessagingEndpoint = "https://external-agent.example.com/api/messages", + AgentIdentityDisplayName = "Test Agent Identity", + DeploymentProjectPath = "." + // AppServicePlanName and WebAppName not provided + }; + + // Act + var errors = config.Validate(); + + // Assert + errors.Should().BeEmpty("messaging endpoint makes App Service fields optional"); + } + + [Fact] + public void Validate_WithoutMessagingEndpoint_RequiresAppServiceFields() + { + // Arrange + var config = new Agent365Config + { + TenantId = "00000000-0000-0000-0000-000000000000", + SubscriptionId = "11111111-1111-1111-1111-111111111111", + ResourceGroup = "test-rg", + Location = "eastus", + AgentIdentityDisplayName = "Test Agent Identity", + DeploymentProjectPath = "." + // AppServicePlanName, WebAppName, and MessagingEndpoint not provided + }; + + // Act + var errors = config.Validate(); + + // Assert + errors.Should().Contain("appServicePlanName is required."); + errors.Should().Contain("webAppName is required."); + } + + [Fact] + public void Validate_WithEmptyMessagingEndpoint_RequiresAppServiceFields() + { + // Arrange + var config = new Agent365Config + { + TenantId = "00000000-0000-0000-0000-000000000000", + SubscriptionId = "11111111-1111-1111-1111-111111111111", + ResourceGroup = "test-rg", + Location = "eastus", + MessagingEndpoint = "", // Empty string should be treated as not provided + AgentIdentityDisplayName = "Test Agent Identity", + DeploymentProjectPath = "." + }; + + // Act + var errors = config.Validate(); + + // Assert + errors.Should().Contain("appServicePlanName is required."); + errors.Should().Contain("webAppName is required."); + } + + [Fact] + public void Validate_WithMessagingEndpoint_StillRequiresBaseFields() + { + // Arrange + var config = new Agent365Config + { + MessagingEndpoint = "https://external-agent.example.com/api/messages" + // Missing all required base fields + }; + + // Act + var errors = config.Validate(); + + // Assert + errors.Should().Contain("tenantId is required."); + errors.Should().Contain("subscriptionId is required."); + errors.Should().Contain("resourceGroup is required."); + errors.Should().Contain("location is required."); + errors.Should().Contain("agentIdentityDisplayName is required."); + errors.Should().Contain("deploymentProjectPath is required."); + } + + #endregion } From 80683deb5472ff0f793e5455cf212110ed9262f4 Mon Sep 17 00:00:00 2001 From: Marimuthu Gurusamy Date: Thu, 20 Nov 2025 15:39:58 -0800 Subject: [PATCH 2/4] Handling messaging endpoint for other commands --- .../Commands/CleanupCommand.cs | 52 +++++++++++++------ .../Commands/CreateInstanceCommand.cs | 4 +- .../Commands/DeployCommand.cs | 29 +++++++++++ .../Services/A365SetupRunner.cs | 20 +++++-- 4 files changed, 83 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs index 3448fda2..5d153118 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs @@ -162,11 +162,19 @@ private static Command CreateAzureCleanupCommand( } // Azure CLI cleanup commands - var commandsList = new List<(string, string)> + var commandsList = new List<(string, string)>(); + + // If WebAppName is configured + if (!string.IsNullOrWhiteSpace(config.WebAppName)) { - ($"az webapp delete --name {config.WebAppName} --resource-group {config.ResourceGroup} --subscription {config.SubscriptionId}", "Web App"), - ($"az appservice plan delete --name {config.AppServicePlanName} --resource-group {config.ResourceGroup} --subscription {config.SubscriptionId} --yes", "App Service Plan") - }; + commandsList.Add(($"az webapp delete --name {config.WebAppName} --resource-group {config.ResourceGroup} --subscription {config.SubscriptionId}", "Web App")); + } + + // Only add App Service Plan deletion if AppServicePlanName is configured + if (!string.IsNullOrWhiteSpace(config.AppServicePlanName)) + { + commandsList.Add(($"az appservice plan delete --name {config.AppServicePlanName} --resource-group {config.ResourceGroup} --subscription {config.SubscriptionId} --yes", "App Service Plan")); + } // Add bot deletion if bot exists if (!string.IsNullOrEmpty(config.BotName)) @@ -192,21 +200,31 @@ private static Command CreateAzureCleanupCommand( } } - var commands = commandsList.ToArray(); - - foreach (var (cmd, name) in commands) + // Check if there are any Azure resources to delete + if (commandsList.Count == 0) { - logger.LogInformation("Deleting {Name}...", name); - var parts = cmd.Split(' ', 2); - var result = await executor.ExecuteAsync(parts[0], parts[1], captureOutput: true); - - if (result.ExitCode == 0) - { - logger.LogInformation("{Name} deleted successfully", name); - } - else + logger.LogInformation("No Azure Web App resources found to clean up."); + logger.LogInformation("This agent is configured with an external messaging endpoint: {MessagingEndpoint}", + config.MessagingEndpoint ?? "(not configured)"); + } + else + { + var commands = commandsList.ToArray(); + + foreach (var (cmd, name) in commands) { - logger.LogWarning("Failed to delete {Name}: {Error}", name, result.StandardError); + logger.LogInformation("Deleting {Name}...", name); + var parts = cmd.Split(' ', 2); + var result = await executor.ExecuteAsync(parts[0], parts[1], captureOutput: true); + + if (result.ExitCode == 0) + { + logger.LogInformation("{Name} deleted successfully", name); + } + else + { + logger.LogWarning("Failed to delete {Name}: {Error}", name, result.StandardError); + } } } diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CreateInstanceCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CreateInstanceCommand.cs index 8ed5ce43..48812a0a 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CreateInstanceCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CreateInstanceCommand.cs @@ -222,8 +222,8 @@ public static Command CreateCommand(ILogger logger, IConf // Update Agent365Config state properties instanceConfig.BotId = instanceConfig.AgentBlueprintId ?? endpointName; instanceConfig.BotMsaAppId = instanceConfig.AgentBlueprintId; - instanceConfig.BotMessagingEndpoint = $"https://{instanceConfig.WebAppName}.azurewebsites.net/api/messages"; - + instanceConfig.BotMessagingEndpoint = !string.IsNullOrEmpty(instanceConfig.WebAppName) ? $"https://{instanceConfig.WebAppName}.azurewebsites.net/api/messages" : instanceConfig.MessagingEndpoint; + logger.LogInformation(" Agent Blueprint ID: {AgentBlueprintId}", instanceConfig.AgentBlueprintId); logger.LogInformation(" Agent Instance ID: {AgenticAppId}", instanceConfig.AgenticAppId); logger.LogInformation(" Agent User ID: {AgenticUserId}", instanceConfig.AgenticUserId); diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DeployCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DeployCommand.cs index 81c0f790..a866c2d5 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DeployCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DeployCommand.cs @@ -74,6 +74,12 @@ public static Command CreateCommand( return; } + // Check if web app deployment should be skipped (external messaging endpoint) + if (ShouldSkipWebAppDeployment(configData, logger)) + { + return; + } + var validatedConfig = await ValidateDeploymentPrerequisitesAsync( config.FullName, configService, azureValidator, executor, logger); if (validatedConfig == null) return; @@ -142,6 +148,12 @@ private static Command CreateAppSubcommand( return; } + // Check if web app deployment should be skipped (external messaging endpoint) + if (ShouldSkipWebAppDeployment(configData, logger)) + { + return; + } + var validatedConfig = await ValidateDeploymentPrerequisitesAsync( config.FullName, configService, azureValidator, executor, logger); if (validatedConfig == null) return; @@ -698,5 +710,22 @@ private static void HandleDeploymentException(Exception ex, ILogger logger) throw new DeployAppException($"Deployment failed: {ex.Message}", ex); } } + + /// + /// Determines if web app deployment should be skipped because an external messaging endpoint is configured + /// + /// Configuration to check + /// Logger for informational messages + /// True if deployment should be skipped, false otherwise + private static bool ShouldSkipWebAppDeployment(Agent365Config config, ILogger logger) + { + if (string.IsNullOrWhiteSpace(config.WebAppName) && !string.IsNullOrWhiteSpace(config.MessagingEndpoint)) + { + logger.LogInformation($"MessagingEndpoint {config.MessagingEndpoint} is already provided and hence WebApp deployment is skipped."); + return true; + } + + return false; + } } diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs index cc8e6c57..95d26f43 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs @@ -86,16 +86,30 @@ public async Task RunAsync(string configPath, string generatedConfigPath, var webAppName = Get("webAppName"); var location = Get("location"); var planSku = Get("appServicePlanSku"); + var messagingEndpoint = Get("messagingEndpoint"); if (string.IsNullOrWhiteSpace(planSku)) planSku = "B1"; var deploymentProjectPath = Get("deploymentProjectPath"); - if (new[] { subscriptionId, tenantId, resourceGroup, planName, webAppName, location }.Any(string.IsNullOrWhiteSpace)) + if (new[] { subscriptionId, tenantId, resourceGroup, location }.Any(string.IsNullOrWhiteSpace)) { - _logger.LogError("Config missing required properties. Need subscriptionId, tenantId, resourceGroup, appServicePlanName, webAppName, location."); + _logger.LogError("Config missing required properties. Need subscriptionId, tenantId, resourceGroup, location."); return false; } + // Validate Web App and Plan names or switch to blueprint-only mode + if (string.IsNullOrWhiteSpace(webAppName) || string.IsNullOrWhiteSpace(planName)) + { + if (string.IsNullOrWhiteSpace(messagingEndpoint)) + { + _logger.LogError("Config missing required properties. Need appServicePlanName, webAppName."); + return false; + } + + _logger.LogInformation("appServicePlanName or webAppName not specified, switching to --blueprint mode and skipping Web App creation."); + blueprintOnly = true; + } + // Detect project platform for appropriate runtime configuration var platform = Models.ProjectPlatform.DotNet; // Default fallback if (!string.IsNullOrWhiteSpace(deploymentProjectPath)) @@ -166,7 +180,7 @@ public async Task RunAsync(string configPath, string generatedConfigPath, if (!loginResult.Success) { - _logger.LogError("Azure CLI login with management scope failed. Please run manually: az login --scope https://management.core.windows.net//.default"); + _logger.LogError("Azure CLI login with management scope failed. Please run manually: az login --scope https://management.core.windows.net//.default"); return false; } From 12f09133d32b9d76038bdb1daee87e6a439c05d6 Mon Sep 17 00:00:00 2001 From: Sellakumaran Kanagarathnam Date: Fri, 21 Nov 2025 12:47:10 -0800 Subject: [PATCH 3/4] Refactor and enhance Azure CLI commands handling Refactored CleanupCommand to improve condition handling for App Service Plan deletion and introduced `config.NeedDeployment` for Web App deletion logic. Simplified DeployCommand by replacing `ShouldSkipWebAppDeployment` with a direct condition check. Updated SetupCommand to persist configuration changes and removed unused methods. Introduced `DefaultAppServicePlanSku` constant in ConfigConstants and replaced hardcoded defaults across files. Changed default `Environment` in Agent365Config to "prod" and improved validation logic. Replaced asynchronous prompts in ConfigurationWizardService with synchronous methods for simplicity. Removed redundant code and unused imports across the codebase for better maintainability. --- .../Commands/CleanupCommand.cs | 13 ++++---- .../Commands/CreateInstanceCommand.cs | 5 ---- .../Commands/DeployCommand.cs | 23 +++----------- .../Commands/SetupCommand.cs | 30 +++++-------------- .../Constants/ConfigConstants.cs | 2 ++ .../Exceptions/SetupValidationException.cs | 2 +- .../Models/Agent365Config.cs | 11 +++---- .../Services/A365SetupRunner.cs | 6 +--- .../Services/ConfigurationWizardService.cs | 26 ++++++++-------- 9 files changed, 37 insertions(+), 81 deletions(-) diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs index 5d153118..53f102bf 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs @@ -165,15 +165,14 @@ private static Command CreateAzureCleanupCommand( var commandsList = new List<(string, string)>(); // If WebAppName is configured - if (!string.IsNullOrWhiteSpace(config.WebAppName)) + if (config.NeedDeployment) { commandsList.Add(($"az webapp delete --name {config.WebAppName} --resource-group {config.ResourceGroup} --subscription {config.SubscriptionId}", "Web App")); - } - - // Only add App Service Plan deletion if AppServicePlanName is configured - if (!string.IsNullOrWhiteSpace(config.AppServicePlanName)) - { - commandsList.Add(($"az appservice plan delete --name {config.AppServicePlanName} --resource-group {config.ResourceGroup} --subscription {config.SubscriptionId} --yes", "App Service Plan")); + // Only add App Service Plan deletion if AppServicePlanName is configured + if (!string.IsNullOrWhiteSpace(config.AppServicePlanName)) + { + commandsList.Add(($"az appservice plan delete --name {config.AppServicePlanName} --resource-group {config.ResourceGroup} --subscription {config.SubscriptionId} --yes", "App Service Plan")); + } } // Add bot deletion if bot exists diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CreateInstanceCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CreateInstanceCommand.cs index 48812a0a..e28b93ca 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CreateInstanceCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CreateInstanceCommand.cs @@ -219,11 +219,6 @@ public static Command CreateCommand(ILogger logger, IConf // Update configuration with the populated values logger.LogInformation("Updating configuration with generated values..."); - // Update Agent365Config state properties - instanceConfig.BotId = instanceConfig.AgentBlueprintId ?? endpointName; - instanceConfig.BotMsaAppId = instanceConfig.AgentBlueprintId; - instanceConfig.BotMessagingEndpoint = !string.IsNullOrEmpty(instanceConfig.WebAppName) ? $"https://{instanceConfig.WebAppName}.azurewebsites.net/api/messages" : instanceConfig.MessagingEndpoint; - logger.LogInformation(" Agent Blueprint ID: {AgentBlueprintId}", instanceConfig.AgentBlueprintId); logger.LogInformation(" Agent Instance ID: {AgenticAppId}", instanceConfig.AgenticAppId); logger.LogInformation(" Agent User ID: {AgenticUserId}", instanceConfig.AgenticUserId); diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DeployCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DeployCommand.cs index b4e4c766..8921b10c 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DeployCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DeployCommand.cs @@ -74,8 +74,9 @@ public static Command CreateCommand( } // Check if web app deployment should be skipped (external messaging endpoint) - if (ShouldSkipWebAppDeployment(configData, logger)) + if (!configData.NeedDeployment) { + logger.LogInformation("Web App deployment is skipped as per configuration."); return; } @@ -148,8 +149,9 @@ private static Command CreateAppSubcommand( } // Check if web app deployment should be skipped (external messaging endpoint) - if (ShouldSkipWebAppDeployment(configData, logger)) + if (!configData.NeedDeployment) { + logger.LogInformation("Web App deployment is skipped as per configuration."); return; } @@ -472,22 +474,5 @@ private static void HandleDeploymentException(Exception ex, ILogger logger) throw new DeployAppException($"Deployment failed: {ex.Message}", ex); } } - - /// - /// Determines if web app deployment should be skipped because an external messaging endpoint is configured - /// - /// Configuration to check - /// Logger for informational messages - /// True if deployment should be skipped, false otherwise - private static bool ShouldSkipWebAppDeployment(Agent365Config config, ILogger logger) - { - if (string.IsNullOrWhiteSpace(config.WebAppName) && !string.IsNullOrWhiteSpace(config.MessagingEndpoint)) - { - logger.LogInformation($"MessagingEndpoint {config.MessagingEndpoint} is already provided and hence WebApp deployment is skipped."); - return true; - } - - return false; - } } diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs index 1b9d3d60..a7329d67 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs @@ -281,6 +281,7 @@ await EnsureMcpInheritablePermissionsAsync( setupConfig = await configService.LoadAsync(config.FullName); await RegisterBlueprintMessagingEndpointAsync(setupConfig, logger, botConfigurator); + await configService.SaveStateAsync(setupConfig); setupResults.MessagingEndpointRegistered = true; logger.LogInformation("Blueprint messaging endpoint registered successfully"); } @@ -493,6 +494,7 @@ private static async Task RegisterBlueprintMessagingEndpointAsync( var hostPart = uri.Host.Replace('.', '-'); var baseEndpointName = $"{hostPart}-endpoint"; endpointName = EndpointHelper.GetEndpointName(baseEndpointName); + } if (endpointName.Length < 4) @@ -518,6 +520,11 @@ private static async Task RegisterBlueprintMessagingEndpointAsync( logger.LogError("Failed to register blueprint messaging endpoint"); throw new SetupValidationException("Blueprint messaging endpoint registration failed"); } + // Update Agent365Config state properties + setupConfig.BotId = setupConfig.AgentBlueprintId; + setupConfig.BotMsaAppId = setupConfig.AgentBlueprintId; + setupConfig.BotMessagingEndpoint = messagingEndpoint; + } /// @@ -685,29 +692,6 @@ private static void DisplaySetupSummary(SetupResults results, ILogger logger) logger.LogInformation("=========================================="); } - - /// - /// Extract web app name from Azure Web App URL (e.g., "SampleAgent" from "https://SampleAgent.azurewebsites.net/api/messages") - /// - private static string? ExtractWebAppNameFromUrl(string url) - { - if (string.IsNullOrWhiteSpace(url)) - return null; - - try - { - var host = new Uri(url).Host; - - // Split by '.' and take the first segment - var segments = host.Split('.'); - return segments.Length > 0 ? segments[0] : null; - } - catch - { - // If URL parsing fails, return null - return null; - } - } } /// diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Constants/ConfigConstants.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Constants/ConfigConstants.cs index 274d1cbf..3cb9178e 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Constants/ConfigConstants.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Constants/ConfigConstants.cs @@ -69,6 +69,8 @@ public static class ConfigConstants "Sites.Read.All" }; + public const string DefaultAppServicePlanSku = "B1"; + /// /// Default Microsoft Graph API scopes for agent application /// diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/SetupValidationException.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/SetupValidationException.cs index 4ca274fd..e26cb208 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/SetupValidationException.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/SetupValidationException.cs @@ -27,4 +27,4 @@ public SetupValidationException( innerException: innerException) { } -} +} \ No newline at end of file diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Models/Agent365Config.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Models/Agent365Config.cs index 1c9b1245..575fe307 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Models/Agent365Config.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Models/Agent365Config.cs @@ -27,17 +27,17 @@ public List Validate() if (string.IsNullOrWhiteSpace(TenantId)) errors.Add("tenantId is required."); + if (string.IsNullOrWhiteSpace(SubscriptionId)) errors.Add("subscriptionId is required."); + if (string.IsNullOrWhiteSpace(ResourceGroup)) errors.Add("resourceGroup is required."); + if (NeedDeployment) { - if (string.IsNullOrWhiteSpace(SubscriptionId)) errors.Add("subscriptionId is required."); - if (string.IsNullOrWhiteSpace(ResourceGroup)) errors.Add("resourceGroup is required."); if (string.IsNullOrWhiteSpace(Location)) errors.Add("location is required."); if (string.IsNullOrWhiteSpace(AppServicePlanName)) errors.Add("appServicePlanName is required."); if (string.IsNullOrWhiteSpace(WebAppName)) errors.Add("webAppName is required."); } else { - // Non-Azure hosting if (string.IsNullOrWhiteSpace(MessagingEndpoint)) errors.Add("messagingEndpoint is required when needDeployment is 'no'."); } @@ -45,9 +45,6 @@ public List Validate() if (string.IsNullOrWhiteSpace(AgentIdentityDisplayName)) errors.Add("agentIdentityDisplayName is required."); if (string.IsNullOrWhiteSpace(DeploymentProjectPath)) errors.Add("deploymentProjectPath is required."); - // agentIdentityScopes and agentApplicationScopes are now hardcoded defaults - // botName and botDisplayName are now derived, not required in config - // Add more validation as needed (e.g., GUID format, allowed values, etc.) return errors; } // ======================================================================== @@ -87,7 +84,7 @@ public List Validate() /// Default: preprod /// [JsonPropertyName("environment")] - public string Environment { get; init; } = "preprod"; + public string Environment { get; init; } = "prod"; /// /// For External hosting, this is the HTTPS messaging endpoint that Bot Framework will call. diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs index a27fad5d..cc3758b9 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs @@ -3,19 +3,16 @@ using Azure.Core; using Azure.Identity; -using System.Linq; using Microsoft.Agents.A365.DevTools.Cli.Constants; using Microsoft.Agents.A365.DevTools.Cli.Exceptions; using Microsoft.Agents.A365.DevTools.Cli.Services.Helpers; using Microsoft.Extensions.Logging; using Microsoft.Graph; -using Microsoft.Graph.Models; using System.Net.Http.Headers; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text.Json; using System.Text.Json.Nodes; -using static System.Formats.Asn1.AsnWriter; namespace Microsoft.Agents.A365.DevTools.Cli.Services; @@ -133,8 +130,7 @@ public async Task RunAsync(string configPath, string generatedConfigPath, var webAppName = Get("webAppName"); var location = Get("location"); var planSku = Get("appServicePlanSku"); - var messagingEndpoint = Get("messagingEndpoint"); - if (string.IsNullOrWhiteSpace(planSku)) planSku = "B1"; + if (string.IsNullOrWhiteSpace(planSku)) planSku = ConfigConstants.DefaultAppServicePlanSku; var deploymentProjectPath = Get("deploymentProjectPath"); diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigurationWizardService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigurationWizardService.cs index ba44dc00..acd673a7 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigurationWizardService.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigurationWizardService.cs @@ -4,6 +4,7 @@ using System.Globalization; using Microsoft.Extensions.Logging; using Microsoft.Agents.A365.DevTools.Cli.Models; +using Microsoft.Agents.A365.DevTools.Cli.Constants; namespace Microsoft.Agents.A365.DevTools.Cli.Services; @@ -92,7 +93,7 @@ private static string ExtractDomainFromAccount(AzureAccountInfo accountInfo) var derivedNames = GenerateDerivedNames(agentName, domain); // Step 4: Validate deployment project path - var deploymentPath = await PromptForDeploymentPathAsync(existingConfig); + var deploymentPath = PromptForDeploymentPath(existingConfig); if (string.IsNullOrWhiteSpace(deploymentPath)) { _logger.LogError("Configuration wizard cancelled: Deployment project path not provided or invalid"); @@ -111,7 +112,8 @@ private static string ExtractDomainFromAccount(AzureAccountInfo accountInfo) string appServicePlan = string.Empty; string messagingEndpoint = string.Empty; - if (await PromptForWebAppCreateAsync(existingConfig, derivedNames)) + bool needDeployment = PromptForWebAppCreate(existingConfig, derivedNames); + if (needDeployment) { appServicePlan = await PromptForAppServicePlanAsync(existingConfig, resourceGroup); if (string.IsNullOrWhiteSpace(appServicePlan)) @@ -122,7 +124,7 @@ private static string ExtractDomainFromAccount(AzureAccountInfo accountInfo) } else { - messagingEndpoint = await PromptForMessagingEndpointAsync(existingConfig); + messagingEndpoint = PromptForMessagingEndpoint(existingConfig); if (string.IsNullOrWhiteSpace(messagingEndpoint)) { _logger.LogError("Configuration wizard cancelled: Messaging Endpoint not provided"); @@ -139,7 +141,7 @@ private static string ExtractDomainFromAccount(AzureAccountInfo accountInfo) } // Step 8: Get location (with smart default from account or existing config) - var location = await PromptForLocationAsync(existingConfig, accountInfo); + var location = PromptForLocation(existingConfig, accountInfo); // Step 9: Show configuration summary and allow override Console.WriteLine(); @@ -193,8 +195,9 @@ private static string ExtractDomainFromAccount(AzureAccountInfo accountInfo) Location = location, Environment = existingConfig?.Environment ?? "prod", // Default to prod, not asking for this AppServicePlanName = appServicePlan, - AppServicePlanSku = string.IsNullOrWhiteSpace(appServicePlan) ? string.Empty : (existingConfig?.AppServicePlanSku ?? "B1"), // Default to B1, if appServicePlan is selected + AppServicePlanSku = string.IsNullOrWhiteSpace(appServicePlan) ? string.Empty : (existingConfig?.AppServicePlanSku ?? ConfigConstants.DefaultAppServicePlanSku), WebAppName = string.IsNullOrWhiteSpace(appServicePlan) ? string.Empty : customizedNames.WebAppName, + NeedDeployment = needDeployment, MessagingEndpoint = messagingEndpoint, AgentIdentityDisplayName = customizedNames.AgentIdentityDisplayName, AgentBlueprintDisplayName = customizedNames.AgentBlueprintDisplayName, @@ -267,11 +270,10 @@ private string ExtractAgentNameFromConfig(Agent365Config config) return $"agent{DateTime.Now:MMdd}"; } - private async Task PromptForDeploymentPathAsync(Agent365Config? existingConfig) + private string PromptForDeploymentPath(Agent365Config? existingConfig) { var defaultPath = existingConfig?.DeploymentProjectPath ?? Environment.CurrentDirectory; - await Task.CompletedTask; // Satisfy async requirement var path = PromptWithDefault( "Deployment project path", defaultPath, @@ -414,24 +416,21 @@ private string PromptForManagerEmail(Agent365Config? existingConfig, AzureAccoun ); } - private async Task PromptForWebAppCreateAsync(Agent365Config? existingConfig, ConfigDerivedNames? configDerivedNames) + private bool PromptForWebAppCreate(Agent365Config? existingConfig, ConfigDerivedNames? configDerivedNames) { Console.WriteLine(); Console.Write($"Would you like to create a Web App [https://{configDerivedNames?.WebAppName}.azurewebsites.net] in Azure for this Agent? (Y/n): "); var response = Console.ReadLine()?.Trim().ToLowerInvariant(); - await Task.CompletedTask; // Satisfy async requirement - // Default to Yes - only return false if explicitly "n" or "no" return response != "n" && response != "no"; } - private async Task PromptForMessagingEndpointAsync(Agent365Config? existingConfig) + private string PromptForMessagingEndpoint(Agent365Config? existingConfig) { Console.WriteLine("Provide the messaging endpoint URL where your Agent will receive messages."); Console.WriteLine("[Example: https://SampleAgent.azurewebsites.net/api/messages]"); - await Task.CompletedTask; // Satisfy async requirement return PromptWithDefault( "Messaging endpoint URL", existingConfig?.MessagingEndpoint ?? "", @@ -439,7 +438,7 @@ private async Task PromptForMessagingEndpointAsync(Agent365Config? exist ); } - private async Task PromptForLocationAsync(Agent365Config? existingConfig, AzureAccountInfo accountInfo) + private string PromptForLocation(Agent365Config? existingConfig, AzureAccountInfo accountInfo) { // Try to get a smart default location var defaultLocation = existingConfig?.Location; @@ -450,7 +449,6 @@ private async Task PromptForLocationAsync(Agent365Config? existingConfig defaultLocation = "westus"; // Conservative default } - await Task.CompletedTask; // Satisfy async requirement return PromptWithDefault( "Azure location", defaultLocation, From 9de6761fc42dcfc9af06b27884ab9d0fca39df17 Mon Sep 17 00:00:00 2001 From: Sellakumaran Kanagarathnam Date: Fri, 21 Nov 2025 14:01:22 -0800 Subject: [PATCH 4/4] Remove test for ExtractWebAppNameFromUrl method --- .../Commands/SetupCommandTests.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/SetupCommandTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/SetupCommandTests.cs index c140df3b..5d410b3b 100644 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/SetupCommandTests.cs +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/SetupCommandTests.cs @@ -399,22 +399,5 @@ args[0] is LogLevel level && }); infoLogCount.Should().BeGreaterThan(0, "Setup should show success message when all steps complete"); } - - [Theory] - [InlineData("https://test.azurewebsites.net:8080/api/messages", "test")] - [InlineData("https://agent.azurewebsites.net:443/api/messages", "agent")] - public void ExtractWebAppNameFromUrl_HandlesPortNumbers(string input, string expectedName) - { - // Arrange - var method = typeof(SetupCommand) - .GetMethod("ExtractWebAppNameFromUrl", BindingFlags.NonPublic | BindingFlags.Static); - method.Should().NotBeNull(); - - // Act - var result = method!.Invoke(null, new object[] { input }) as string; - - // Assert - result.Should().Be(expectedName, "should extract subdomain correctly even with port number"); - } }