// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
using System.Reflection;
using Microsoft.Agents.AI.DurableTask.State;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Client.Entities;
using Microsoft.DurableTask.Entities;
using Microsoft.Extensions.Configuration;
using OpenAI.Chat;
namespace Microsoft.Agents.AI.DurableTask.IntegrationTests;
///
/// Tests for scenarios where an external client interacts with Durable Task Agents.
///
[Collection("Sequential")]
[Trait("Category", "Integration")]
public sealed class AgentEntityTests(ITestOutputHelper outputHelper) : IDisposable
{
private static readonly TimeSpan s_defaultTimeout = Debugger.IsAttached
? TimeSpan.FromMinutes(5)
: TimeSpan.FromSeconds(30);
private static readonly IConfiguration s_configuration =
new ConfigurationBuilder()
.AddUserSecrets(Assembly.GetExecutingAssembly())
.AddEnvironmentVariables()
.Build();
private readonly ITestOutputHelper _outputHelper = outputHelper;
private readonly CancellationTokenSource _cts = new(delay: s_defaultTimeout);
private CancellationToken TestTimeoutToken => this._cts.Token;
public void Dispose() => this._cts.Dispose();
[Fact]
public async Task EntityNamePrefixAsync()
{
// Setup
AIAgent simpleAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).AsAIAgent(
name: "TestAgent",
instructions: "You are a helpful assistant that always responds with a friendly greeting."
);
using TestHelper testHelper = TestHelper.Start([simpleAgent], this._outputHelper);
// A proxy agent is needed to call the hosted test agent
AIAgent simpleAgentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services);
AgentSession session = await simpleAgentProxy.CreateSessionAsync(this.TestTimeoutToken);
DurableTaskClient client = testHelper.GetClient();
AgentSessionId sessionId = session.GetService();
EntityInstanceId expectedEntityId = new($"dafx-{simpleAgent.Name}", sessionId.Key);
EntityMetadata? entity = await client.Entities.GetEntityAsync(expectedEntityId, false, this.TestTimeoutToken);
Assert.Null(entity);
// Act: send a prompt to the agent
await simpleAgentProxy.RunAsync(
message: "Hello!",
session,
cancellationToken: this.TestTimeoutToken);
// Assert: verify the agent state was stored with the correct entity name prefix
entity = await client.Entities.GetEntityAsync(expectedEntityId, true, this.TestTimeoutToken);
Assert.NotNull(entity);
Assert.True(entity.IncludesState);
DurableAgentState state = entity.State.ReadAs();
DurableAgentStateRequest request = Assert.Single(state.Data.ConversationHistory.OfType());
Assert.Null(request.OrchestrationId);
}
[Theory]
[InlineData("run")]
[InlineData("Run")]
[InlineData("RunAgentAsync")]
public async Task RunAgentMethodNamesAllWorkAsync(string runAgentMethodName)
{
// Setup
AIAgent simpleAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).AsAIAgent(
name: "TestAgent",
instructions: "You are a helpful assistant that always responds with a friendly greeting."
);
using TestHelper testHelper = TestHelper.Start([simpleAgent], this._outputHelper);
// A proxy agent is needed to call the hosted test agent
AIAgent simpleAgentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services);
AgentSession session = await simpleAgentProxy.CreateSessionAsync(this.TestTimeoutToken);
DurableTaskClient client = testHelper.GetClient();
AgentSessionId sessionId = session.GetService();
EntityInstanceId expectedEntityId = new($"dafx-{simpleAgent.Name}", sessionId.Key);
EntityMetadata? entity = await client.Entities.GetEntityAsync(expectedEntityId, false, this.TestTimeoutToken);
Assert.Null(entity);
// Act: send a prompt to the agent
await client.Entities.SignalEntityAsync(
expectedEntityId,
runAgentMethodName,
new RunRequest("Hello!"),
cancellation: this.TestTimeoutToken);
while (!this.TestTimeoutToken.IsCancellationRequested)
{
await Task.Delay(500, this.TestTimeoutToken);
// Assert: verify the agent state was stored with the correct entity name prefix
entity = await client.Entities.GetEntityAsync(expectedEntityId, true, this.TestTimeoutToken);
if (entity is not null)
{
break;
}
}
Assert.NotNull(entity);
Assert.True(entity.IncludesState);
DurableAgentState state = entity.State.ReadAs();
DurableAgentStateRequest request = Assert.Single(state.Data.ConversationHistory.OfType());
Assert.Null(request.OrchestrationId);
}
[Fact]
public async Task OrchestrationIdSetDuringOrchestrationAsync()
{
// Arrange
AIAgent simpleAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).AsAIAgent(
name: "TestAgent",
instructions: "You are a helpful assistant that always responds with a friendly greeting."
);
using TestHelper testHelper = TestHelper.Start(
[simpleAgent],
this._outputHelper,
registry => registry.AddOrchestrator());
DurableTaskClient client = testHelper.GetClient();
// Act
string orchestrationId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(TestOrchestrator), "What is the capital of Maine?");
OrchestrationMetadata? status = await client.WaitForInstanceCompletionAsync(
orchestrationId,
true,
this.TestTimeoutToken);
// Assert
EntityInstanceId expectedEntityId = AgentSessionId.Parse(status.ReadOutputAs()!);
EntityMetadata? entity = await client.Entities.GetEntityAsync(expectedEntityId, true, this.TestTimeoutToken);
Assert.NotNull(entity);
Assert.True(entity.IncludesState);
DurableAgentState state = entity.State.ReadAs();
DurableAgentStateRequest request = Assert.Single(state.Data.ConversationHistory.OfType());
Assert.Equal(orchestrationId, request.OrchestrationId);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Constructed via reflection.")]
private sealed class TestOrchestrator : TaskOrchestrator
{
public override async Task RunAsync(TaskOrchestrationContext context, string input)
{
DurableAIAgent writer = context.GetAgent("TestAgent");
AgentSession writerSession = await writer.CreateSessionAsync();
await writer.RunAsync(
message: context.GetInput()!,
session: writerSession);
AgentSessionId sessionId = writerSession.GetService();
return sessionId.ToString();
}
}
}