build-flow.api/tests/BuildFlow.Rag.Tests/Chat/RedisChatMessageCacheTests.cs
2026-07-24 09:58:24 +08:00

102 lines
3.8 KiB
C#

using BuildFlow.Domain.Interfaces;
using BuildFlow.Infrastructure.Cache;
using FluentAssertions;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using Xunit;
namespace BuildFlow.Rag.Tests.Chat;
/// <summary>Redis List 聊天缓存的并发追加、传输粒度、顺序和 TTL 集成测试。</summary>
[Collection(RedisChatMessageCacheCollection.Name)]
public sealed class RedisChatMessageCacheTests(RedisChatMessageCacheFixture fixture)
{
[Fact]
public async Task ConcurrentAppendsPreserveEveryMessage()
{
var conversationId = Guid.NewGuid();
await fixture.DeleteAsync(conversationId);
var cache = CreateCache();
var messages = Enumerable.Range(0, 100)
.Select(index => CreateMessage(index))
.ToArray();
await Task.WhenAll(messages.Select(message =>
cache.AppendMessageAsync(conversationId, message, CancellationToken.None)));
var stored = await cache.GetMessagesAsync(conversationId, CancellationToken.None);
var database = fixture.Redis.GetDatabase();
stored.Should().NotBeNull();
var storedMessages = stored!;
storedMessages.Should().HaveCount(100);
storedMessages.Select(x => x.Id).Should().OnlyHaveUniqueItems();
(await database.KeyTypeAsync(RedisChatMessageCacheFixture.Key(conversationId)))
.Should().Be(RedisType.List);
(await database.ListLengthAsync(RedisChatMessageCacheFixture.Key(conversationId)))
.Should().Be(100);
}
[Fact]
public async Task AppendAddsOneListElementAndRefreshesConfiguredTtl()
{
var conversationId = Guid.NewGuid();
await fixture.DeleteAsync(conversationId);
var cache = CreateCache();
var existing = Enumerable.Range(0, 500)
.Select(CreateMessage)
.ToList();
await cache.SetMessagesAsync(conversationId, existing, CancellationToken.None);
var database = fixture.Redis.GetDatabase();
var key = RedisChatMessageCacheFixture.Key(conversationId);
await cache.AppendMessageAsync(
conversationId,
CreateMessage(500),
CancellationToken.None);
(await database.KeyTypeAsync(key)).Should().Be(RedisType.List);
(await database.ListLengthAsync(key)).Should().Be(501);
var ttl = await database.KeyTimeToLiveAsync(key);
ttl.Should().NotBeNull();
ttl!.Value.Should().BeGreaterThan(TimeSpan.FromMinutes(4));
ttl.Value.Should().BeLessThanOrEqualTo(TimeSpan.FromMinutes(5));
}
[Fact]
public async Task SetAndGetRoundTripCompleteHistoryInOrder()
{
var conversationId = Guid.NewGuid();
await fixture.DeleteAsync(conversationId);
var cache = CreateCache();
var messages = Enumerable.Range(0, 20)
.Select(CreateMessage)
.ToList();
await cache.SetMessagesAsync(conversationId, messages, CancellationToken.None);
var result = await cache.GetMessagesAsync(conversationId, CancellationToken.None);
result.Should().NotBeNull();
var storedMessages = result!;
storedMessages.Select(x => x.Id).Should().Equal(messages.Select(x => x.Id));
storedMessages.Select(x => x.Content).Should().Equal(messages.Select(x => x.Content));
}
private ChatMessageCache CreateCache()
=> new(
fixture.Redis,
Options.Create(new ChatOptions
{
MaxContextMessages = 50,
MaxContextTokens = 8000,
CacheTtlMinutes = 5,
}));
private static CachedChatMessage CreateMessage(int index)
=> new(
Guid.NewGuid(),
index % 2 == 0 ? "User" : "Assistant",
$"message-{index}",
null,
new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMinutes(index));
}