mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into feature-session-statebag
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Agents.AI.CosmosNoSql.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="DeclarativeWorkflowOptions"/> telemetry configuration.
|
||||
/// </summary>
|
||||
[Collection("DeclarativeWorkflowOptionsTest")]
|
||||
public sealed class DeclarativeWorkflowOptionsTest : IDisposable
|
||||
{
|
||||
// These constants mirror Microsoft.Agents.AI.Workflows.Observability.ActivityNames
|
||||
// which is internal and not accessible from this test project.
|
||||
private const string WorkflowBuildActivityName = "workflow.build";
|
||||
private const string WorkflowRunActivityName = "workflow_invoke";
|
||||
|
||||
// The default activity source name used by the workflow telemetry context.
|
||||
private const string DefaultTelemetrySourceName = "Microsoft.Agents.AI.Workflows";
|
||||
|
||||
private const string SimpleWorkflowYaml = """
|
||||
kind: Workflow
|
||||
trigger:
|
||||
kind: OnConversationStart
|
||||
id: test_workflow
|
||||
actions:
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
""";
|
||||
|
||||
private readonly ActivitySource _activitySource = new("TestSource");
|
||||
private readonly ActivityListener _activityListener;
|
||||
private readonly ConcurrentBag<Activity> _capturedActivities = [];
|
||||
|
||||
public DeclarativeWorkflowOptionsTest()
|
||||
{
|
||||
this._activityListener = new ActivityListener
|
||||
{
|
||||
ShouldListenTo = source =>
|
||||
source.Name == DefaultTelemetrySourceName ||
|
||||
source.Name == "TestSource",
|
||||
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData,
|
||||
ActivityStarted = activity => this._capturedActivities.Add(activity),
|
||||
};
|
||||
ActivitySource.AddActivityListener(this._activityListener);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._activityListener.Dispose();
|
||||
this._activitySource.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureTelemetry_DefaultIsNull()
|
||||
{
|
||||
// Arrange
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Null(options.ConfigureTelemetry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureTelemetry_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
bool callbackInvoked = false;
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
ConfigureTelemetry = opt =>
|
||||
{
|
||||
callbackInvoked = true;
|
||||
opt.EnableSensitiveData = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(options.ConfigureTelemetry);
|
||||
WorkflowTelemetryOptions telemetryOptions = new();
|
||||
options.ConfigureTelemetry(telemetryOptions);
|
||||
Assert.True(callbackInvoked);
|
||||
Assert.True(telemetryOptions.EnableSensitiveData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TelemetryActivitySource_DefaultIsNull()
|
||||
{
|
||||
// Arrange
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Null(options.TelemetryActivitySource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TelemetryActivitySource_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
TelemetryActivitySource = this._activitySource
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Same(this._activitySource, options.TelemetryActivitySource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithDefaultTelemetry_AppliesTelemetryAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("DefaultTelemetryTest").Start()!;
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
ConfigureTelemetry = _ => { },
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId && a.Source.Name == DefaultTelemetrySourceName)
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(capturedActivities);
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowBuildActivityName, StringComparison.Ordinal));
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowRunActivityName, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithTelemetryActivitySource_AppliesTelemetryAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("TelemetryActivitySourceTest").Start()!;
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
TelemetryActivitySource = this._activitySource,
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId && a.Source.Name == "TestSource")
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(capturedActivities);
|
||||
Assert.All(capturedActivities, a => Assert.Equal("TestSource", a.Source.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithConfigureTelemetry_AppliesConfigurationAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("ConfigureTelemetryTest").Start()!;
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
bool configureInvoked = false;
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
ConfigureTelemetry = opt =>
|
||||
{
|
||||
configureInvoked = true;
|
||||
opt.EnableSensitiveData = true;
|
||||
},
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert
|
||||
Assert.True(configureInvoked);
|
||||
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId && a.Source.Name == DefaultTelemetrySourceName)
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(capturedActivities);
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowBuildActivityName, StringComparison.Ordinal));
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowRunActivityName, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithoutTelemetry_DoesNotCreateActivitiesAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("NoTelemetryTest").Start()!;
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert - No workflow activities should be created when telemetry is disabled
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId &&
|
||||
(a.OperationName.StartsWith(WorkflowBuildActivityName, StringComparison.Ordinal) ||
|
||||
a.OperationName.StartsWith(WorkflowRunActivityName, StringComparison.Ordinal)))
|
||||
.ToArray();
|
||||
|
||||
Assert.Empty(capturedActivities);
|
||||
}
|
||||
|
||||
private static Mock<WorkflowAgentProvider> CreateMockProvider()
|
||||
{
|
||||
Mock<WorkflowAgentProvider> mockAgentProvider = new(MockBehavior.Strict);
|
||||
mockAgentProvider
|
||||
.Setup(provider => provider.CreateConversationAsync(It.IsAny<CancellationToken>()))
|
||||
.Returns(() => Task.FromResult(Guid.NewGuid().ToString("N")));
|
||||
mockAgentProvider
|
||||
.Setup(provider => provider.CreateMessageAsync(It.IsAny<string>(), It.IsAny<ChatMessage>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.FromResult(new ChatMessage(ChatRole.Assistant, "Test response")));
|
||||
return mockAgentProvider;
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
|
||||
return builder.Build();
|
||||
return builder.WithOpenTelemetry().Build();
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> GetExpectedActivityNameCounts() =>
|
||||
@@ -111,8 +111,6 @@ public sealed class ObservabilityTests : IDisposable
|
||||
Run run = await executionEnvironment.RunAsync(workflow, "Hello, World!");
|
||||
await run.DisposeAsync();
|
||||
|
||||
await Task.Delay(100); // Allow time for activities to be captured
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().HaveCount(8, "Exactly 8 activities should be created.");
|
||||
@@ -122,12 +120,12 @@ public sealed class ObservabilityTests : IDisposable
|
||||
{
|
||||
var activityName = kvp.Key;
|
||||
var expectedCount = kvp.Value;
|
||||
var actualCount = capturedActivities.Count(a => a.OperationName == activityName);
|
||||
var actualCount = capturedActivities.Count(a => a.OperationName.StartsWith(activityName, StringComparison.Ordinal));
|
||||
actualCount.Should().Be(expectedCount, $"Activity '{activityName}' should occur {expectedCount} times.");
|
||||
}
|
||||
|
||||
// Verify WorkflowRun activity events include workflow lifecycle events
|
||||
var workflowRunActivity = capturedActivities.First(a => a.OperationName == ActivityNames.WorkflowRun);
|
||||
var workflowRunActivity = capturedActivities.First(a => a.OperationName.StartsWith(ActivityNames.WorkflowRun, StringComparison.Ordinal));
|
||||
var activityEvents = workflowRunActivity.Events.ToList();
|
||||
activityEvents.Should().Contain(e => e.Name == EventNames.WorkflowStarted, "activity should have workflow started event");
|
||||
activityEvents.Should().Contain(e => e.Name == EventNames.WorkflowCompleted, "activity should have workflow completed event");
|
||||
@@ -166,8 +164,6 @@ public sealed class ObservabilityTests : IDisposable
|
||||
|
||||
// Act
|
||||
CreateWorkflow();
|
||||
await Task.Delay(100); // Allow time for activities to be captured
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().HaveCount(1, "Exactly 1 activity should be created.");
|
||||
@@ -183,4 +179,325 @@ public sealed class ObservabilityTests : IDisposable
|
||||
tags.Should().ContainKey(Tags.WorkflowId);
|
||||
tags.Should().ContainKey(Tags.WorkflowDefinition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TelemetryDisabledByDefault_CreatesNoActivitiesAsync()
|
||||
{
|
||||
// Arrange
|
||||
// Create a test activity to correlate captured activities
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
// Act - Build workflow WITHOUT calling WithOpenTelemetry()
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.Build(); // No WithOpenTelemetry() call
|
||||
// Assert - No activities should be created
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().BeEmpty("No activities should be created when telemetry is disabled (default).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WithOpenTelemetry_UsesProvidedActivitySourceAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
using var userActivitySource = new ActivitySource("UserProvidedSource");
|
||||
|
||||
// Set up a separate listener for the user-provided source
|
||||
ConcurrentBag<Activity> userActivities = [];
|
||||
using var userListener = new ActivityListener
|
||||
{
|
||||
ShouldListenTo = source => source.Name == "UserProvidedSource",
|
||||
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData,
|
||||
ActivityStarted = activity => userActivities.Add(activity),
|
||||
};
|
||||
ActivitySource.AddActivityListener(userListener);
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry(activitySource: userActivitySource).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = userActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotBeEmpty("Activities should be created with user-provided ActivitySource.");
|
||||
capturedActivities.Should().OnlyContain(
|
||||
a => a.Source.Name == "UserProvidedSource",
|
||||
"All activities should come from the user-provided ActivitySource.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableWorkflowBuild_PreventsWorkflowBuildActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOpenTelemetry(configure: opts => opts.DisableWorkflowBuild = true).Build();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.WorkflowBuild, StringComparison.Ordinal),
|
||||
"WorkflowBuild activity should be disabled.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableWorkflowRun_PreventsWorkflowRunActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOutputFrom(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry(configure: opts => opts.DisableWorkflowRun = true).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.WorkflowRun, StringComparison.Ordinal),
|
||||
"WorkflowRun activity should be disabled.");
|
||||
capturedActivities.Should().Contain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.WorkflowBuild, StringComparison.Ordinal),
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableExecutorProcess_PreventsExecutorProcessActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOutputFrom(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry(configure: opts => opts.DisableExecutorProcess = true).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal),
|
||||
"ExecutorProcess activity should be disabled.");
|
||||
capturedActivities.Should().Contain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.WorkflowRun, StringComparison.Ordinal),
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableEdgeGroupProcess_PreventsEdgeGroupProcessActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
var workflow = CreateWorkflowWithDisabledEdges();
|
||||
|
||||
// Act
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.EdgeGroupProcess, StringComparison.Ordinal),
|
||||
"EdgeGroupProcess activity should be disabled.");
|
||||
capturedActivities.Should().Contain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal),
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableMessageSend_PreventsMessageSendActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
var workflow = CreateWorkflowWithDisabledMessages();
|
||||
|
||||
// Act
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.MessageSend, StringComparison.Ordinal),
|
||||
"MessageSend activity should be disabled.");
|
||||
capturedActivities.Should().Contain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal),
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
private static Workflow CreateWorkflowWithDisabledEdges()
|
||||
{
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
Func<string, string> reverseFunc = s => new string(s.Reverse().ToArray());
|
||||
var reverse = reverseFunc.BindAsExecutor("ReverseTextExecutor");
|
||||
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
|
||||
return builder.WithOpenTelemetry(configure: opts => opts.DisableEdgeGroupProcess = true).Build();
|
||||
}
|
||||
|
||||
private static Workflow CreateWorkflowWithDisabledMessages()
|
||||
{
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
Func<string, string> reverseFunc = s => new string(s.Reverse().ToArray());
|
||||
var reverse = reverseFunc.BindAsExecutor("ReverseTextExecutor");
|
||||
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
|
||||
return builder.WithOpenTelemetry(configure: opts => opts.DisableMessageSend = true).Build();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSensitiveData_LogsExecutorInputAndOutputAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOutputFrom(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry(configure: opts => opts.EnableSensitiveData = true).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
var executorActivity = capturedActivities.FirstOrDefault(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal));
|
||||
|
||||
executorActivity.Should().NotBeNull("ExecutorProcess activity should be created.");
|
||||
|
||||
var tags = executorActivity!.Tags.ToDictionary(t => t.Key, t => t.Value);
|
||||
tags.Should().ContainKey(Tags.ExecutorInput, "Input should be logged when EnableSensitiveData is true.");
|
||||
tags.Should().ContainKey(Tags.ExecutorOutput, "Output should be logged when EnableSensitiveData is true.");
|
||||
tags[Tags.ExecutorInput].Should().Contain("hello", "Input should contain the input value.");
|
||||
tags[Tags.ExecutorOutput].Should().Contain("HELLO", "Output should contain the transformed value.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSensitiveData_Disabled_DoesNotLogInputOutputAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act - EnableSensitiveData is false by default
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOutputFrom(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry().Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
var executorActivity = capturedActivities.FirstOrDefault(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal));
|
||||
|
||||
executorActivity.Should().NotBeNull("ExecutorProcess activity should be created.");
|
||||
|
||||
var tags = executorActivity!.Tags.ToDictionary(t => t.Key, t => t.Value);
|
||||
tags.Should().NotContainKey(Tags.ExecutorInput, "Input should NOT be logged when EnableSensitiveData is false.");
|
||||
tags.Should().NotContainKey(Tags.ExecutorOutput, "Output should NOT be logged when EnableSensitiveData is false.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSensitiveData_LogsMessageSendContentAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
Func<string, string> reverseFunc = s => new string(s.Reverse().ToArray());
|
||||
var reverse = reverseFunc.BindAsExecutor("ReverseTextExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
var workflow = builder.WithOpenTelemetry(configure: opts => opts.EnableSensitiveData = true).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
var messageSendActivity = capturedActivities.FirstOrDefault(
|
||||
a => a.OperationName.StartsWith(ActivityNames.MessageSend, StringComparison.Ordinal));
|
||||
|
||||
messageSendActivity.Should().NotBeNull("MessageSend activity should be created.");
|
||||
|
||||
var tags = messageSendActivity!.Tags.ToDictionary(t => t.Key, t => t.Value);
|
||||
tags.Should().ContainKey(Tags.MessageContent, "Message content should be logged when EnableSensitiveData is true.");
|
||||
tags.Should().ContainKey(Tags.MessageSourceId, "Source ID should be logged.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSensitiveData_Disabled_DoesNotLogMessageContentAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
Func<string, string> reverseFunc = s => new string(s.Reverse().ToArray());
|
||||
var reverse = reverseFunc.BindAsExecutor("ReverseTextExecutor");
|
||||
|
||||
// Act - EnableSensitiveData is false by default
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
var workflow = builder.WithOpenTelemetry().Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
var messageSendActivity = capturedActivities.FirstOrDefault(
|
||||
a => a.OperationName.StartsWith(ActivityNames.MessageSend, StringComparison.Ordinal));
|
||||
|
||||
messageSendActivity.Should().NotBeNull("MessageSend activity should be created.");
|
||||
|
||||
var tags = messageSendActivity!.Tags.ToDictionary(t => t.Key, t => t.Value);
|
||||
tags.Should().NotContainKey(Tags.MessageContent, "Message content should NOT be logged when EnableSensitiveData is false.");
|
||||
tags.Should().ContainKey(Tags.MessageSourceId, "Source ID should still be logged.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Execution;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
@@ -133,6 +134,8 @@ public class TestRunContext : IRunnerContext
|
||||
public bool WithCheckpointing => false;
|
||||
public bool ConcurrentRunsEnabled => false;
|
||||
|
||||
WorkflowTelemetryContext IRunnerContext.TelemetryContext => WorkflowTelemetryContext.Disabled;
|
||||
|
||||
ValueTask<Executor> IRunnerContext.EnsureExecutorAsync(string executorId, IStepTracer? tracer, CancellationToken cancellationToken) =>
|
||||
new(this.Executors[executorId]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user