diff --git a/src/Observability/Runtime/Tracing/Scopes/ExecuteToolScope.cs b/src/Observability/Runtime/Tracing/Scopes/ExecuteToolScope.cs index 9a64e5c8..91f5bb0e 100644 --- a/src/Observability/Runtime/Tracing/Scopes/ExecuteToolScope.cs +++ b/src/Observability/Runtime/Tracing/Scopes/ExecuteToolScope.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Diagnostics; using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; @@ -30,6 +31,8 @@ public sealed class ExecuteToolScope : OpenTelemetryScope /// Optional metadata describing the source of the call (e.g., component, file, line) for observability. /// Optional threat diagnostics summary containing security-related information about blocked actions. /// Optional details about the non-agentic caller. + /// Optional explicit start time. Useful when recording a tool call after execution has already completed. + /// Optional explicit end time. When provided, the span will use this timestamp when disposed instead of the current wall-clock time. /// A new ExecuteToolScope instance. /// /// @@ -44,15 +47,17 @@ public sealed class ExecuteToolScope : OpenTelemetryScope /// Learn more about certification requirements /// /// - public static ExecuteToolScope Start(ToolCallDetails details, AgentDetails agentDetails, TenantDetails tenantDetails, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, ThreatDiagnosticsSummary? threatDiagnosticsSummary = null, CallerDetails? callerDetails = null) => new ExecuteToolScope(details, agentDetails, tenantDetails, parentId, conversationId, sourceMetadata, threatDiagnosticsSummary, callerDetails); + public static ExecuteToolScope Start(ToolCallDetails details, AgentDetails agentDetails, TenantDetails tenantDetails, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, ThreatDiagnosticsSummary? threatDiagnosticsSummary = null, CallerDetails? callerDetails = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null) => new ExecuteToolScope(details, agentDetails, tenantDetails, parentId, conversationId, sourceMetadata, threatDiagnosticsSummary, callerDetails, startTime, endTime); - private ExecuteToolScope(ToolCallDetails details, AgentDetails agentDetails, TenantDetails tenantDetails, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, ThreatDiagnosticsSummary? threatDiagnosticsSummary = null, CallerDetails? callerDetails = null) + private ExecuteToolScope(ToolCallDetails details, AgentDetails agentDetails, TenantDetails tenantDetails, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, ThreatDiagnosticsSummary? threatDiagnosticsSummary = null, CallerDetails? callerDetails = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null) : base( kind: ActivityKind.Internal, agentDetails: agentDetails, tenantDetails: tenantDetails, operationName: OperationName, activityName: $"{OperationName} {details.ToolName}", + startTime: startTime, + endTime: endTime, parentId: parentId, conversationId: conversationId, sourceMetadata: sourceMetadata, diff --git a/src/Observability/Runtime/Tracing/Scopes/InferenceScope.cs b/src/Observability/Runtime/Tracing/Scopes/InferenceScope.cs index fa79988e..eb440ac4 100644 --- a/src/Observability/Runtime/Tracing/Scopes/InferenceScope.cs +++ b/src/Observability/Runtime/Tracing/Scopes/InferenceScope.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Diagnostics; using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; using static Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes.OpenTelemetryConstants; @@ -25,6 +26,8 @@ public sealed class InferenceScope : OpenTelemetryScope /// Optional conversation or session correlation ID for the inference. /// Optional metadata describing the source of the call (e.g., component, file, line) for observability. /// Optional details about the non-agentic caller. + /// Optional explicit start time. Useful when recording an inference call after execution has already completed. + /// Optional explicit end time. When provided, the span will use this timestamp when disposed instead of the current wall-clock time. /// A new InferenceScope instance. /// /// @@ -39,15 +42,17 @@ public sealed class InferenceScope : OpenTelemetryScope /// Learn more about certification requirements /// /// - public static InferenceScope Start(InferenceCallDetails details, AgentDetails agentDetails, TenantDetails tenantDetails, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, CallerDetails? callerDetails = null) => new InferenceScope(details, agentDetails, tenantDetails, parentId, conversationId, sourceMetadata, callerDetails); + public static InferenceScope Start(InferenceCallDetails details, AgentDetails agentDetails, TenantDetails tenantDetails, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, CallerDetails? callerDetails = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null) => new InferenceScope(details, agentDetails, tenantDetails, parentId, conversationId, sourceMetadata, callerDetails, startTime, endTime); - private InferenceScope(InferenceCallDetails details, AgentDetails agentDetails, TenantDetails tenantDetails, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, CallerDetails? callerDetails = null) + private InferenceScope(InferenceCallDetails details, AgentDetails agentDetails, TenantDetails tenantDetails, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, CallerDetails? callerDetails = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null) : base( kind: ActivityKind.Client, agentDetails: agentDetails, tenantDetails: tenantDetails, operationName: details.OperationName.ToString(), activityName: $"{details.OperationName} {details.Model}", + startTime: startTime, + endTime: endTime, parentId: parentId, conversationId: conversationId, sourceMetadata: sourceMetadata, diff --git a/src/Observability/Runtime/Tracing/Scopes/InvokeAgentScope.cs b/src/Observability/Runtime/Tracing/Scopes/InvokeAgentScope.cs index 755221aa..5f0b4fac 100644 --- a/src/Observability/Runtime/Tracing/Scopes/InvokeAgentScope.cs +++ b/src/Observability/Runtime/Tracing/Scopes/InvokeAgentScope.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Diagnostics; using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; @@ -31,6 +32,8 @@ public sealed class InvokeAgentScope : OpenTelemetryScope /// The details of the non-agentic caller. /// The conversation ID for the agent invocation. /// Optional threat diagnostics summary containing security-related information about blocked actions. + /// Optional explicit start time. Useful when recording an agent invocation after execution has already completed. + /// Optional explicit end time. When provided, the span will use this timestamp when disposed instead of the current wall-clock time. /// A new InvokeAgentScope instance. /// /// @@ -50,9 +53,9 @@ public sealed class InvokeAgentScope : OpenTelemetryScope /// /// public static InvokeAgentScope Start( - InvokeAgentDetails invokeAgentDetails, TenantDetails tenantDetails, Request? request = null, AgentDetails? callerAgentDetails = null, CallerDetails? callerDetails = null, string? conversationId = null, ThreatDiagnosticsSummary? threatDiagnosticsSummary = null) => new InvokeAgentScope(invokeAgentDetails, tenantDetails, request, callerAgentDetails, callerDetails, conversationId, threatDiagnosticsSummary); + InvokeAgentDetails invokeAgentDetails, TenantDetails tenantDetails, Request? request = null, AgentDetails? callerAgentDetails = null, CallerDetails? callerDetails = null, string? conversationId = null, ThreatDiagnosticsSummary? threatDiagnosticsSummary = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null) => new InvokeAgentScope(invokeAgentDetails, tenantDetails, request, callerAgentDetails, callerDetails, conversationId, threatDiagnosticsSummary, startTime, endTime); - private InvokeAgentScope(InvokeAgentDetails invokeAgentDetails, TenantDetails tenantDetails, Request? request, AgentDetails? callerAgentDetails, CallerDetails? callerDetails, string? conversationId, ThreatDiagnosticsSummary? threatDiagnosticsSummary) + private InvokeAgentScope(InvokeAgentDetails invokeAgentDetails, TenantDetails tenantDetails, Request? request, AgentDetails? callerAgentDetails, CallerDetails? callerDetails, string? conversationId, ThreatDiagnosticsSummary? threatDiagnosticsSummary, DateTimeOffset? startTime, DateTimeOffset? endTime) : base( kind: ActivityKind.Client, agentDetails: invokeAgentDetails.Details, @@ -61,6 +64,8 @@ private InvokeAgentScope(InvokeAgentDetails invokeAgentDetails, TenantDetails te activityName: string.IsNullOrWhiteSpace(invokeAgentDetails.Details.AgentName) ? OperationName : $"invoke_agent {invokeAgentDetails.Details.AgentName}", + startTime: startTime, + endTime: endTime, conversationId: conversationId, sourceMetadata: request?.SourceMetadata, callerDetails: callerDetails) diff --git a/src/Observability/Runtime/Tracing/Scopes/OpenTelemetryScope.cs b/src/Observability/Runtime/Tracing/Scopes/OpenTelemetryScope.cs index 14e22b42..c3db1c04 100644 --- a/src/Observability/Runtime/Tracing/Scopes/OpenTelemetryScope.cs +++ b/src/Observability/Runtime/Tracing/Scopes/OpenTelemetryScope.cs @@ -43,13 +43,15 @@ public abstract class OpenTelemetryScope : IDisposable /// The name of the operation being traced. /// The name of the activity for display purposes. /// Optional custom start time for the scope. If not provided, the current time is used. + /// Optional custom end time for the scope. When provided, the span will use this timestamp when disposed instead of the current wall-clock time. /// Optional parent ID for the activity. /// Optional conversation id. /// Optional source metadata. /// Optional details about the non-agentic caller. - protected OpenTelemetryScope(ActivityKind kind, AgentDetails agentDetails, TenantDetails tenantDetails, string operationName, string activityName, DateTimeOffset? startTime = null, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, CallerDetails? callerDetails = null) + protected OpenTelemetryScope(ActivityKind kind, AgentDetails agentDetails, TenantDetails tenantDetails, string operationName, string activityName, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, CallerDetails? callerDetails = null) { customStartTime = startTime; + customEndTime = endTime; activity = ActivitySource.CreateActivity(activityName, kind, default(ActivityContext)); if (!string.IsNullOrEmpty(parentId)) { diff --git a/src/Observability/Runtime/Tracing/Scopes/OutputScope.cs b/src/Observability/Runtime/Tracing/Scopes/OutputScope.cs index 3c24fb34..2c00557e 100644 --- a/src/Observability/Runtime/Tracing/Scopes/OutputScope.cs +++ b/src/Observability/Runtime/Tracing/Scopes/OutputScope.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Diagnostics; using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; @@ -26,17 +27,21 @@ public sealed class OutputScope : OpenTelemetryScope /// Tenant context used for telemetry enrichment and correlation. /// Response containing output messages. /// Optional parent Activity ID used to link this span to an upstream operation. + /// Optional explicit start time. Useful when recording an output operation after execution has already completed. + /// Optional explicit end time. When provided, the span will use this timestamp when disposed instead of the current wall-clock time. /// A new OutputScope instance. - public static OutputScope Start(AgentDetails agentDetails, TenantDetails tenantDetails, Response response, string? parentId = null) - => new OutputScope(agentDetails, tenantDetails, response, parentId); + public static OutputScope Start(AgentDetails agentDetails, TenantDetails tenantDetails, Response response, string? parentId = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null) + => new OutputScope(agentDetails, tenantDetails, response, parentId, startTime, endTime); - private OutputScope(AgentDetails agentDetails, TenantDetails tenantDetails, Response response, string? parentId) + private OutputScope(AgentDetails agentDetails, TenantDetails tenantDetails, Response response, string? parentId, DateTimeOffset? startTime, DateTimeOffset? endTime) : base( kind: ActivityKind.Client, agentDetails: agentDetails, tenantDetails: tenantDetails, operationName: OperationName, activityName: $"{OperationName} {agentDetails?.AgentId}", + startTime: startTime, + endTime: endTime, parentId: parentId) { if (response.Messages.Count > 0) diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/ExecuteToolScopeTest.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/ExecuteToolScopeTest.cs index 3bcc5f65..89830cfa 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/ExecuteToolScopeTest.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/ExecuteToolScopeTest.cs @@ -258,4 +258,73 @@ public void Start_SetsCallerDetails_WhenProvided() activity.ShouldHaveTag(OpenTelemetryConstants.GenAiCallerClientIpKey, callerDetails.CallerClientIP!.ToString()); activity.ShouldHaveTag(OpenTelemetryConstants.GenAiCallerTenantIdKey, callerDetails.TenantId!); } + + [TestMethod] + public void Start_WithCustomStartTime_SetsActivityStartTime() + { + // Arrange + var customStartTime = new DateTimeOffset(2023, 11, 14, 22, 13, 20, TimeSpan.Zero); + + // Act + var activity = ListenForActivity(() => + { + using var scope = ExecuteToolScope.Start( + new ToolCallDetails("TestTool", "args"), + Util.GetAgentDetails(), + Util.GetTenantDetails(), + startTime: customStartTime); + }); + + // Assert + var startTime = new DateTimeOffset(activity.StartTimeUtc); + startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); + } + + [TestMethod] + public void Start_WithCustomStartAndEndTime_SetsActivityTimes() + { + // Arrange + var customStartTime = new DateTimeOffset(2023, 11, 14, 22, 13, 20, TimeSpan.Zero); + var customEndTime = new DateTimeOffset(2023, 11, 14, 22, 13, 25, TimeSpan.Zero); // 5 seconds later + + // Act + var activity = ListenForActivity(() => + { + using var scope = ExecuteToolScope.Start( + new ToolCallDetails("TestTool", "args"), + Util.GetAgentDetails(), + Util.GetTenantDetails(), + startTime: customStartTime, + endTime: customEndTime); + }); + + // Assert - Start time should be set to custom time + var startTime = new DateTimeOffset(activity.StartTimeUtc); + startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); + } + + [TestMethod] + public void SetEndTime_OverridesEndTime() + { + // Arrange + var customStartTime = new DateTimeOffset(2023, 11, 14, 22, 13, 40, TimeSpan.Zero); + var initialEndTime = new DateTimeOffset(2023, 11, 14, 22, 13, 45, TimeSpan.Zero); + var laterEndTime = new DateTimeOffset(2023, 11, 14, 22, 13, 48, TimeSpan.Zero); + + // Act + var activity = ListenForActivity(() => + { + using var scope = ExecuteToolScope.Start( + new ToolCallDetails("TestTool", "args"), + Util.GetAgentDetails(), + Util.GetTenantDetails(), + startTime: customStartTime, + endTime: initialEndTime); + scope.SetEndTime(laterEndTime); + }); + + // Assert - The start time should be set + var startTime = new DateTimeOffset(activity.StartTimeUtc); + startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); + } } \ No newline at end of file diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/InferenceScopeTest.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/InferenceScopeTest.cs index 798f2d2e..634779fa 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/InferenceScopeTest.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/InferenceScopeTest.cs @@ -280,4 +280,85 @@ public void Start_SetsCallerDetails_WhenProvided() activity.ShouldHaveTag(OpenTelemetryConstants.GenAiCallerClientIpKey, callerDetails.CallerClientIP!.ToString()); activity.ShouldHaveTag(OpenTelemetryConstants.GenAiCallerTenantIdKey, callerDetails.TenantId!); } + + [TestMethod] + public void Start_WithCustomStartTime_SetsActivityStartTime() + { + // Arrange + var customStartTime = new DateTimeOffset(2023, 11, 14, 22, 13, 20, TimeSpan.Zero); + var details = new InferenceCallDetails( + InferenceOperationType.Chat, + "gpt-4o", + "openai"); + + // Act + var activity = ListenForActivity(() => + { + using var scope = InferenceScope.Start( + details, + Util.GetAgentDetails(), + Util.GetTenantDetails(), + startTime: customStartTime); + }); + + // Assert + var startTime = new DateTimeOffset(activity.StartTimeUtc); + startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); + } + + [TestMethod] + public void Start_WithCustomStartAndEndTime_SetsActivityTimes() + { + // Arrange + var customStartTime = new DateTimeOffset(2023, 11, 14, 22, 13, 20, TimeSpan.Zero); + var customEndTime = new DateTimeOffset(2023, 11, 14, 22, 13, 25, TimeSpan.Zero); // 5 seconds later + var details = new InferenceCallDetails( + InferenceOperationType.Chat, + "gpt-4o", + "openai"); + + // Act + var activity = ListenForActivity(() => + { + using var scope = InferenceScope.Start( + details, + Util.GetAgentDetails(), + Util.GetTenantDetails(), + startTime: customStartTime, + endTime: customEndTime); + }); + + // Assert - Start time should be set to custom time + var startTime = new DateTimeOffset(activity.StartTimeUtc); + startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); + } + + [TestMethod] + public void SetEndTime_OverridesEndTime() + { + // Arrange + var customStartTime = new DateTimeOffset(2023, 11, 14, 22, 13, 40, TimeSpan.Zero); + var initialEndTime = new DateTimeOffset(2023, 11, 14, 22, 13, 45, TimeSpan.Zero); + var laterEndTime = new DateTimeOffset(2023, 11, 14, 22, 13, 48, TimeSpan.Zero); + var details = new InferenceCallDetails( + InferenceOperationType.Chat, + "gpt-4o", + "openai"); + + // Act + var activity = ListenForActivity(() => + { + using var scope = InferenceScope.Start( + details, + Util.GetAgentDetails(), + Util.GetTenantDetails(), + startTime: customStartTime, + endTime: initialEndTime); + scope.SetEndTime(laterEndTime); + }); + + // Assert - The start time should be set + var startTime = new DateTimeOffset(activity.StartTimeUtc); + startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); + } } diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/InvokeAgentScopeTest.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/InvokeAgentScopeTest.cs index 5601e393..7acb1499 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/InvokeAgentScopeTest.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/InvokeAgentScopeTest.cs @@ -306,4 +306,70 @@ public void RecordThreatDiagnosticsSummary_SetsTagCorrectly() tagValue.Should().Contain("\"reason\":\"Blocked due to policy violation.\""); tagValue.Should().Contain("data-loss-prevention"); } + + [TestMethod] + public void Start_WithCustomStartTime_SetsActivityStartTime() + { + // Arrange + var customStartTime = new DateTimeOffset(2023, 11, 14, 22, 13, 20, TimeSpan.Zero); + + // Act + var activity = ListenForActivity(() => + { + using var scope = InvokeAgentScope.Start( + Details, + Util.GetTenantDetails(), + startTime: customStartTime); + }); + + // Assert + var startTime = new DateTimeOffset(activity.StartTimeUtc); + startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); + } + + [TestMethod] + public void Start_WithCustomStartAndEndTime_SetsActivityTimes() + { + // Arrange + var customStartTime = new DateTimeOffset(2023, 11, 14, 22, 13, 20, TimeSpan.Zero); + var customEndTime = new DateTimeOffset(2023, 11, 14, 22, 13, 25, TimeSpan.Zero); // 5 seconds later + + // Act + var activity = ListenForActivity(() => + { + using var scope = InvokeAgentScope.Start( + Details, + Util.GetTenantDetails(), + startTime: customStartTime, + endTime: customEndTime); + }); + + // Assert - Start time should be set to custom time + var startTime = new DateTimeOffset(activity.StartTimeUtc); + startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); + } + + [TestMethod] + public void SetEndTime_OverridesEndTime() + { + // Arrange + var customStartTime = new DateTimeOffset(2023, 11, 14, 22, 13, 40, TimeSpan.Zero); + var initialEndTime = new DateTimeOffset(2023, 11, 14, 22, 13, 45, TimeSpan.Zero); + var laterEndTime = new DateTimeOffset(2023, 11, 14, 22, 13, 48, TimeSpan.Zero); + + // Act + var activity = ListenForActivity(() => + { + using var scope = InvokeAgentScope.Start( + Details, + Util.GetTenantDetails(), + startTime: customStartTime, + endTime: initialEndTime); + scope.SetEndTime(laterEndTime); + }); + + // Assert - The start time should be set + var startTime = new DateTimeOffset(activity.StartTimeUtc); + startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); + } } \ No newline at end of file diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/OutputScopeTest.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/OutputScopeTest.cs index 20da5dd4..020e30b4 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/OutputScopeTest.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/OutputScopeTest.cs @@ -90,4 +90,82 @@ public void Start_WithParentId_SetsParentIdCorrectly() childActivity.ShouldHaveTag(OpenTelemetryConstants.GenAiOperationNameKey, OutputScope.OperationName); childActivity.ShouldHaveTag(OpenTelemetryConstants.GenAiOutputMessagesKey, "Test message"); } + + [TestMethod] + public void Start_WithCustomStartTime_SetsActivityStartTime() + { + // Arrange + var customStartTime = new DateTimeOffset(2023, 11, 14, 22, 13, 20, TimeSpan.Zero); + var response = new Response(new[] { "Test message" }); + var agentDetails = Util.GetAgentDetails(); + var tenantDetails = Util.GetTenantDetails(); + + // Act + var activity = ListenForActivity(() => + { + using var scope = OutputScope.Start( + agentDetails, + tenantDetails, + response, + startTime: customStartTime); + }); + + // Assert + var startTime = new DateTimeOffset(activity.StartTimeUtc); + startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); + } + + [TestMethod] + public void Start_WithCustomStartAndEndTime_SetsActivityTimes() + { + // Arrange + var customStartTime = new DateTimeOffset(2023, 11, 14, 22, 13, 20, TimeSpan.Zero); + var customEndTime = new DateTimeOffset(2023, 11, 14, 22, 13, 25, TimeSpan.Zero); // 5 seconds later + var response = new Response(new[] { "Test message" }); + var agentDetails = Util.GetAgentDetails(); + var tenantDetails = Util.GetTenantDetails(); + + // Act + var activity = ListenForActivity(() => + { + using var scope = OutputScope.Start( + agentDetails, + tenantDetails, + response, + startTime: customStartTime, + endTime: customEndTime); + }); + + // Assert - Start time should be set to custom time + var startTime = new DateTimeOffset(activity.StartTimeUtc); + startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); + } + + [TestMethod] + public void SetEndTime_OverridesEndTime() + { + // Arrange + var customStartTime = new DateTimeOffset(2023, 11, 14, 22, 13, 40, TimeSpan.Zero); + var initialEndTime = new DateTimeOffset(2023, 11, 14, 22, 13, 45, TimeSpan.Zero); + var laterEndTime = new DateTimeOffset(2023, 11, 14, 22, 13, 48, TimeSpan.Zero); + var response = new Response(new[] { "Test message" }); + var agentDetails = Util.GetAgentDetails(); + var tenantDetails = Util.GetTenantDetails(); + + // Act + var activity = ListenForActivity(() => + { + using var scope = OutputScope.Start( + agentDetails, + tenantDetails, + response, + startTime: customStartTime, + endTime: initialEndTime); + scope.SetEndTime(laterEndTime); + }); + + // Assert - The start time should be set + var startTime = new DateTimeOffset(activity.StartTimeUtc); + startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); + } }