79492eac16
Comprehensive NUnit test suite covering ChannelManager, CliChannel, TelegramChannel, DI registration, and integration scenarios. Uses Moq for mocking with test stubs for isolated unit testing.
241 lines
7.9 KiB
C#
241 lines
7.9 KiB
C#
using Luna.Channels.Abstractions;
|
|
using Luna.Channels.Tests.Stubs;
|
|
using Luna.Core.Abstractions;
|
|
using Luna.Shared;
|
|
using Microsoft.Extensions.Logging;
|
|
using Moq;
|
|
|
|
namespace Luna.Channels.Tests;
|
|
|
|
[TestFixture]
|
|
[Category("Unit")]
|
|
public class ChannelManagerTests
|
|
{
|
|
private readonly Mock<ILogger<ChannelManager>> loggerMock = new();
|
|
|
|
private static Mock<IChannel> BuildChannelMock(string id)
|
|
{
|
|
var mock = new Mock<IChannel>();
|
|
mock.Setup(c => c.ChannelId).Returns(id);
|
|
mock.Setup(c => c.ChannelType).Returns("test");
|
|
mock.Setup(c => c.DisplayName).Returns($"Channel {id}");
|
|
mock.Setup(c => c.IsConnected).Returns(true);
|
|
mock.Setup(c => c.SendStreamingMessageAsync(
|
|
It.IsAny<IAsyncEnumerable<ChatStreamUpdate>>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
return mock;
|
|
}
|
|
|
|
private static ChannelMessage BuildMessage(string content = "Hello") =>
|
|
new ChannelMessage(
|
|
Guid.NewGuid().ToString(),
|
|
Guid.NewGuid().ToString(),
|
|
"sender-1",
|
|
"Sender Name",
|
|
content,
|
|
DateTimeOffset.UtcNow);
|
|
|
|
[Test]
|
|
public void RegisterChannel_AddsToRegistry_GetChannel_ReturnsIt()
|
|
{
|
|
var sessionManagerMock = new Mock<ISessionManager>();
|
|
var sut = new ChannelManager(sessionManagerMock.Object, loggerMock.Object);
|
|
var mockChannel = BuildChannelMock("ch-reg-1");
|
|
|
|
sut.RegisterChannel(mockChannel.Object);
|
|
var result = sut.GetChannel("ch-reg-1");
|
|
|
|
Assert.That(result, Is.SameAs(mockChannel.Object));
|
|
}
|
|
|
|
[Test]
|
|
public void RegisterChannel_DuplicateId_Throws_InvalidOperationException()
|
|
{
|
|
var sessionManagerMock = new Mock<ISessionManager>();
|
|
var sut = new ChannelManager(sessionManagerMock.Object, loggerMock.Object);
|
|
var mockChannel = BuildChannelMock("ch-dup-1");
|
|
|
|
sut.RegisterChannel(mockChannel.Object);
|
|
|
|
Assert.That(
|
|
() => sut.RegisterChannel(mockChannel.Object),
|
|
Throws.TypeOf<InvalidOperationException>());
|
|
}
|
|
|
|
[Test]
|
|
public void UnregisterChannel_RemovesChannel_GetChannel_ReturnsNull()
|
|
{
|
|
var sessionManagerMock = new Mock<ISessionManager>();
|
|
var sut = new ChannelManager(sessionManagerMock.Object, loggerMock.Object);
|
|
var mockChannel = BuildChannelMock("ch-rem-1");
|
|
|
|
sut.RegisterChannel(mockChannel.Object);
|
|
sut.UnregisterChannel("ch-rem-1");
|
|
var result = sut.GetChannel("ch-rem-1");
|
|
|
|
Assert.That(result, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void UnregisterChannel_UnknownId_NoOp()
|
|
{
|
|
var sessionManagerMock = new Mock<ISessionManager>();
|
|
var sut = new ChannelManager(sessionManagerMock.Object, loggerMock.Object);
|
|
|
|
Assert.That(() => sut.UnregisterChannel("does-not-exist"), Throws.Nothing);
|
|
}
|
|
|
|
[Test]
|
|
public void GetAllChannels_ReturnsAllRegistered()
|
|
{
|
|
var sessionManagerMock = new Mock<ISessionManager>();
|
|
var sut = new ChannelManager(sessionManagerMock.Object, loggerMock.Object);
|
|
|
|
for (var i = 0; i < 3; i++)
|
|
sut.RegisterChannel(new CliChannelStub { ChannelId = i.ToString() });
|
|
|
|
var all = sut.GetAllChannels();
|
|
|
|
Assert.That(all.Count, Is.EqualTo(3));
|
|
}
|
|
|
|
[Test]
|
|
public async Task MessageReceived_Routes_To_SessionManager()
|
|
{
|
|
var mockSession = new Mock<ISessionManager>();
|
|
mockSession
|
|
.Setup(s => s.RouteMessagesAsync(
|
|
It.IsAny<string>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(AsyncEnumerable.Empty<ChatStreamUpdate>());
|
|
|
|
var sut = new ChannelManager(mockSession.Object, loggerMock.Object);
|
|
var mockChannel = BuildChannelMock("ch-route-1");
|
|
|
|
sut.RegisterChannel(mockChannel.Object);
|
|
|
|
var message = BuildMessage("Hello from channel");
|
|
mockChannel.Raise(c => c.MessageReceived += null, mockChannel.Object,
|
|
new ChannelMessageReceivedEventArgs
|
|
{
|
|
Message = message,
|
|
Channel = mockChannel.Object
|
|
});
|
|
|
|
await Task.Delay(100);
|
|
|
|
mockSession.Verify(
|
|
s => s.RouteMessagesAsync(
|
|
message.Content,
|
|
message.ConversationId,
|
|
It.IsAny<string>(),
|
|
It.IsAny<CancellationToken>()),
|
|
Times.Once);
|
|
}
|
|
|
|
[Test]
|
|
public async Task MessageReceived_Pipes_StreamingResponse_To_Channel()
|
|
{
|
|
async IAsyncEnumerable<ChatStreamUpdate> FakeStreamAsync()
|
|
{
|
|
yield return new ChatStreamUpdate(ChatStreamUpdateType.Text, "AI reply", null, false);
|
|
yield return new ChatStreamUpdate(ChatStreamUpdateType.Complete, null, null, true);
|
|
}
|
|
|
|
var mockSession = new Mock<ISessionManager>();
|
|
mockSession
|
|
.Setup(s => s.RouteMessagesAsync(
|
|
It.IsAny<string>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(FakeStreamAsync());
|
|
|
|
var sut = new ChannelManager(mockSession.Object, loggerMock.Object);
|
|
var mockChannel = BuildChannelMock("ch-stream-1");
|
|
|
|
sut.RegisterChannel(mockChannel.Object);
|
|
|
|
await mockChannel.RaiseAsync(c => c.MessageReceived += null, mockChannel.Object,
|
|
new ChannelMessageReceivedEventArgs
|
|
{
|
|
Message = BuildMessage("stream test"),
|
|
Channel = mockChannel.Object
|
|
});
|
|
|
|
await Task.Delay(200);
|
|
|
|
mockChannel.Verify(
|
|
c => c.SendStreamingMessageAsync(
|
|
It.IsAny<IAsyncEnumerable<ChatStreamUpdate>>(),
|
|
It.IsAny<CancellationToken>()),
|
|
Times.Once);
|
|
}
|
|
|
|
[Test]
|
|
public async Task MessageReceived_IsHandled_True_SkipsRouting()
|
|
{
|
|
var mockSession = new Mock<ISessionManager>();
|
|
|
|
var sut = new ChannelManager(mockSession.Object, loggerMock.Object);
|
|
var mockChannel = BuildChannelMock("ch-handled-1");
|
|
|
|
sut.RegisterChannel(mockChannel.Object);
|
|
|
|
await mockChannel.RaiseAsync(c => c.MessageReceived += null, mockChannel.Object,
|
|
new ChannelMessageReceivedEventArgs
|
|
{
|
|
Message = BuildMessage("already handled"),
|
|
Channel = mockChannel.Object,
|
|
IsHandled = true
|
|
});
|
|
|
|
await Task.Delay(100);
|
|
|
|
mockSession.Verify(
|
|
s => s.RouteMessagesAsync(
|
|
It.IsAny<string>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<CancellationToken>()),
|
|
Times.Never);
|
|
}
|
|
|
|
[Test]
|
|
public async Task RouteMessagesAsync_Throws_ChannelNotDisconnected()
|
|
{
|
|
var mockSession = new Mock<ISessionManager>();
|
|
mockSession
|
|
.Setup(s => s.RouteMessagesAsync(
|
|
It.IsAny<string>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Throws(new Exception("Simulated routing failure"));
|
|
|
|
var sut = new ChannelManager(mockSession.Object, loggerMock.Object);
|
|
var mockChannel = BuildChannelMock("ch-err-1");
|
|
mockChannel.Setup(c => c.IsConnected).Returns(true);
|
|
|
|
sut.RegisterChannel(mockChannel.Object);
|
|
|
|
Assert.That(
|
|
async () =>
|
|
{
|
|
await mockChannel.RaiseAsync(c => c.MessageReceived += null, mockChannel.Object,
|
|
new ChannelMessageReceivedEventArgs
|
|
{
|
|
Message = BuildMessage("error trigger"),
|
|
Channel = mockChannel.Object
|
|
});
|
|
await Task.Delay(200);
|
|
},
|
|
Throws.Nothing);
|
|
|
|
Assert.That(mockChannel.Object.IsConnected, Is.True);
|
|
}
|
|
}
|