chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using Microsoft.Agents.AI.DurableTask.State;
|
||||
using Microsoft.DurableTask.Client;
|
||||
using Microsoft.DurableTask.Client.Entities;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using OpenAI.Chat;
|
||||
|
||||
namespace Microsoft.Agents.AI.DurableTask.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for Time-To-Live (TTL) functionality of durable agent entities.
|
||||
/// </summary>
|
||||
[Collection("Sequential")]
|
||||
[Trait("Category", "IntegrationDisabled")]
|
||||
public sealed class TimeToLiveTests(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 EntityExpiresAfterTTLAsync()
|
||||
{
|
||||
// Arrange: Create agent with short TTL (10 seconds)
|
||||
TimeSpan ttl = TimeSpan.FromSeconds(10);
|
||||
AIAgent simpleAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).AsAIAgent(
|
||||
name: "TTLTestAgent",
|
||||
instructions: "You are a helpful assistant."
|
||||
);
|
||||
|
||||
using TestHelper testHelper = TestHelper.Start(
|
||||
this._outputHelper,
|
||||
options =>
|
||||
{
|
||||
options.DefaultTimeToLive = ttl;
|
||||
options.MinimumTimeToLiveSignalDelay = TimeSpan.FromSeconds(1);
|
||||
options.AddAIAgent(simpleAgent);
|
||||
});
|
||||
|
||||
AIAgent agentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services);
|
||||
AgentSession session = await agentProxy.CreateSessionAsync(this.TestTimeoutToken);
|
||||
DurableTaskClient client = testHelper.GetClient();
|
||||
AgentSessionId sessionId = session.GetService<AgentSessionId>();
|
||||
|
||||
// Act: Send a message to the agent
|
||||
await agentProxy.RunAsync(
|
||||
message: "Hello!",
|
||||
session,
|
||||
cancellationToken: this.TestTimeoutToken);
|
||||
|
||||
// Verify entity exists and get expiration time
|
||||
EntityMetadata? entity = await client.Entities.GetEntityAsync(sessionId, true, this.TestTimeoutToken);
|
||||
Assert.NotNull(entity);
|
||||
Assert.True(entity.IncludesState);
|
||||
|
||||
DurableAgentState state = entity.State.ReadAs<DurableAgentState>();
|
||||
Assert.NotNull(state.Data.ExpirationTimeUtc);
|
||||
DateTime expirationTime = state.Data.ExpirationTimeUtc.Value;
|
||||
Assert.True(expirationTime > DateTime.UtcNow);
|
||||
|
||||
// Calculate how long to wait: expiration time + buffer for signal processing
|
||||
TimeSpan waitTime = expirationTime - DateTime.UtcNow + TimeSpan.FromSeconds(1);
|
||||
if (waitTime > TimeSpan.Zero)
|
||||
{
|
||||
await Task.Delay(waitTime, this.TestTimeoutToken);
|
||||
}
|
||||
|
||||
// Poll the entity state until it's deleted (with timeout)
|
||||
DateTime pollTimeout = DateTime.UtcNow.AddSeconds(10);
|
||||
bool entityDeleted = false;
|
||||
while (DateTime.UtcNow < pollTimeout && !entityDeleted)
|
||||
{
|
||||
entity = await client.Entities.GetEntityAsync(sessionId, true, this.TestTimeoutToken);
|
||||
entityDeleted = entity is null;
|
||||
|
||||
if (!entityDeleted)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), this.TestTimeoutToken);
|
||||
}
|
||||
}
|
||||
|
||||
// Assert: Verify entity state is deleted
|
||||
Assert.True(entityDeleted, "Entity should have been deleted after TTL expiration");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EntityTTLResetsOnInteractionAsync()
|
||||
{
|
||||
// Arrange: Create agent with short TTL
|
||||
TimeSpan ttl = TimeSpan.FromSeconds(6);
|
||||
AIAgent simpleAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).AsAIAgent(
|
||||
name: "TTLResetTestAgent",
|
||||
instructions: "You are a helpful assistant."
|
||||
);
|
||||
|
||||
using TestHelper testHelper = TestHelper.Start(
|
||||
this._outputHelper,
|
||||
options =>
|
||||
{
|
||||
options.DefaultTimeToLive = ttl;
|
||||
options.MinimumTimeToLiveSignalDelay = TimeSpan.FromSeconds(1);
|
||||
options.AddAIAgent(simpleAgent);
|
||||
});
|
||||
|
||||
AIAgent agentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services);
|
||||
AgentSession session = await agentProxy.CreateSessionAsync(this.TestTimeoutToken);
|
||||
DurableTaskClient client = testHelper.GetClient();
|
||||
AgentSessionId sessionId = session.GetService<AgentSessionId>();
|
||||
|
||||
// Act: Send first message
|
||||
await agentProxy.RunAsync(
|
||||
message: "Hello!",
|
||||
session,
|
||||
cancellationToken: this.TestTimeoutToken);
|
||||
|
||||
EntityMetadata? entity = await client.Entities.GetEntityAsync(sessionId, true, this.TestTimeoutToken);
|
||||
Assert.NotNull(entity);
|
||||
Assert.True(entity.IncludesState);
|
||||
|
||||
DurableAgentState state = entity.State.ReadAs<DurableAgentState>();
|
||||
DateTime firstExpirationTime = state.Data.ExpirationTimeUtc!.Value;
|
||||
|
||||
// Wait partway through TTL
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), this.TestTimeoutToken);
|
||||
|
||||
// Send second message (should reset TTL)
|
||||
await agentProxy.RunAsync(
|
||||
message: "Hello again!",
|
||||
session,
|
||||
cancellationToken: this.TestTimeoutToken);
|
||||
|
||||
// Verify expiration time was updated
|
||||
entity = await client.Entities.GetEntityAsync(sessionId, true, this.TestTimeoutToken);
|
||||
Assert.NotNull(entity);
|
||||
Assert.True(entity.IncludesState);
|
||||
|
||||
state = entity.State.ReadAs<DurableAgentState>();
|
||||
DateTime secondExpirationTime = state.Data.ExpirationTimeUtc!.Value;
|
||||
Assert.True(secondExpirationTime > firstExpirationTime);
|
||||
|
||||
// Calculate when the original expiration time would have been
|
||||
DateTime originalExpirationTime = firstExpirationTime;
|
||||
TimeSpan waitUntilOriginalExpiration = originalExpirationTime - DateTime.UtcNow + TimeSpan.FromSeconds(2);
|
||||
|
||||
if (waitUntilOriginalExpiration > TimeSpan.Zero)
|
||||
{
|
||||
await Task.Delay(waitUntilOriginalExpiration, this.TestTimeoutToken);
|
||||
}
|
||||
|
||||
// Assert: Entity should still exist because TTL was reset
|
||||
// The new expiration time should be in the future
|
||||
entity = await client.Entities.GetEntityAsync(sessionId, true, this.TestTimeoutToken);
|
||||
Assert.NotNull(entity);
|
||||
Assert.True(entity.IncludesState);
|
||||
|
||||
state = entity.State.ReadAs<DurableAgentState>();
|
||||
Assert.NotNull(state);
|
||||
Assert.NotNull(state.Data.ExpirationTimeUtc);
|
||||
Assert.True(
|
||||
state.Data.ExpirationTimeUtc > DateTime.UtcNow,
|
||||
"Entity should still be valid because TTL was reset");
|
||||
|
||||
// Wait for the entity to be deleted
|
||||
DateTime pollTimeout = DateTime.UtcNow.AddSeconds(10);
|
||||
bool entityDeleted = false;
|
||||
while (DateTime.UtcNow < pollTimeout && !entityDeleted)
|
||||
{
|
||||
entity = await client.Entities.GetEntityAsync(sessionId, true, this.TestTimeoutToken);
|
||||
entityDeleted = entity is null;
|
||||
|
||||
if (!entityDeleted)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), this.TestTimeoutToken);
|
||||
}
|
||||
}
|
||||
|
||||
// Assert: Entity should have been deleted
|
||||
Assert.True(entityDeleted, "Entity should have been deleted after TTL expiration");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user