From 849689e4ba44960da1bec9989353921ffd4daace Mon Sep 17 00:00:00 2001 From: Lavanya Kappagantu Date: Mon, 24 Nov 2025 15:56:49 -0800 Subject: [PATCH 1/3] Using the correct command to delete agent blueprints --- .../Commands/CleanupCommand.cs | 62 +++++++++++---- .../Program.cs | 2 +- .../Services/GraphApiService.cs | 79 +++++++++++++++++++ .../Commands/CleanupCommandTests.cs | 23 +++--- 4 files changed, 140 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs index 53f102bf..c34d4d83 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs @@ -16,7 +16,8 @@ public static Command CreateCommand( ILogger logger, IConfigService configService, IBotConfigurator botConfigurator, - CommandExecutor executor) + CommandExecutor executor, + GraphApiService graphApiService) { var cleanupCommand = new Command("cleanup", "Clean up ALL resources (blueprint, instance, Azure) - use subcommands for granular cleanup"); @@ -33,11 +34,11 @@ public static Command CreateCommand( // Set default handler for 'a365 cleanup' (without subcommand) - cleans up everything cleanupCommand.SetHandler(async (configFile) => { - await ExecuteAllCleanupAsync(logger, configService, botConfigurator, executor, configFile); + await ExecuteAllCleanupAsync(logger, configService, botConfigurator, executor, graphApiService, configFile); }, configOption); // Add subcommands for granular control - cleanupCommand.AddCommand(CreateBlueprintCleanupCommand(logger, configService, executor)); + cleanupCommand.AddCommand(CreateBlueprintCleanupCommand(logger, configService, executor, graphApiService)); cleanupCommand.AddCommand(CreateAzureCleanupCommand(logger, configService, botConfigurator, executor)); cleanupCommand.AddCommand(CreateInstanceCleanupCommand(logger, configService, executor)); @@ -47,7 +48,8 @@ public static Command CreateCommand( private static Command CreateBlueprintCleanupCommand( ILogger logger, IConfigService configService, - CommandExecutor executor) + CommandExecutor executor, + GraphApiService graphApiService) { var command = new Command("blueprint", "Remove Entra ID blueprint application and service principal"); @@ -80,6 +82,7 @@ private static Command CreateBlueprintCleanupCommand( logger.LogInformation("Blueprint Cleanup Preview:"); logger.LogInformation("============================="); logger.LogInformation("Will delete Entra ID application: {BlueprintId}", config.AgentBlueprintId); + logger.LogInformation(" Name: {DisplayName}", config.AgentBlueprintDisplayName); logger.LogInformation(""); Console.Write("Continue with blueprint cleanup? (y/N): "); @@ -90,14 +93,17 @@ private static Command CreateBlueprintCleanupCommand( return; } - // Delete the Entra ID application - logger.LogInformation("Deleting blueprint application..."); - var deleteCommand = $"az ad app delete --id {config.AgentBlueprintId}"; - await executor.ExecuteAsync("az", $"ad app delete --id {config.AgentBlueprintId}", null, true, false, CancellationToken.None); - - logger.LogInformation("Blueprint application deleted successfully"); + // Delete the agent blueprint using the special Graph API endpoint + logger.LogInformation("Deleting agent blueprint application..."); + var deleted = await graphApiService.DeleteAgentBlueprintAsync( + config.TenantId, + config.AgentBlueprintId); + + // Always clear blueprint data from config, even if deletion failed + // User can delete manually using Portal/PowerShell/Graph Explorer + logger.LogInformation(""); + logger.LogInformation("Clearing blueprint data from local configuration..."); - // Clear the blueprint data from generated config config.AgentBlueprintId = string.Empty; config.AgentBlueprintClientSecret = string.Empty; config.ConsentUrlGraph = string.Empty; @@ -106,7 +112,19 @@ private static Command CreateBlueprintCleanupCommand( config.Consent2Granted = false; await configService.SaveStateAsync(config); - logger.LogInformation("Configuration updated"); + logger.LogInformation("Local configuration cleared"); + + if (deleted) + { + logger.LogInformation(""); + logger.LogInformation("Blueprint cleanup completed successfully!"); + } + else + { + logger.LogWarning(""); + logger.LogWarning("Blueprint deletion failed, but local configuration has been cleared."); + logger.LogWarning("Please manually delete the blueprint using one of the methods shown above."); + } } catch (Exception ex) { @@ -359,6 +377,7 @@ private static async Task ExecuteAllCleanupAsync( IConfigService configService, IBotConfigurator botConfigurator, CommandExecutor executor, + GraphApiService graphApiService, FileInfo? configFile) { try @@ -405,12 +424,23 @@ private static async Task ExecuteAllCleanupAsync( logger.LogInformation("Starting complete cleanup..."); - // 1. Delete blueprint application + // 1. Delete agent blueprint application if (!string.IsNullOrEmpty(config.AgentBlueprintId)) { - logger.LogInformation("Deleting blueprint application..."); - await executor.ExecuteAsync("az", $"ad app delete --id {config.AgentBlueprintId}", null, true, false, CancellationToken.None); - logger.LogInformation("Blueprint application deleted"); + logger.LogInformation("Deleting agent blueprint application..."); + var deleted = await graphApiService.DeleteAgentBlueprintAsync( + config.TenantId, + config.AgentBlueprintId); + + if (deleted) + { + logger.LogInformation("Agent blueprint application deleted successfully"); + } + else + { + logger.LogWarning("Failed to delete agent blueprint application (will continue with other resources)"); + logger.LogWarning("Local configuration will still be cleared at the end"); + } } // 2. Delete agent identity application diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Program.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Program.cs index 5ae28e59..3ce0fb4f 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Program.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Program.cs @@ -100,7 +100,7 @@ static async Task Main(string[] args) var manifestTemplateService = serviceProvider.GetRequiredService(); rootCommand.AddCommand(ConfigCommand.CreateCommand(configLogger, wizardService: wizardService)); rootCommand.AddCommand(QueryEntraCommand.CreateCommand(queryEntraLogger, configService, executor, graphApiService)); - rootCommand.AddCommand(CleanupCommand.CreateCommand(cleanupLogger, configService, botConfigurator, executor)); + rootCommand.AddCommand(CleanupCommand.CreateCommand(cleanupLogger, configService, botConfigurator, executor, graphApiService)); rootCommand.AddCommand(PublishCommand.CreateCommand(publishLogger, configService, graphApiService, manifestTemplateService)); // Wrap all command handlers with exception handling diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs index e949343e..8034dd78 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs @@ -587,6 +587,85 @@ private async Task AssignAppRoleAsync( #endregion + /// + /// Delete an Agent Blueprint application using the special agentIdentityBlueprint endpoint + /// + /// The tenant ID for authentication + /// The blueprint application ID (object ID or app ID) + /// Cancellation token + /// True if deletion succeeded or resource not found; false otherwise + public async Task DeleteAgentBlueprintAsync( + string tenantId, + string blueprintId, + CancellationToken cancellationToken = default) + { + try + { + _logger.LogInformation("Deleting agent blueprint application: {BlueprintId}", blueprintId); + + // Agent Blueprint deletion requires AgentIdentityBlueprint.ReadWrite.All delegated permission + // Azure CLI tokens don't support this scope, so we must use interactive authentication + var requiredScopes = new[] { "AgentIdentityBlueprint.ReadWrite.All" }; + + string? token = null; + + if (_tokenProvider != null) + { + _logger.LogInformation("Acquiring access token with AgentIdentityBlueprint.ReadWrite.All scope..."); + _logger.LogInformation("A browser window will open for authentication."); + token = await _tokenProvider.GetMgGraphAccessTokenAsync(tenantId, requiredScopes, useDeviceCode: false, cancellationToken); + } + + if (string.IsNullOrWhiteSpace(token)) + { + _logger.LogError("Failed to acquire access token with required scope"); + _logger.LogError("Agent Blueprint deletion requires interactive authentication with AgentIdentityBlueprint.ReadWrite.All permission"); + return false; + } + + // Set authorization header with the token that has the correct scope + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + _httpClient.DefaultRequestHeaders.Remove("ConsistencyLevel"); + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("ConsistencyLevel", "eventual"); + + // Use the special agentIdentityBlueprint endpoint for deletion + var deletePath = $"/beta/applications/{blueprintId}/microsoft.graph.agentIdentityBlueprint"; + var url = deletePath.StartsWith("http", StringComparison.OrdinalIgnoreCase) + ? deletePath + : $"https://graph.microsoft.com{deletePath}"; + + using var req = new HttpRequestMessage(HttpMethod.Delete, url); + using var resp = await _httpClient.SendAsync(req, cancellationToken); + + // 404 can be considered success for idempotent deletes + if ((int)resp.StatusCode == 404) + { + _logger.LogInformation("Agent blueprint not found (may have been already deleted)"); + return true; + } + + if (!resp.IsSuccessStatusCode) + { + var body = await resp.Content.ReadAsStringAsync(cancellationToken); + _logger.LogError("Graph DELETE {Url} failed {Code} {Reason}: {Body}", url, (int)resp.StatusCode, resp.ReasonPhrase, body); + return false; + } + + _logger.LogInformation("Agent blueprint application deleted successfully"); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception deleting agent blueprint application"); + return false; + } + finally + { + // Clear authorization header to avoid issues with other requests + _httpClient.DefaultRequestHeaders.Authorization = null; + } + } + private async Task EnsureGraphHeadersAsync(string tenantId, CancellationToken ct = default, IEnumerable? scopes = null) { var token = (scopes != null && _tokenProvider != null) ? await _tokenProvider.GetMgGraphAccessTokenAsync(tenantId, scopes, false, ct) : await GetGraphAccessTokenAsync(tenantId, ct); diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/CleanupCommandTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/CleanupCommandTests.cs index 9b63030c..7e2d5499 100644 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/CleanupCommandTests.cs +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/CleanupCommandTests.cs @@ -17,6 +17,7 @@ public class CleanupCommandTests private readonly IConfigService _mockConfigService; private readonly IBotConfigurator _mockBotConfigurator; private readonly CommandExecutor _mockExecutor; + private readonly GraphApiService _graphApiService; public CleanupCommandTests() { @@ -30,6 +31,10 @@ public CleanupCommandTests() _mockExecutor.ExecuteAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new Microsoft.Agents.A365.DevTools.Cli.Services.CommandResult { ExitCode = 0, StandardOutput = string.Empty, StandardError = string.Empty })); _mockBotConfigurator = Substitute.For(); + + // Create a real GraphApiService instance with mocked logger for testing + var mockGraphLogger = Substitute.For>(); + _graphApiService = new GraphApiService(mockGraphLogger, _mockExecutor); } [Fact(Skip = "Test requires interactive confirmation - cleanup commands now enforce user confirmation instead of --force")] @@ -39,7 +44,7 @@ public async Task CleanupAzure_WithValidConfig_ShouldExecuteResourceDeleteComman var config = CreateValidConfig(); _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()).Returns(config); - var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor); + var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor, _graphApiService); var args = new[] { "cleanup", "azure", "--config", "test.json" }; // Act @@ -68,7 +73,7 @@ public async Task CleanupInstance_WithValidConfig_ShouldReturnSuccess() _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()).Returns(config); _mockBotConfigurator.DeleteEndpointWithAgentBlueprintAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(true)); - var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor); + var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor, _graphApiService); var args = new[] { "cleanup", "instance", "--config", "test.json" }; var originalIn = Console.In; @@ -99,7 +104,7 @@ public async Task Cleanup_WithoutSubcommand_ShouldExecuteCompleteCleanup() var config = CreateValidConfig(); _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()).Returns(config); - var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor); + var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor, _graphApiService); var args = new[] { "cleanup", "--config", "test.json" }; // Act @@ -129,7 +134,7 @@ public async Task CleanupAzure_WithMissingWebAppName_ShouldStillExecuteCommand() var config = CreateConfigWithMissingWebApp(); // Create config without web app name _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()).Returns(config); - var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor); + var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor, _graphApiService); var args = new[] { "cleanup", "azure", "--config", "test.json" }; // Act @@ -156,7 +161,7 @@ public async Task CleanupCommand_WithInvalidConfigFile_ShouldReturnError() _mockBotConfigurator.DeleteEndpointWithAgentBlueprintAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(false)); - var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor); + var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor, _graphApiService); var args = new[] { "cleanup", "azure", "--config", "invalid.json" }; // Act @@ -176,7 +181,7 @@ await _mockExecutor.DidNotReceive().ExecuteAsync( public void CleanupCommand_ShouldHaveCorrectSubcommands() { // Arrange & Act - var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor); + var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor, _graphApiService); // Assert - Verify command structure (what users see) Assert.Equal("cleanup", command.Name); @@ -195,7 +200,7 @@ public void CleanupCommand_ShouldHaveCorrectSubcommands() public void CleanupCommand_ShouldHaveDefaultHandlerOptions() { // Arrange & Act - var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor); + var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor, _graphApiService); // Assert - Verify parent command has options for default handler var optionNames = command.Options.Select(opt => opt.Name).ToList(); @@ -208,7 +213,7 @@ public void CleanupCommand_ShouldHaveDefaultHandlerOptions() public void CleanupSubcommands_ShouldHaveRequiredOptions() { // Arrange & Act - var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor); + var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor, _graphApiService); var blueprintCommand = command.Subcommands.First(sc => sc.Name == "blueprint"); // Assert - Verify user-facing options @@ -225,7 +230,7 @@ public async Task CleanupBlueprint_WithValidConfig_ShouldReturnSuccess() var config = CreateValidConfig(); _mockConfigService.LoadAsync(Arg.Any(), Arg.Any()).Returns(config); - var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor); + var command = CleanupCommand.CreateCommand(_mockLogger, _mockConfigService, _mockBotConfigurator, _mockExecutor, _graphApiService); var args = new[] { "cleanup", "blueprint", "--config", "test.json" }; // Act From 1b208245218af051be551795a1fb53a9f92631f4 Mon Sep 17 00:00:00 2001 From: LavanyaK235 <2009radha9@gmail.com> Date: Tue, 25 Nov 2025 09:18:42 -0800 Subject: [PATCH 2/3] Update src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Commands/CleanupCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs index c34d4d83..1439da81 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs @@ -123,7 +123,7 @@ private static Command CreateBlueprintCleanupCommand( { logger.LogWarning(""); logger.LogWarning("Blueprint deletion failed, but local configuration has been cleared."); - logger.LogWarning("Please manually delete the blueprint using one of the methods shown above."); + logger.LogWarning("Please manually delete the blueprint application using the Azure Portal, PowerShell, or Microsoft Graph Explorer."); } } catch (Exception ex) From 123af2cd069f20775429ea0b87423393404b3c79 Mon Sep 17 00:00:00 2001 From: Lavanya Kappagantu Date: Tue, 25 Nov 2025 09:45:51 -0800 Subject: [PATCH 3/3] Addressed comments --- .../Services/GraphApiService.cs | 71 +++++++++---------- .../Commands/CleanupCommandTests.cs | 16 ++++- 2 files changed, 46 insertions(+), 41 deletions(-) diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs index 8034dd78..4bd61295 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/GraphApiService.cs @@ -588,7 +588,15 @@ private async Task AssignAppRoleAsync( #endregion /// - /// Delete an Agent Blueprint application using the special agentIdentityBlueprint endpoint + /// Delete an Agent Blueprint application using the special agentIdentityBlueprint endpoint. + /// + /// SPECIAL AUTHENTICATION REQUIREMENTS: + /// Agent Blueprint deletion requires the AgentIdentityBlueprint.ReadWrite.All delegated permission scope. + /// This scope is not available through Azure CLI tokens, so we use interactive authentication via + /// the token provider (same authentication method used during blueprint creation in the setup command). + /// + /// This method uses the GraphDeleteAsync helper but with special scopes - the duplication is intentional + /// because blueprint operations require elevated permissions that standard Graph operations don't need. /// /// The tenant ID for authentication /// The blueprint application ID (object ID or app ID) @@ -603,56 +611,40 @@ public async Task DeleteAgentBlueprintAsync( { _logger.LogInformation("Deleting agent blueprint application: {BlueprintId}", blueprintId); - // Agent Blueprint deletion requires AgentIdentityBlueprint.ReadWrite.All delegated permission - // Azure CLI tokens don't support this scope, so we must use interactive authentication + // Agent Blueprint deletion requires special delegated permission scope var requiredScopes = new[] { "AgentIdentityBlueprint.ReadWrite.All" }; - string? token = null; - - if (_tokenProvider != null) - { - _logger.LogInformation("Acquiring access token with AgentIdentityBlueprint.ReadWrite.All scope..."); - _logger.LogInformation("A browser window will open for authentication."); - token = await _tokenProvider.GetMgGraphAccessTokenAsync(tenantId, requiredScopes, useDeviceCode: false, cancellationToken); - } - - if (string.IsNullOrWhiteSpace(token)) + if (_tokenProvider == null) { - _logger.LogError("Failed to acquire access token with required scope"); - _logger.LogError("Agent Blueprint deletion requires interactive authentication with AgentIdentityBlueprint.ReadWrite.All permission"); + _logger.LogError("Token provider is not configured. Agent Blueprint deletion requires interactive authentication."); + _logger.LogError("Please ensure the GraphApiService is initialized with a token provider."); return false; } - // Set authorization header with the token that has the correct scope - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - _httpClient.DefaultRequestHeaders.Remove("ConsistencyLevel"); - _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("ConsistencyLevel", "eventual"); + _logger.LogInformation("Acquiring access token with AgentIdentityBlueprint.ReadWrite.All scope..."); + _logger.LogInformation("A browser window will open for authentication."); // Use the special agentIdentityBlueprint endpoint for deletion var deletePath = $"/beta/applications/{blueprintId}/microsoft.graph.agentIdentityBlueprint"; - var url = deletePath.StartsWith("http", StringComparison.OrdinalIgnoreCase) - ? deletePath - : $"https://graph.microsoft.com{deletePath}"; - - using var req = new HttpRequestMessage(HttpMethod.Delete, url); - using var resp = await _httpClient.SendAsync(req, cancellationToken); - - // 404 can be considered success for idempotent deletes - if ((int)resp.StatusCode == 404) + + // Use GraphDeleteAsync with the special scopes required for blueprint operations + var success = await GraphDeleteAsync( + tenantId, + deletePath, + cancellationToken, + treatNotFoundAsSuccess: true, + scopes: requiredScopes); + + if (success) { - _logger.LogInformation("Agent blueprint not found (may have been already deleted)"); - return true; + _logger.LogInformation("Agent blueprint application deleted successfully"); } - - if (!resp.IsSuccessStatusCode) + else { - var body = await resp.Content.ReadAsStringAsync(cancellationToken); - _logger.LogError("Graph DELETE {Url} failed {Code} {Reason}: {Body}", url, (int)resp.StatusCode, resp.ReasonPhrase, body); - return false; + _logger.LogError("Failed to delete agent blueprint application"); } - _logger.LogInformation("Agent blueprint application deleted successfully"); - return true; + return success; } catch (Exception ex) { @@ -757,9 +749,10 @@ public async Task GraphDeleteAsync( string tenantId, string relativePath, CancellationToken ct = default, - bool treatNotFoundAsSuccess = true) + bool treatNotFoundAsSuccess = true, + IEnumerable? scopes = null) { - if (!await EnsureGraphHeadersAsync(tenantId, ct)) return false; + if (!await EnsureGraphHeadersAsync(tenantId, ct, scopes)) return false; var url = relativePath.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? relativePath diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/CleanupCommandTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/CleanupCommandTests.cs index 7e2d5499..53ed6726 100644 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/CleanupCommandTests.cs +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/CleanupCommandTests.cs @@ -18,6 +18,7 @@ public class CleanupCommandTests private readonly IBotConfigurator _mockBotConfigurator; private readonly CommandExecutor _mockExecutor; private readonly GraphApiService _graphApiService; + private readonly IMicrosoftGraphTokenProvider _mockTokenProvider; public CleanupCommandTests() { @@ -32,9 +33,20 @@ public CleanupCommandTests() .Returns(Task.FromResult(new Microsoft.Agents.A365.DevTools.Cli.Services.CommandResult { ExitCode = 0, StandardOutput = string.Empty, StandardError = string.Empty })); _mockBotConfigurator = Substitute.For(); - // Create a real GraphApiService instance with mocked logger for testing + // Create a mock token provider for GraphApiService + _mockTokenProvider = Substitute.For(); + + // Configure token provider to return a test token + _mockTokenProvider.GetMgGraphAccessTokenAsync( + Arg.Any(), + Arg.Any>(), + Arg.Any(), + Arg.Any()) + .Returns("test-token"); + + // Create a real GraphApiService instance with mocked dependencies var mockGraphLogger = Substitute.For>(); - _graphApiService = new GraphApiService(mockGraphLogger, _mockExecutor); + _graphApiService = new GraphApiService(mockGraphLogger, _mockExecutor, null, _mockTokenProvider); } [Fact(Skip = "Test requires interactive confirmation - cleanup commands now enforce user confirmation instead of --force")]