Files
agent-framework/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIServerSentEventsResultTests.cs
Javier Calvarro Nelson b03a4fb95e .NET: AG-UI support for .NET (#1776)
* Initial plan

* Infrastructure setup

* Plan for minimal client

* Plan update

* Basic agentic chat

* cleanup

* Cleanups

* More cleanups

* Cleanups

* More cleanups

* Test plan

* Sample

* Fix streaming and error handling

* Fix notifications

* Cleanups

* cleanup sample

* Additional tests

* Additional tests

* Run dotnet format

* Remove unnecessary files

* Mark packages as non packable

* Fix build

* Address feedback

* Fix build

* Fix remaining warnings

* Feedback

* Feedback and cleanup

* Cleanup

* Cleanups

* Cleanups

* Cleanups

* Retrieve existing messages from the store to send them along the way and update the sample client

* Run dotnet format

* Add ADR for AG-UI

* Switch to use the SG and use a convention for run ids

* Cleanup MapAGUI API

* Fix formatting

* Fix solution

* Fix solution
2025-11-05 15:51:37 +00:00

153 lines
5.6 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests;
/// <summary>
/// Unit tests for the <see cref="AGUIServerSentEventsResult"/> class.
/// </summary>
public sealed class AGUIServerSentEventsResultTests
{
[Fact]
public async Task ExecuteAsync_SetsCorrectResponseHeaders_ContentTypeAndCacheControlAsync()
{
// Arrange
List<BaseEvent> events = [];
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
AGUIServerSentEventsResult result = new(events.ToAsyncEnumerableAsync(), logger);
DefaultHttpContext httpContext = new();
httpContext.Response.Body = new MemoryStream();
// Act
await result.ExecuteAsync(httpContext);
// Assert
Assert.Equal("text/event-stream", httpContext.Response.ContentType);
Assert.Equal("no-cache,no-store", httpContext.Response.Headers.CacheControl.ToString());
Assert.Equal("no-cache", httpContext.Response.Headers.Pragma.ToString());
}
[Fact]
public async Task ExecuteAsync_SerializesEventsInSSEFormat_WithDataPrefixAndNewlinesAsync()
{
// Arrange
List<BaseEvent> events =
[
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
];
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
AGUIServerSentEventsResult result = new(events.ToAsyncEnumerableAsync(), logger);
DefaultHttpContext httpContext = new();
MemoryStream responseStream = new();
httpContext.Response.Body = responseStream;
// Act
await result.ExecuteAsync(httpContext);
// Assert
string responseContent = Encoding.UTF8.GetString(responseStream.ToArray());
Assert.Contains("data: ", responseContent);
Assert.Contains("\n\n", responseContent);
string[] eventStrings = responseContent.Split("\n\n", StringSplitOptions.RemoveEmptyEntries);
Assert.Equal(2, eventStrings.Length);
}
[Fact]
public async Task ExecuteAsync_FlushesResponse_AfterEachEventAsync()
{
// Arrange
List<BaseEvent> events =
[
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
new TextMessageContentEvent { MessageId = "msg1", Delta = "Hello" },
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
];
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
AGUIServerSentEventsResult result = new(events.ToAsyncEnumerableAsync(), logger);
DefaultHttpContext httpContext = new();
MemoryStream responseStream = new();
httpContext.Response.Body = responseStream;
// Act
await result.ExecuteAsync(httpContext);
// Assert
string responseContent = Encoding.UTF8.GetString(responseStream.ToArray());
string[] eventStrings = responseContent.Split("\n\n", StringSplitOptions.RemoveEmptyEntries);
Assert.Equal(3, eventStrings.Length);
}
[Fact]
public async Task ExecuteAsync_WithEmptyEventStream_CompletesSuccessfullyAsync()
{
// Arrange
List<BaseEvent> events = [];
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
AGUIServerSentEventsResult result = new(events.ToAsyncEnumerableAsync(), logger);
DefaultHttpContext httpContext = new();
httpContext.Response.Body = new MemoryStream();
// Act
await result.ExecuteAsync(httpContext);
// Assert
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
}
[Fact]
public async Task ExecuteAsync_RespectsCancellationToken_WhenCancelledAsync()
{
// Arrange
using CancellationTokenSource cts = new();
List<BaseEvent> events =
[
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
new TextMessageContentEvent { MessageId = "msg1", Delta = "Hello" }
];
async IAsyncEnumerable<BaseEvent> GetEventsWithCancellationAsync()
{
foreach (BaseEvent evt in events)
{
yield return evt;
await Task.Delay(10);
}
}
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
AGUIServerSentEventsResult result = new(GetEventsWithCancellationAsync(), logger);
DefaultHttpContext httpContext = new();
httpContext.Response.Body = new MemoryStream();
httpContext.RequestAborted = cts.Token;
// Act
cts.Cancel();
// Assert
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => result.ExecuteAsync(httpContext));
}
[Fact]
public async Task ExecuteAsync_WithNullHttpContext_ThrowsArgumentNullExceptionAsync()
{
// Arrange
List<BaseEvent> events = [];
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
AGUIServerSentEventsResult result = new(events.ToAsyncEnumerableAsync(), logger);
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => result.ExecuteAsync(null!));
}
}