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> loggerMock = new(); private static Mock BuildChannelMock(string id) { var mock = new Mock(); 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>(), It.IsAny())) .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(); 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(); 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()); } [Test] public void UnregisterChannel_RemovesChannel_GetChannel_ReturnsNull() { var sessionManagerMock = new Mock(); 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(); 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(); 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(); mockSession .Setup(s => s.RouteMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(AsyncEnumerable.Empty()); 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(), It.IsAny()), Times.Once); } [Test] public async Task MessageReceived_Pipes_StreamingResponse_To_Channel() { async IAsyncEnumerable FakeStreamAsync() { yield return new ChatStreamUpdate(ChatStreamUpdateType.Text, "AI reply", null, false); yield return new ChatStreamUpdate(ChatStreamUpdateType.Complete, null, null, true); } var mockSession = new Mock(); mockSession .Setup(s => s.RouteMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .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>(), It.IsAny()), Times.Once); } [Test] public async Task MessageReceived_IsHandled_True_SkipsRouting() { var mockSession = new Mock(); 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(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task RouteMessagesAsync_Throws_ChannelNotDisconnected() { var mockSession = new Mock(); mockSession .Setup(s => s.RouteMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .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); } }