using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.External; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Tests.Infrastructure; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.External; public sealed class ListHandlerMcpToolsTests : IDisposable { private readonly DbFixture _db = new(); private readonly ClaudeDoDbContext _ctx; private readonly TaskRepository _tasks; private readonly ListRepository _lists; private readonly CapturingHubContext _hubContext = new(); public ListHandlerMcpToolsTests() { _ctx = _db.CreateContext(); _tasks = new TaskRepository(_ctx); _lists = new ListRepository(_ctx); } public void Dispose() { _ctx.Dispose(); _db.Dispose(); } private ListHandlerMcpTools BuildSut(ConnectedClientTracker tracker) => new(_lists, _tasks, new HubBroadcaster(_hubContext), tracker); private async Task SeedListAsync() { var listId = Guid.NewGuid().ToString(); await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow }); return listId; } private async Task SeedTaskAsync(string listId, TaskStatus status = TaskStatus.Idle, string title = "t") { var task = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = listId, Title = title, Status = status, CreatedAt = DateTime.UtcNow, CommitType = "chore", }; await _tasks.AddAsync(task); return task; } [Fact] public async Task StartListHandler_NoConnectedClient_DoesNotBroadcastAndReportsNotRequested() { var listId = await SeedListAsync(); await SeedTaskAsync(listId); var tracker = new ConnectedClientTracker(); var result = await BuildSut(tracker).StartListHandler(listId, cancellationToken: CancellationToken.None); Assert.False(result.Requested); Assert.False(string.IsNullOrWhiteSpace(result.Reason)); Assert.Equal(1, result.TaskCount); Assert.Empty(_hubContext.Proxy.Calls); } [Fact] public async Task StartListHandler_ConnectedClient_BroadcastsAndReportsRequested() { var listId = await SeedListAsync(); var task = await SeedTaskAsync(listId); var tracker = new ConnectedClientTracker(); tracker.Increment(); var result = await BuildSut(tracker).StartListHandler(listId, cancellationToken: CancellationToken.None); Assert.True(result.Requested); Assert.Null(result.Reason); Assert.Equal(1, result.TaskCount); Assert.Equal(new[] { task.Number }, result.TaskNumbers); var call = Assert.Single(_hubContext.Proxy.Calls); Assert.Equal("ListHandlerStartRequested", call.Method); Assert.Equal(listId, call.Args[0]); } [Fact] public async Task StartListHandler_LastClientDisconnects_StopsReportingRequested() { var listId = await SeedListAsync(); await SeedTaskAsync(listId); var tracker = new ConnectedClientTracker(); tracker.Increment(); tracker.Decrement(); var result = await BuildSut(tracker).StartListHandler(listId, cancellationToken: CancellationToken.None); Assert.False(result.Requested); Assert.Empty(_hubContext.Proxy.Calls); } [Fact] public async Task StartListHandler_NoTasksToHandle_ThrowsRegardlessOfConnection() { var listId = await SeedListAsync(); var tracker = new ConnectedClientTracker(); tracker.Increment(); await Assert.ThrowsAsync(() => BuildSut(tracker).StartListHandler(listId, cancellationToken: CancellationToken.None)); } }