diff --git a/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs b/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs new file mode 100644 index 0000000..a05c3a9 --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs @@ -0,0 +1,101 @@ +using ClaudeDo.Data; +using ClaudeDo.Data.Models; +using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.External; +using ClaudeDo.Worker.Hub; +using ClaudeDo.Worker.Services; +using ClaudeDo.Worker.Tests.Infrastructure; +using Microsoft.AspNetCore.SignalR; +using TaskStatus = ClaudeDo.Data.Models.TaskStatus; + +namespace ClaudeDo.Worker.Tests.External; + +internal sealed class ExternalRecordingHubClients : IHubClients +{ + public ExternalRecordingClientProxy Proxy { get; } = new(); + public IClientProxy All => Proxy; + public IClientProxy AllExcept(IReadOnlyList excludedConnectionIds) => Proxy; + public IClientProxy Client(string connectionId) => Proxy; + public IClientProxy Clients(IReadOnlyList connectionIds) => Proxy; + public IClientProxy Group(string groupName) => Proxy; + public IClientProxy GroupExcept(string groupName, IReadOnlyList excludedConnectionIds) => Proxy; + public IClientProxy Groups(IReadOnlyList groupNames) => Proxy; + public IClientProxy User(string userId) => Proxy; + public IClientProxy Users(IReadOnlyList userIds) => Proxy; +} + +internal sealed class ExternalRecordingClientProxy : IClientProxy +{ + public List<(string Method, object?[] Args)> Calls { get; } = new(); + public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + { + Calls.Add((method, args)); + return Task.CompletedTask; + } +} + +internal sealed class ExternalFakeHubContext : IHubContext +{ + public ExternalRecordingHubClients RecordingClients { get; } = new(); + public IHubClients Clients => RecordingClients; + public IGroupManager Groups => throw new NotImplementedException(); +} + +public sealed class ExternalMcpServiceTests : IDisposable +{ + private readonly DbFixture _db = new(); + private readonly ClaudeDoDbContext _ctx; + private readonly TaskRepository _tasks; + private readonly ListRepository _lists; + private readonly TagRepository _tags; + private readonly ExternalFakeHubContext _hub = new(); + private readonly HubBroadcaster _broadcaster; + + public ExternalMcpServiceTests() + { + _ctx = _db.CreateContext(); + _tasks = new TaskRepository(_ctx); + _lists = new ListRepository(_ctx); + _tags = new TagRepository(_ctx); + _broadcaster = new HubBroadcaster(_hub); + } + + public void Dispose() { _ctx.Dispose(); _db.Dispose(); } + + private async Task SeedListAsync(string name = "L") + { + var id = Guid.NewGuid().ToString(); + await _lists.AddAsync(new ListEntity { Id = id, Name = name, CreatedAt = DateTime.UtcNow }); + return id; + } + + private async Task SeedTaskAsync(string listId, string title = "t", TaskStatus status = TaskStatus.Manual) + { + 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; + } + + // QueueService is needed by ExternalMcpService's constructor. For tests that + // only exercise UpdateTask / DeleteTask / SetTaskTags / ListTags / ListTags, + // we never call its WakeQueue/RunNow/CancelTask paths, so a real QueueService + // built with the same approach used in QueueServiceTests is sufficient. + private ExternalMcpService BuildSut(QueueService queue) => + new(_tasks, _lists, queue, _broadcaster, _tags); + + [Fact] + public async Task SeededListAndTask_AreRetrievable() + { + var listId = await SeedListAsync(); + var task = await SeedTaskAsync(listId); + Assert.NotNull(await _tasks.GetByIdAsync(task.Id)); + } +}