Files
agent-framework/dotnet/tests/Microsoft.Agents.AI.Valkey.UnitTests/ValkeyChatHistoryProviderTests.cs
T
Matthias Howell 8e1998ddcb .NET: Adds Valkey to chat message history - issue 5445 (#5542)
* Adds Valkey to chat message history

* Address review: switch to Valkey.Glide, add options class, remove context provider

- Switch from StackExchange.Redis to Valkey.Glide 1.1.0 (official Valkey .NET client)
- Extract optional params into ValkeyChatHistoryProviderOptions
- Add JsonSerializerOptions support, remove [RequiresUnreferencedCode]
- Make MaxMessages/MaxMessagesToRetrieve readonly via options
- Remove ValkeyContextProvider (overlaps with ChatHistoryMemoryProvider + MEVD)
- Remove ValkeyProviderScope (only used by context provider)
- Remove connection string constructors (caller manages IConnectionMultiplexer)
- Update samples to use new API and gpt-5.4-mini

* Use type-safe JsonSerializer overloads, remove suppress attributes

Use JsonSerializerOptions.GetTypeInfo() for Serialize/Deserialize calls
to enable NativeAOT/trimming compatibility without suppress attributes.
Default to AgentAbstractionsJsonUtilities.DefaultOptions when no options provided.

Signed-off-by: Matthias Howell <matthias.howell@improving.com>

* Update READMEs: remove context provider references

Remove ValkeyContextProvider and long-term memory references from sample
READMEs since the context provider was removed from this PR. Simplify
Valkey server requirements (no search module needed for chat history).

Signed-off-by: Matthias Howell <matthias.howell@improving.com>

* Apply suggestion from @westey-m

* Fix formatting (dotnet format)

Signed-off-by: Matthias Howell <matthias.howell@improving.com>

* Update dotnet/src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

---------

Signed-off-by: Matthias Howell <matthias.howell@improving.com>
Co-authored-by: Matthias Howell <matthias.howell@yoppworks.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2026-06-11 13:18:00 +00:00

250 lines
8.0 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
using Valkey.Glide;
namespace Microsoft.Agents.AI.Valkey.UnitTests;
/// <summary>
/// Unit tests for <see cref="ValkeyChatHistoryProvider"/>.
/// </summary>
public sealed class ValkeyChatHistoryProviderTests
{
private static Mock<IConnectionMultiplexer> CreateMockConnection(Mock<IDatabase>? dbMock = null)
{
var mockConnection = new Mock<IConnectionMultiplexer>();
dbMock ??= new Mock<IDatabase>();
mockConnection.Setup(c => c.GetDatabase()).Returns(dbMock.Object);
return mockConnection;
}
// --- Constructor tests ---
[Fact]
public void Constructor_WithConnection_SetsProperties()
{
// Arrange & Act
var provider = new ValkeyChatHistoryProvider(
CreateMockConnection().Object,
static (_) => new ValkeyChatHistoryProvider.State("conv-1"),
new ValkeyChatHistoryProviderOptions { KeyPrefix = "test_prefix" });
// Assert
Assert.NotNull(provider);
}
[Fact]
public void Constructor_WithConnection_NullConnection_Throws()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
new ValkeyChatHistoryProvider(
null!,
static (_) => new ValkeyChatHistoryProvider.State("conv-1")));
}
[Fact]
public void Constructor_WithConnection_NullStateInitializer_Throws()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
new ValkeyChatHistoryProvider(
CreateMockConnection().Object,
null!));
}
// --- State tests ---
[Fact]
public void State_NullConversationId_Throws()
{
Assert.Throws<ArgumentNullException>(() => new ValkeyChatHistoryProvider.State(null!));
}
[Fact]
public void State_EmptyConversationId_Throws()
{
Assert.Throws<ArgumentException>(() => new ValkeyChatHistoryProvider.State(""));
}
[Fact]
public void State_ValidConversationId_SetsProperty()
{
var state = new ValkeyChatHistoryProvider.State("my-conversation");
Assert.Equal("my-conversation", state.ConversationId);
}
[Fact]
public void State_JsonConstructor_RoundTrips()
{
// Arrange
var original = new ValkeyChatHistoryProvider.State("test-conv");
// Act
var json = JsonSerializer.Serialize(original);
var deserialized = JsonSerializer.Deserialize<ValkeyChatHistoryProvider.State>(json);
// Assert
Assert.NotNull(deserialized);
Assert.Equal("test-conv", deserialized.ConversationId);
}
// --- StateKeys tests ---
[Fact]
public void StateKeys_ReturnsProviderTypeName()
{
var provider = new ValkeyChatHistoryProvider(
CreateMockConnection().Object,
_ => new ValkeyChatHistoryProvider.State("conv-1"));
var keys = provider.StateKeys;
Assert.Single(keys);
Assert.Equal(nameof(ValkeyChatHistoryProvider), keys[0]);
}
[Fact]
public void StateKeys_WithCustomKey_ReturnsCustomKey()
{
var provider = new ValkeyChatHistoryProvider(
CreateMockConnection().Object,
_ => new ValkeyChatHistoryProvider.State("conv-1"),
new ValkeyChatHistoryProviderOptions { StateKey = "custom_key" });
var keys = provider.StateKeys;
Assert.Single(keys);
Assert.Equal("custom_key", keys[0]);
}
// --- ProvideChatHistoryAsync tests ---
[Fact]
public async Task ProvideChatHistoryAsync_ReturnsDeserializedMessagesAsync()
{
// Arrange
var dbMock = new Mock<IDatabase>();
var msg1 = new ChatMessage(ChatRole.User, "hello");
var msg2 = new ChatMessage(ChatRole.Assistant, "hi there");
var values = new ValkeyValue[]
{
JsonSerializer.Serialize(msg1),
JsonSerializer.Serialize(msg2)
};
dbMock.Setup(d => d.ListRangeAsync(It.IsAny<ValkeyKey>(), It.IsAny<long>(), It.IsAny<long>()))
.ReturnsAsync(values);
var provider = new ValkeyChatHistoryProvider(
CreateMockConnection(dbMock).Object,
_ => new ValkeyChatHistoryProvider.State("conv-1"));
var context = TestHelpers.CreateChatHistoryInvokingContext();
// Act — should not throw
var result = await provider.InvokingAsync(context);
var messages = result.ToList();
// Assert — only the valid message + request message
Assert.True(messages.Count >= 1);
}
[Fact]
public async Task ProvideChatHistoryAsync_WithMaxMessagesToRetrieve_UsesRangeQueryAsync()
{
// Arrange
var dbMock = new Mock<IDatabase>();
dbMock.Setup(d => d.ListRangeAsync(It.IsAny<ValkeyKey>(), It.IsAny<long>(), It.IsAny<long>()))
.ReturnsAsync([]);
var provider = new ValkeyChatHistoryProvider(
CreateMockConnection(dbMock).Object,
_ => new ValkeyChatHistoryProvider.State("conv-1"),
new ValkeyChatHistoryProviderOptions { MaxMessagesToRetrieve = 5 });
var context = TestHelpers.CreateChatHistoryInvokingContext();
// Act
await provider.InvokingAsync(context);
// Assert — should use -5, -1 range
dbMock.Verify(d => d.ListRangeAsync(
It.IsAny<ValkeyKey>(), -5, -1), Times.Once);
}
[Fact]
public async Task ProvideChatHistoryAsync_CancellationToken_ThrowsAsync()
{
// Arrange
var provider = new ValkeyChatHistoryProvider(
CreateMockConnection().Object,
_ => new ValkeyChatHistoryProvider.State("conv-1"));
var cts = new CancellationTokenSource();
cts.Cancel();
var context = TestHelpers.CreateChatHistoryInvokingContext();
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(() =>
provider.InvokingAsync(context, cts.Token).AsTask());
}
// --- StoreChatHistoryAsync tests ---
[Fact]
public async Task StoreChatHistoryAsync_BatchPushesMessagesAsync()
{
// Arrange
var dbMock = new Mock<IDatabase>();
dbMock.Setup(d => d.ListRightPushAsync(It.IsAny<ValkeyKey>(), It.IsAny<ValkeyValue[]>()))
.ReturnsAsync(2);
var provider = new ValkeyChatHistoryProvider(
CreateMockConnection(dbMock).Object,
_ => new ValkeyChatHistoryProvider.State("conv-1"));
var context = TestHelpers.CreateChatHistoryInvokedContext(
[new ChatMessage(ChatRole.User, "hello")],
[new ChatMessage(ChatRole.Assistant, "hi")]);
// Act
await provider.InvokedAsync(context);
// Assert — batch push called once with array
dbMock.Verify(d => d.ListRightPushAsync(
It.IsAny<ValkeyKey>(), It.IsAny<ValkeyValue[]>()), Times.Once);
}
[Fact]
public async Task StoreChatHistoryAsync_WithMaxMessages_TrimsAsync()
{
// Arrange
var dbMock = new Mock<IDatabase>();
dbMock.Setup(d => d.ListRightPushAsync(It.IsAny<ValkeyKey>(), It.IsAny<ValkeyValue[]>()))
.ReturnsAsync(1);
dbMock.Setup(d => d.ListTrimAsync(It.IsAny<ValkeyKey>(), It.IsAny<long>(), It.IsAny<long>()))
.Returns(Task.CompletedTask);
var provider = new ValkeyChatHistoryProvider(
CreateMockConnection(dbMock).Object,
_ => new ValkeyChatHistoryProvider.State("conv-1"),
new ValkeyChatHistoryProviderOptions { MaxMessages = 10 });
var context = TestHelpers.CreateChatHistoryInvokedContext(
[new ChatMessage(ChatRole.User, "hello")],
[new ChatMessage(ChatRole.Assistant, "hi")]);
// Act
await provider.InvokedAsync(context);
// Assert — trim called unconditionally when MaxMessages is set
dbMock.Verify(d => d.ListTrimAsync(
It.IsAny<ValkeyKey>(), -10, -1), Times.Once);
}
}