102 lines
3.8 KiB
C#
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));
|
|
}
|