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 HandoffMcpToolsTests : IDisposable { private readonly DbFixture _db = new(); private readonly ClaudeDoDbContext _ctx; private readonly TaskRepository _tasks; private readonly ListRepository _lists; private readonly CapturingHubContext _hubContext = new(); public HandoffMcpToolsTests() { _ctx = _db.CreateContext(); _tasks = new TaskRepository(_ctx); _lists = new ListRepository(_ctx); } public void Dispose() { _ctx.Dispose(); _db.Dispose(); } private HandoffMcpTools BuildSut() => new(_tasks, new HubBroadcaster(_hubContext)); 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; } private async Task SeedListAsync() { var listId = Guid.NewGuid().ToString(); await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow }); return listId; } [Fact] public async Task HandoffListHandler_ValidIds_BroadcastsAndReturnsCount() { var listId = await SeedListAsync(); var handlerTask = await SeedTaskAsync(listId, title: "List handler: Alpha"); var survivor = await SeedTaskAsync(listId, TaskStatus.WaitingForReview, title: "Survivor"); var sut = BuildSut(); var result = await sut.HandoffListHandler(handlerTask.Id, new[] { survivor.Id }, cancellationToken: CancellationToken.None); Assert.True(result.Requested); Assert.Equal(handlerTask.Id, result.TaskId); Assert.Equal(1, result.SurvivingCount); Assert.Equal("wait", result.NextPhase); var call = Assert.Single(_hubContext.Proxy.Calls); Assert.Equal("HandoffRequested", call.Method); Assert.Equal(handlerTask.Id, call.Args[0]); Assert.Equal("wait", call.Args[2]); } [Fact] public async Task HandoffListHandler_NextPhaseDefaultsToWait() { var listId = await SeedListAsync(); var handlerTask = await SeedTaskAsync(listId); var survivor = await SeedTaskAsync(listId, TaskStatus.WaitingForReview, title: "Survivor"); var sut = BuildSut(); var result = await sut.HandoffListHandler(handlerTask.Id, new[] { survivor.Id }, cancellationToken: CancellationToken.None); Assert.Equal("wait", result.NextPhase); } [Theory] [InlineData("wait")] [InlineData("merge")] [InlineData("wait_final")] [InlineData("merge_final")] public async Task HandoffListHandler_AcceptsEveryKnownPhase(string phase) { var listId = await SeedListAsync(); var handlerTask = await SeedTaskAsync(listId); var survivor = await SeedTaskAsync(listId, TaskStatus.WaitingForReview, title: "Survivor"); var sut = BuildSut(); var result = await sut.HandoffListHandler(handlerTask.Id, new[] { survivor.Id }, phase, CancellationToken.None); Assert.Equal(phase, result.NextPhase); } [Fact] public async Task HandoffListHandler_UnknownPhase_Throws() { var listId = await SeedListAsync(); var handlerTask = await SeedTaskAsync(listId); var survivor = await SeedTaskAsync(listId, TaskStatus.WaitingForReview, title: "Survivor"); var sut = BuildSut(); var ex = await Assert.ThrowsAsync(() => sut.HandoffListHandler(handlerTask.Id, new[] { survivor.Id }, "phase-9", CancellationToken.None)); Assert.Contains("phase-9", ex.Message); } [Fact] public async Task HandoffListHandler_EmptySurvivingIds_Throws() { var listId = await SeedListAsync(); var handlerTask = await SeedTaskAsync(listId); var sut = BuildSut(); await Assert.ThrowsAsync(() => sut.HandoffListHandler(handlerTask.Id, Array.Empty(), cancellationToken: CancellationToken.None)); } [Fact] public async Task HandoffListHandler_UnknownHandlerTask_Throws() { var sut = BuildSut(); await Assert.ThrowsAsync(() => sut.HandoffListHandler("missing", new[] { "x" }, cancellationToken: CancellationToken.None)); } [Fact] public async Task HandoffListHandler_UnknownSurvivingTask_Throws() { var listId = await SeedListAsync(); var handlerTask = await SeedTaskAsync(listId); var sut = BuildSut(); await Assert.ThrowsAsync(() => sut.HandoffListHandler(handlerTask.Id, new[] { "missing" }, cancellationToken: CancellationToken.None)); } }