diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs index 3448fda2..53f102bf 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs @@ -162,11 +162,18 @@ 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 (config.NeedDeployment) { - ($"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 +199,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..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 = $"https://{instanceConfig.WebAppName}.azurewebsites.net/api/messages"; - 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 a388063a..8921b10c 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DeployCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DeployCommand.cs @@ -73,6 +73,13 @@ public static Command CreateCommand( return; } + // Check if web app deployment should be skipped (external messaging endpoint) + if (!configData.NeedDeployment) + { + logger.LogInformation("Web App deployment is skipped as per configuration."); + return; + } + var validatedConfig = await ValidateDeploymentPrerequisitesAsync( config.FullName, configService, azureValidator, executor, logger); if (validatedConfig == null) return; @@ -141,6 +148,13 @@ private static Command CreateAppSubcommand( return; } + // Check if web app deployment should be skipped (external messaging endpoint) + if (!configData.NeedDeployment) + { + logger.LogInformation("Web App deployment is skipped as per configuration."); + return; + } + var validatedConfig = await ValidateDeploymentPrerequisitesAsync( config.FullName, configService, azureValidator, executor, logger); if (validatedConfig == null) return; diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs index 8d5e9712..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; + } /// 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 63a414a0..e26cb208 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/SetupValidationException.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/SetupValidationException.cs @@ -1,30 +1,30 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Agents.A365.DevTools.Cli.Constants; - -namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions; - -/// -/// Validation errors that occur during `a365 setup` (user-fixable issues). -/// -public sealed class SetupValidationException : Agent365Exception -{ - public override int ExitCode => 2; - - public SetupValidationException( - string issueDescription, - List? errorDetails = null, - List? mitigationSteps = null, - Dictionary? context = null, - Exception? innerException = null) - : base( - errorCode: ErrorCodes.SetupValidationFailed, - issueDescription: issueDescription, - errorDetails: errorDetails, - mitigationSteps: mitigationSteps, - context: context, - innerException: innerException) - { - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Agents.A365.DevTools.Cli.Constants; + +namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions; + +/// +/// Validation errors that occur during `a365 setup` (user-fixable issues). +/// +public sealed class SetupValidationException : Agent365Exception +{ + public override int ExitCode => 2; + + public SetupValidationException( + string issueDescription, + List? errorDetails = null, + List? mitigationSteps = null, + Dictionary? context = null, + Exception? innerException = null) + : base( + errorCode: ErrorCodes.SetupValidationFailed, + issueDescription: issueDescription, + errorDetails: errorDetails, + mitigationSteps: mitigationSteps, + context: context, + 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 6b5e5ab5..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,14 +84,14 @@ 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. /// For AzureAppService, this is optional; the CLI derives the endpoint from webAppName. /// [JsonPropertyName("messagingEndpoint")] - public string? MessagingEndpoint { get; init; } + public string MessagingEndpoint { get; init; } = string.Empty; /// /// Whether the CLI should create and deploy an Azure Web App for this agent. @@ -119,7 +116,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). diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs index b1134575..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,7 +130,7 @@ public async Task RunAsync(string configPath, string generatedConfigPath, var webAppName = Get("webAppName"); var location = Get("location"); var planSku = Get("appServicePlanSku"); - 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 c4b1df23..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"); @@ -107,12 +108,28 @@ 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; + + bool needDeployment = PromptForWebAppCreate(existingConfig, derivedNames); + if (needDeployment) { - _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 = PromptForMessagingEndpoint(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) @@ -124,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(); @@ -132,7 +149,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 +167,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 +195,10 @@ 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 ?? ConfigConstants.DefaultAppServicePlanSku), + WebAppName = string.IsNullOrWhiteSpace(appServicePlan) ? string.Empty : customizedNames.WebAppName, + NeedDeployment = needDeployment, + MessagingEndpoint = messagingEndpoint, AgentIdentityDisplayName = customizedNames.AgentIdentityDisplayName, AgentBlueprintDisplayName = customizedNames.AgentBlueprintDisplayName, AgentUserPrincipalName = customizedNames.AgentUserPrincipalName, @@ -242,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, @@ -389,7 +416,29 @@ private string PromptForManagerEmail(Agent365Config? existingConfig, AzureAccoun ); } - private async Task PromptForLocationAsync(Agent365Config? existingConfig, AzureAccountInfo accountInfo) + 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(); + + // Default to Yes - only return false if explicitly "n" or "no" + return response != "n" && response != "no"; + } + + 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]"); + + return PromptWithDefault( + "Messaging endpoint URL", + existingConfig?.MessagingEndpoint ?? "", + ValidateUrl + ); + } + + private string PromptForLocation(Agent365Config? existingConfig, AzureAccountInfo accountInfo) { // Try to get a smart default location var defaultLocation = existingConfig?.Location; @@ -400,7 +449,6 @@ private async Task PromptForLocationAsync(Agent365Config? existingConfig defaultLocation = "westus"; // Conservative default } - await Task.CompletedTask; // Satisfy async requirement return PromptWithDefault( "Azure location", defaultLocation, @@ -554,6 +602,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 6b845d1d..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 @@ -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; 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..131cc0ed 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,100 @@ 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 = ".", + NeedDeployment = false + // 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 }