Skip to content

Commit 9a446e4

Browse files
Copilotgwharris7
andauthored
Surface query failure explicitly in GetAgentInstancesForBlueprintAsync (#305)
* Initial plan * Surface query failure explicitly in GetAgentInstancesForBlueprintAsync Co-authored-by: gwharris7 <96964444+gwharris7@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gwharris7 <96964444+gwharris7@users.noreply.github.com>
1 parent 9adf6dc commit 9a446e4

2 files changed

Lines changed: 59 additions & 49 deletions

File tree

src/Microsoft.Agents.A365.DevTools.Cli/Services/AgentBlueprintService.cs

Lines changed: 43 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -164,74 +164,68 @@ public virtual async Task<bool> DeleteAgentIdentityAsync(
164164

165165
/// <summary>
166166
/// Queries Entra ID for all agent identity service principals linked to the given blueprint.
167-
/// Returns an empty list if the query fails or no instances are found.
167+
/// Returns an empty list when no instances are found.
168+
/// Throws if the query fails so callers can distinguish a true "no instances" result from a query error.
168169
/// </summary>
169170
/// <param name="tenantId">The tenant ID for authentication.</param>
170171
/// <param name="blueprintId">The blueprint application ID or object ID.</param>
171172
/// <param name="cancellationToken">Cancellation token.</param>
172173
/// <returns>List of agent instances linked to the blueprint.</returns>
174+
/// <exception cref="Exception">Thrown when the Graph query fails.</exception>
173175
public virtual async Task<IReadOnlyList<AgentInstanceInfo>> GetAgentInstancesForBlueprintAsync(
174176
string tenantId,
175177
string blueprintId,
176178
CancellationToken cancellationToken = default)
177179
{
178-
try
179-
{
180-
var requiredScopes = new[] { "AgentIdentityBlueprint.ReadWrite.All" };
181-
var encodedId = Uri.EscapeDataString(blueprintId);
180+
var requiredScopes = new[] { "AgentIdentityBlueprint.ReadWrite.All" };
181+
var encodedId = Uri.EscapeDataString(blueprintId);
182182

183-
// Fetch agent identity SPs and agent users for this blueprint sequentially to avoid races on shared HTTP headers
184-
var spItems = await FetchAllPagesAsync(
185-
tenantId,
186-
$"/beta/servicePrincipals/microsoft.graph.agentIdentity?$filter=agentIdentityBlueprintId eq '{encodedId}'&$select=id,displayName",
187-
requiredScopes,
188-
cancellationToken);
183+
// Fetch agent identity SPs and agent users for this blueprint sequentially to avoid races on shared HTTP headers
184+
var spItems = await FetchAllPagesAsync(
185+
tenantId,
186+
$"/beta/servicePrincipals/microsoft.graph.agentIdentity?$filter=agentIdentityBlueprintId eq '{encodedId}'&$select=id,displayName",
187+
requiredScopes,
188+
cancellationToken);
189189

190-
var userItems = await FetchAllPagesAsync(
191-
tenantId,
192-
$"/beta/users/microsoft.graph.agentUser?$filter=agentIdentityBlueprintId eq '{encodedId}'&$select=id,identityParentId",
193-
requiredScopes,
194-
cancellationToken);
195-
// Build lookup: identityParentId (SP object ID) -> user object ID
196-
var userBySpId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
197-
foreach (var user in userItems)
190+
var userItems = await FetchAllPagesAsync(
191+
tenantId,
192+
$"/beta/users/microsoft.graph.agentUser?$filter=agentIdentityBlueprintId eq '{encodedId}'&$select=id,identityParentId",
193+
requiredScopes,
194+
cancellationToken);
195+
// Build lookup: identityParentId (SP object ID) -> user object ID
196+
var userBySpId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
197+
foreach (var user in userItems)
198+
{
199+
var parentId = user.TryGetProperty("identityParentId", out var p) ? p.GetString() : null;
200+
var userId = user.TryGetProperty("id", out var uid) ? uid.GetString() : null;
201+
if (!string.IsNullOrWhiteSpace(parentId) && !string.IsNullOrWhiteSpace(userId))
198202
{
199-
var parentId = user.TryGetProperty("identityParentId", out var p) ? p.GetString() : null;
200-
var userId = user.TryGetProperty("id", out var uid) ? uid.GetString() : null;
201-
if (!string.IsNullOrWhiteSpace(parentId) && !string.IsNullOrWhiteSpace(userId))
202-
{
203-
userBySpId[parentId] = userId;
204-
}
203+
userBySpId[parentId] = userId;
205204
}
205+
}
206206

207-
// Correlate SPs with their agent users
208-
var results = new List<AgentInstanceInfo>();
209-
foreach (var item in spItems)
207+
// Correlate SPs with their agent users
208+
var results = new List<AgentInstanceInfo>();
209+
foreach (var item in spItems)
210+
{
211+
var spId = item.TryGetProperty("id", out var id) ? id.GetString() : null;
212+
if (string.IsNullOrWhiteSpace(spId))
210213
{
211-
var spId = item.TryGetProperty("id", out var id) ? id.GetString() : null;
212-
if (string.IsNullOrWhiteSpace(spId))
213-
{
214-
continue;
215-
}
216-
217-
var displayName = item.TryGetProperty("displayName", out var dn) ? dn.GetString() : null;
218-
userBySpId.TryGetValue(spId, out var agentUserId);
219-
220-
results.Add(new AgentInstanceInfo
221-
{
222-
IdentitySpId = spId,
223-
DisplayName = displayName,
224-
AgentUserId = string.IsNullOrWhiteSpace(agentUserId) ? null : agentUserId
225-
});
214+
continue;
226215
}
227216

228-
return results;
229-
}
230-
catch (Exception ex)
231-
{
232-
_logger.LogError(ex, "Exception querying agent instances for blueprint {BlueprintId}", blueprintId);
233-
return Array.Empty<AgentInstanceInfo>();
217+
var displayName = item.TryGetProperty("displayName", out var dn) ? dn.GetString() : null;
218+
userBySpId.TryGetValue(spId, out var agentUserId);
219+
220+
results.Add(new AgentInstanceInfo
221+
{
222+
IdentitySpId = spId,
223+
DisplayName = displayName,
224+
AgentUserId = string.IsNullOrWhiteSpace(agentUserId) ? null : agentUserId
225+
});
234226
}
227+
228+
return results;
235229
}
236230

237231
/// <summary>

src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/AgentBlueprintServiceTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,22 @@ public async Task GetAgentInstancesForBlueprintAsync_ReturnsEmpty_WhenNoneFound(
380380
}
381381
}
382382

383+
[Fact]
384+
public async Task GetAgentInstancesForBlueprintAsync_Throws_WhenGraphQueryFails()
385+
{
386+
// Arrange
387+
var (service, _) = CreateServiceWithFakeHandler();
388+
389+
// Override token provider to throw so the Graph call fails
390+
_mockTokenProvider.GetMgGraphAccessTokenAsync(
391+
Arg.Any<string>(), Arg.Any<IEnumerable<string>>(), Arg.Any<bool>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
392+
.Returns(Task.FromException<string?>(new HttpRequestException("Connection timeout")));
393+
394+
// Act & Assert - exception must propagate so callers can abort rather than proceeding with 0 instances
395+
await service.Invoking(s => s.GetAgentInstancesForBlueprintAsync("tenant-id", "blueprint-id"))
396+
.Should().ThrowAsync<HttpRequestException>();
397+
}
398+
383399
[Fact]
384400
public async Task DeleteAgentUserAsync_ReturnsTrue_OnSuccess()
385401
{

0 commit comments

Comments
 (0)