-
Notifications
You must be signed in to change notification settings - Fork 750
.NET: [Feature Branch] Durable Task extension integration tests #2017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cgillum
merged 6 commits into
microsoft:feature-azure-functions
from
cgillum:integration-tests2
Nov 10, 2025
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
46e7b94
Add .NET integration tests
cgillum 46c13be
Azure Functions integration tests
cgillum 90848d3
Merge branch 'feature-azure-functions' into integration-tests2
cgillum 972b009
Copilot PR fixes
cgillum a54a2a2
Merge branch 'integration-tests2' of https://github.com/cgillum/agent…
cgillum 3f269cf
Remove unused code
cgillum File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/AgentEntityTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System.Diagnostics; | ||
| using System.Reflection; | ||
| using Microsoft.DurableTask.Client; | ||
| using Microsoft.DurableTask.Client.Entities; | ||
| using Microsoft.DurableTask.Entities; | ||
| using Microsoft.Extensions.Configuration; | ||
| using OpenAI; | ||
| using Xunit.Abstractions; | ||
|
|
||
| namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; | ||
|
|
||
| /// <summary> | ||
| /// Tests for scenarios where an external client interacts with Durable Task Agents. | ||
| /// </summary> | ||
| [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).CreateAIAgent( | ||
| 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); | ||
|
|
||
| AgentThread thread = simpleAgentProxy.GetNewThread(); | ||
|
|
||
| DurableTaskClient client = testHelper.GetClient(); | ||
|
|
||
| AgentSessionId sessionId = thread.GetService<AgentSessionId>(); | ||
| 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!", | ||
| thread, | ||
| cancellationToken: this.TestTimeoutToken); | ||
|
|
||
| // Assert: verify the agent state was stored with the correct entity name prefix | ||
| entity = await client.Entities.GetEntityAsync(expectedEntityId, false, this.TestTimeoutToken); | ||
|
|
||
| Assert.NotNull(entity); | ||
| } | ||
| } |
215 changes: 215 additions & 0 deletions
215
dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System.ComponentModel; | ||
| using System.Diagnostics; | ||
| using System.Reflection; | ||
| using Microsoft.Agents.AI.DurableTask.IntegrationTests.Logging; | ||
| using Microsoft.DurableTask; | ||
| using Microsoft.DurableTask.Client; | ||
| using Microsoft.Extensions.AI; | ||
| using Microsoft.Extensions.Configuration; | ||
| using OpenAI; | ||
| using Xunit.Abstractions; | ||
|
|
||
| namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; | ||
|
|
||
| /// <summary> | ||
| /// Tests for scenarios where an external client interacts with Durable Task Agents. | ||
| /// </summary> | ||
| [Collection("Sequential")] | ||
| [Trait("Category", "Integration")] | ||
| public sealed class ExternalClientTests(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 SimplePromptAsync() | ||
| { | ||
| // Setup | ||
| AIAgent simpleAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).CreateAIAgent( | ||
| instructions: "You are a helpful assistant that always responds with a friendly greeting.", | ||
| name: "TestAgent"); | ||
|
|
||
| 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); | ||
|
|
||
| // Act: send a prompt to the agent and wait for a response | ||
| AgentThread thread = simpleAgentProxy.GetNewThread(); | ||
| await simpleAgentProxy.RunAsync( | ||
| message: "Hello!", | ||
| thread, | ||
| cancellationToken: this.TestTimeoutToken); | ||
|
|
||
| AgentRunResponse response = await simpleAgentProxy.RunAsync( | ||
| message: "Repeat what you just said but say it like a pirate", | ||
| thread, | ||
| cancellationToken: this.TestTimeoutToken); | ||
|
|
||
| // Assert: verify the agent responded appropriately | ||
| // We can't predict the exact response, but we can check that there is one response | ||
| Assert.NotNull(response); | ||
| Assert.NotEmpty(response.Text); | ||
|
|
||
| // Assert: verify the expected log entries were created in the expected category | ||
| IReadOnlyCollection<LogEntry> logs = testHelper.GetLogs(); | ||
| Assert.NotEmpty(logs); | ||
| List<LogEntry> agentLogs = [.. logs.Where(log => log.Category.Contains(simpleAgent.Name!)).ToList()]; | ||
| Assert.NotEmpty(agentLogs); | ||
| Assert.Contains(agentLogs, log => log.EventId.Name == "LogAgentRequest" && log.Message.Contains("Hello!")); | ||
| Assert.Contains(agentLogs, log => log.EventId.Name == "LogAgentResponse"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task CallFunctionToolsAsync() | ||
| { | ||
| int weatherToolInvocationCount = 0; | ||
| int packingListToolInvocationCount = 0; | ||
|
|
||
| string GetWeather(string location) | ||
| { | ||
| weatherToolInvocationCount++; | ||
| return $"The weather in {location} is sunny with a high of 75°F and a low of 55°F."; | ||
| } | ||
|
|
||
| string SuggestPackingList(string weather, bool isSunny) | ||
| { | ||
| packingListToolInvocationCount++; | ||
| return isSunny ? "Pack sunglasses and sunscreen." : "Pack a raincoat and umbrella."; | ||
| } | ||
|
|
||
| AIAgent tripPlanningAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).CreateAIAgent( | ||
| instructions: "You are a trip planning assistant. Use the weather tool and packing list tool as needed.", | ||
| name: "TripPlanningAgent", | ||
| description: "An agent to help plan your day trips", | ||
| tools: [AIFunctionFactory.Create(GetWeather), AIFunctionFactory.Create(SuggestPackingList)] | ||
| ); | ||
|
|
||
| using TestHelper testHelper = TestHelper.Start([tripPlanningAgent], this._outputHelper); | ||
| AIAgent tripPlanningAgentProxy = tripPlanningAgent.AsDurableAgentProxy(testHelper.Services); | ||
|
|
||
| // Act: send a prompt to the agent | ||
| AgentRunResponse response = await tripPlanningAgentProxy.RunAsync( | ||
| message: "Help me figure out what to pack for my Seattle trip next Sunday", | ||
| cancellationToken: this.TestTimeoutToken); | ||
|
|
||
| // Assert: verify the agent responded appropriately | ||
| // We can't predict the exact response, but we can check that there is one response | ||
| Assert.NotNull(response); | ||
| Assert.NotEmpty(response.Text); | ||
|
|
||
| // Assert: verify the expected log entries were created in the expected category | ||
| IReadOnlyCollection<LogEntry> logs = testHelper.GetLogs(); | ||
| Assert.NotEmpty(logs); | ||
|
|
||
| List<LogEntry> agentLogs = [.. logs.Where(log => log.Category.Contains(tripPlanningAgent.Name!)).ToList()]; | ||
| Assert.NotEmpty(agentLogs); | ||
| Assert.Contains(agentLogs, log => log.EventId.Name == "LogAgentRequest" && log.Message.Contains("Seattle trip")); | ||
| Assert.Contains(agentLogs, log => log.EventId.Name == "LogAgentResponse"); | ||
|
|
||
| // Assert: verify the tools were called | ||
| Assert.Equal(1, weatherToolInvocationCount); | ||
| Assert.Equal(1, packingListToolInvocationCount); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task CallLongRunningFunctionToolsAsync() | ||
| { | ||
| [Description("Starts a greeting workflow and returns the workflow instance ID")] | ||
| string StartWorkflowTool(string name) | ||
| { | ||
| return DurableAgentContext.Current.ScheduleNewOrchestration(nameof(RunWorkflowAsync), input: name); | ||
| } | ||
|
|
||
| [Description("Gets the current status of a previously started workflow. A null response means the workflow has not started yet.")] | ||
| static async Task<OrchestrationMetadata?> GetWorkflowStatusToolAsync(string instanceId) | ||
| { | ||
| OrchestrationMetadata? status = await DurableAgentContext.Current.GetOrchestrationStatusAsync( | ||
| instanceId, | ||
| includeDetails: true); | ||
| if (status == null) | ||
| { | ||
| // If the status is not found, wait a bit before returning null to give the workflow time to start | ||
| await Task.Delay(TimeSpan.FromSeconds(1)); | ||
| } | ||
|
|
||
| return status; | ||
| } | ||
|
|
||
| async Task<string> RunWorkflowAsync(TaskOrchestrationContext context, string name) | ||
| { | ||
| // 1. Get agent and create a session | ||
| DurableAIAgent agent = context.GetAgent("SimpleAgent"); | ||
| AgentThread thread = agent.GetNewThread(); | ||
|
|
||
| // 2. Call an agent and tell it my name | ||
| await agent.RunAsync($"My name is {name}.", thread); | ||
|
|
||
| // 3. Call the agent again with the same thread (ask it to tell me my name) | ||
| AgentRunResponse response = await agent.RunAsync("What is my name?", thread); | ||
|
|
||
| return response.Text; | ||
| } | ||
|
|
||
| using TestHelper testHelper = TestHelper.Start( | ||
| this._outputHelper, | ||
| configureAgents: agents => | ||
| { | ||
| // This is the agent that will be used to start the workflow | ||
| agents.AddAIAgentFactory( | ||
| "WorkflowAgent", | ||
| sp => TestHelper.GetAzureOpenAIChatClient(s_configuration).CreateAIAgent( | ||
| name: "WorkflowAgent", | ||
| instructions: "You can start greeting workflows and check their status.", | ||
| services: sp, | ||
| tools: [ | ||
| AIFunctionFactory.Create(StartWorkflowTool), | ||
| AIFunctionFactory.Create(GetWorkflowStatusToolAsync) | ||
| ])); | ||
|
|
||
| // This is the agent that will be called by the workflow | ||
| agents.AddAIAgent(TestHelper.GetAzureOpenAIChatClient(s_configuration).CreateAIAgent( | ||
| name: "SimpleAgent", | ||
| instructions: "You are a simple assistant." | ||
| )); | ||
| }, | ||
| durableTaskRegistry: registry => registry.AddOrchestratorFunc<string, string>(nameof(RunWorkflowAsync), RunWorkflowAsync)); | ||
|
|
||
| AIAgent workflowManagerAgentProxy = testHelper.Services.GetDurableAgentProxy("WorkflowAgent"); | ||
|
|
||
| // Act: send a prompt to the agent | ||
| AgentThread thread = workflowManagerAgentProxy.GetNewThread(); | ||
| await workflowManagerAgentProxy.RunAsync( | ||
| message: "Start a greeting workflow for \"John Doe\".", | ||
| thread, | ||
| cancellationToken: this.TestTimeoutToken); | ||
|
|
||
| // Act: prompt it again to wait for the workflow to complete | ||
| AgentRunResponse response = await workflowManagerAgentProxy.RunAsync( | ||
| message: "Wait for the workflow to complete and tell me the result.", | ||
| thread, | ||
| cancellationToken: this.TestTimeoutToken); | ||
|
|
||
| // Assert: verify the agent responded appropriately | ||
| // We can't predict the exact response, but we can check that there is one response | ||
| Assert.NotNull(response); | ||
| Assert.NotEmpty(response.Text); | ||
| Assert.Contains("John Doe", response.Text); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.