From ca3b2ffd0828fd9060bd553c516ebecaed3a5221 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 00:57:27 +0000 Subject: [PATCH 1/2] Initial plan From eeedde2dd95ee2b40c5302b0b9686f877a2e1f79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:03:46 +0000 Subject: [PATCH 2/2] Surface query failure explicitly in GetAgentInstancesForBlueprintAsync Co-authored-by: gwharris7 <96964444+gwharris7@users.noreply.github.com> --- .../Services/AgentBlueprintService.cs | 92 +++++++++---------- .../Services/AgentBlueprintServiceTests.cs | 16 ++++ 2 files changed, 59 insertions(+), 49 deletions(-) diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/AgentBlueprintService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/AgentBlueprintService.cs index 4de39fc..a5f8acb 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/AgentBlueprintService.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/AgentBlueprintService.cs @@ -164,74 +164,68 @@ public virtual async Task DeleteAgentIdentityAsync( /// /// Queries Entra ID for all agent identity service principals linked to the given blueprint. - /// Returns an empty list if the query fails or no instances are found. + /// Returns an empty list when no instances are found. + /// Throws if the query fails so callers can distinguish a true "no instances" result from a query error. /// /// The tenant ID for authentication. /// The blueprint application ID or object ID. /// Cancellation token. /// List of agent instances linked to the blueprint. + /// Thrown when the Graph query fails. public virtual async Task> GetAgentInstancesForBlueprintAsync( string tenantId, string blueprintId, CancellationToken cancellationToken = default) { - try - { - var requiredScopes = new[] { "AgentIdentityBlueprint.ReadWrite.All" }; - var encodedId = Uri.EscapeDataString(blueprintId); + var requiredScopes = new[] { "AgentIdentityBlueprint.ReadWrite.All" }; + var encodedId = Uri.EscapeDataString(blueprintId); - // Fetch agent identity SPs and agent users for this blueprint sequentially to avoid races on shared HTTP headers - var spItems = await FetchAllPagesAsync( - tenantId, - $"/beta/servicePrincipals/microsoft.graph.agentIdentity?$filter=agentIdentityBlueprintId eq '{encodedId}'&$select=id,displayName", - requiredScopes, - cancellationToken); + // Fetch agent identity SPs and agent users for this blueprint sequentially to avoid races on shared HTTP headers + var spItems = await FetchAllPagesAsync( + tenantId, + $"/beta/servicePrincipals/microsoft.graph.agentIdentity?$filter=agentIdentityBlueprintId eq '{encodedId}'&$select=id,displayName", + requiredScopes, + cancellationToken); - var userItems = await FetchAllPagesAsync( - tenantId, - $"/beta/users/microsoft.graph.agentUser?$filter=agentIdentityBlueprintId eq '{encodedId}'&$select=id,identityParentId", - requiredScopes, - cancellationToken); - // Build lookup: identityParentId (SP object ID) -> user object ID - var userBySpId = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var user in userItems) + var userItems = await FetchAllPagesAsync( + tenantId, + $"/beta/users/microsoft.graph.agentUser?$filter=agentIdentityBlueprintId eq '{encodedId}'&$select=id,identityParentId", + requiredScopes, + cancellationToken); + // Build lookup: identityParentId (SP object ID) -> user object ID + var userBySpId = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var user in userItems) + { + var parentId = user.TryGetProperty("identityParentId", out var p) ? p.GetString() : null; + var userId = user.TryGetProperty("id", out var uid) ? uid.GetString() : null; + if (!string.IsNullOrWhiteSpace(parentId) && !string.IsNullOrWhiteSpace(userId)) { - var parentId = user.TryGetProperty("identityParentId", out var p) ? p.GetString() : null; - var userId = user.TryGetProperty("id", out var uid) ? uid.GetString() : null; - if (!string.IsNullOrWhiteSpace(parentId) && !string.IsNullOrWhiteSpace(userId)) - { - userBySpId[parentId] = userId; - } + userBySpId[parentId] = userId; } + } - // Correlate SPs with their agent users - var results = new List(); - foreach (var item in spItems) + // Correlate SPs with their agent users + var results = new List(); + foreach (var item in spItems) + { + var spId = item.TryGetProperty("id", out var id) ? id.GetString() : null; + if (string.IsNullOrWhiteSpace(spId)) { - var spId = item.TryGetProperty("id", out var id) ? id.GetString() : null; - if (string.IsNullOrWhiteSpace(spId)) - { - continue; - } - - var displayName = item.TryGetProperty("displayName", out var dn) ? dn.GetString() : null; - userBySpId.TryGetValue(spId, out var agentUserId); - - results.Add(new AgentInstanceInfo - { - IdentitySpId = spId, - DisplayName = displayName, - AgentUserId = string.IsNullOrWhiteSpace(agentUserId) ? null : agentUserId - }); + continue; } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex, "Exception querying agent instances for blueprint {BlueprintId}", blueprintId); - return Array.Empty(); + var displayName = item.TryGetProperty("displayName", out var dn) ? dn.GetString() : null; + userBySpId.TryGetValue(spId, out var agentUserId); + + results.Add(new AgentInstanceInfo + { + IdentitySpId = spId, + DisplayName = displayName, + AgentUserId = string.IsNullOrWhiteSpace(agentUserId) ? null : agentUserId + }); } + + return results; } /// diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/AgentBlueprintServiceTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/AgentBlueprintServiceTests.cs index 449a429..5d61768 100644 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/AgentBlueprintServiceTests.cs +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/AgentBlueprintServiceTests.cs @@ -380,6 +380,22 @@ public async Task GetAgentInstancesForBlueprintAsync_ReturnsEmpty_WhenNoneFound( } } + [Fact] + public async Task GetAgentInstancesForBlueprintAsync_Throws_WhenGraphQueryFails() + { + // Arrange + var (service, _) = CreateServiceWithFakeHandler(); + + // Override token provider to throw so the Graph call fails + _mockTokenProvider.GetMgGraphAccessTokenAsync( + Arg.Any(), Arg.Any>(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromException(new HttpRequestException("Connection timeout"))); + + // Act & Assert - exception must propagate so callers can abort rather than proceeding with 0 instances + await service.Invoking(s => s.GetAgentInstancesForBlueprintAsync("tenant-id", "blueprint-id")) + .Should().ThrowAsync(); + } + [Fact] public async Task DeleteAgentUserAsync_ReturnsTrue_OnSuccess() {