diff --git a/src/Observability/Hosting/Extensions/TurnContextExtensions.cs b/src/Observability/Hosting/Extensions/TurnContextExtensions.cs index 5ea8f8bc..f187824c 100644 --- a/src/Observability/Hosting/Extensions/TurnContextExtensions.cs +++ b/src/Observability/Hosting/Extensions/TurnContextExtensions.cs @@ -20,8 +20,9 @@ public static class TurnContextExtensions /// public static IEnumerable> GetCallerBaggagePairs(this ITurnContext turnContext) { - yield return new KeyValuePair(OpenTelemetryConstants.UserIdKey, turnContext.Activity?.From?.AadObjectId); - yield return new KeyValuePair(OpenTelemetryConstants.UserNameKey, turnContext.Activity?.From?.Name); + var from = turnContext.Activity?.From; + yield return new KeyValuePair(OpenTelemetryConstants.UserIdKey, from?.AadObjectId ?? from?.AgenticUserId ?? from?.Id); + yield return new KeyValuePair(OpenTelemetryConstants.UserNameKey, from?.Name); } /// diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs index e5cde121..c090c241 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs @@ -107,9 +107,114 @@ public async Task OnTurnAsync_RestoresBaggageAfterNext() baggageAfterMiddleware.Should().Be(baggageBeforeMiddleware); } + [TestMethod] + public async Task OnTurnAsync_UserId_FallsBackToFromId_WhenAadObjectIdIsNull() + { + // Arrange — simulates email/Word/SPO channel where AadObjectId is null + var middleware = new BaggageTurnMiddleware(); + var turnContext = CreateTurnContext( + channelName: "outlook", + fromId: "lukemoenning@microsoft.com", + fromAadObjectId: null); + + string? capturedUserId = null; + + NextDelegate next = (ct) => + { + capturedUserId = Baggage.Current.GetBaggage(OpenTelemetryConstants.UserIdKey); + return Task.CompletedTask; + }; + + // Act + await middleware.OnTurnAsync(turnContext, next); + + // Assert + capturedUserId.Should().Be("lukemoenning@microsoft.com"); + } + + [TestMethod] + public async Task OnTurnAsync_UserId_FallsBackToAgenticUserId_WhenAadObjectIdIsNull() + { + // Arrange — simulates A2A call where AadObjectId is null but AgenticUserId is set + var middleware = new BaggageTurnMiddleware(); + var turnContext = CreateTurnContext( + fromId: "29:1sH5NArUwkWAX", + fromAadObjectId: null, + fromAgenticUserId: "agent@contoso.onmicrosoft.com"); + + string? capturedUserId = null; + + NextDelegate next = (ct) => + { + capturedUserId = Baggage.Current.GetBaggage(OpenTelemetryConstants.UserIdKey); + return Task.CompletedTask; + }; + + // Act + await middleware.OnTurnAsync(turnContext, next); + + // Assert + capturedUserId.Should().Be("agent@contoso.onmicrosoft.com"); + } + + [TestMethod] + public async Task OnTurnAsync_UserId_PrefersAadObjectId_WhenBothAadAndAgenticUserIdSet() + { + // Arrange — both AadObjectId and AgenticUserId are populated; AadObjectId should win + var middleware = new BaggageTurnMiddleware(); + var turnContext = CreateTurnContext( + channelName: "msteams", + fromId: "8:orgid:17649762-cd35-4a35-95ab-75eeb3017308", + fromAadObjectId: "aad-object-id-123", + fromAgenticUserId: "agent@contoso.onmicrosoft.com"); + + string? capturedUserId = null; + + NextDelegate next = (ct) => + { + capturedUserId = Baggage.Current.GetBaggage(OpenTelemetryConstants.UserIdKey); + return Task.CompletedTask; + }; + + // Act + await middleware.OnTurnAsync(turnContext, next); + + // Assert + capturedUserId.Should().Be("aad-object-id-123"); + } + + [TestMethod] + public async Task OnTurnAsync_UserId_FallsBackToGuidAgenticUserId() + { + // Arrange — A2A where AgenticUserId is a GUID, not an email + var middleware = new BaggageTurnMiddleware(); + var turnContext = CreateTurnContext( + fromId: "29:1sH5NArUwkWAX", + fromAadObjectId: null, + fromAgenticUserId: "bef730f4-d6f5-4ffb-b759-26ffa449ed7e"); + + string? capturedUserId = null; + + NextDelegate next = (ct) => + { + capturedUserId = Baggage.Current.GetBaggage(OpenTelemetryConstants.UserIdKey); + return Task.CompletedTask; + }; + + // Act + await middleware.OnTurnAsync(turnContext, next); + + // Assert + capturedUserId.Should().Be("bef730f4-d6f5-4ffb-b759-26ffa449ed7e"); + } + private static ITurnContext CreateTurnContext( string activityType = "message", - string? activityName = null) + string? activityName = null, + string? fromId = "caller-id", + string? fromAadObjectId = "caller-aad", + string? fromAgenticUserId = null, + string channelName = "msteams") { var mockActivity = new Mock(); mockActivity.Setup(a => a.Type).Returns(activityType); @@ -120,9 +225,10 @@ private static ITurnContext CreateTurnContext( mockActivity.Setup(a => a.Text).Returns("Hello"); mockActivity.Setup(a => a.From).Returns(new ChannelAccount { - Id = "caller-id", + Id = fromId, Name = "Caller", - AadObjectId = "caller-aad", + AadObjectId = fromAadObjectId, + AgenticUserId = fromAgenticUserId, }); mockActivity.Setup(a => a.Recipient).Returns(new ChannelAccount { @@ -133,7 +239,7 @@ private static ITurnContext CreateTurnContext( }); mockActivity.Setup(a => a.Conversation).Returns(new ConversationAccount { Id = "conv-id" }); mockActivity.Setup(a => a.ServiceUrl).Returns("https://example.com"); - mockActivity.Setup(a => a.ChannelId).Returns(new ChannelId("test-channel")); + mockActivity.Setup(a => a.ChannelId).Returns(new ChannelId(channelName)); var mockTurnContext = new Mock(); mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);