using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Xunit; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Hub; public sealed class DeleteTaskHubTests : IDisposable { private readonly DbFixture _db = new(); public void Dispose() => _db.Dispose(); private WorkerHub CreateHub() { var factory = _db.CreateFactory(); var built = TaskStateServiceBuilder.Build(factory); var broadcaster = new HubBroadcaster(new CapturingHubContext()); var hub = new WorkerHub( null!, null!, null!, null!, broadcaster, factory, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, built.State, null!, null!, null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(), new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!); hub.Clients = new FakeHubCallerClients(new RecordingClientProxy()); hub.Context = new FakeHubCallerContext(); return hub; } private async Task<(string ParentId, string ChildId)> SeedParentWithChildAsync(TaskStatus childStatus) { using var ctx = _db.CreateContext(); var listId = Guid.NewGuid().ToString(); await new ListRepository(ctx).AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow, }); var repo = new TaskRepository(ctx); var parentId = Guid.NewGuid().ToString(); await repo.AddAsync(new TaskEntity { Id = parentId, ListId = listId, Title = "Parent", Status = TaskStatus.WaitingForChildren, PlanningPhase = PlanningPhase.Finalized, CreatedAt = DateTime.UtcNow, CommitType = "feat", }); var childId = Guid.NewGuid().ToString(); await repo.AddAsync(new TaskEntity { Id = childId, ListId = listId, Title = "Child", ParentTaskId = parentId, Status = childStatus, CreatedAt = DateTime.UtcNow, CommitType = "feat", }); return (parentId, childId); } [Fact] public async Task DeleteTask_LastChildOfWaitingForChildrenParent_AdvancesParentToWaitingForReview() { var (parentId, childId) = await SeedParentWithChildAsync(TaskStatus.Idle); var hub = CreateHub(); await hub.DeleteTask(childId, default); await using var ctx = _db.CreateContext(); Assert.Null(await ctx.Tasks.FindAsync(childId)); var parent = await ctx.Tasks.FindAsync(parentId); Assert.Equal(TaskStatus.WaitingForReview, parent!.Status); } [Fact] public async Task DeleteTask_RunningTask_Throws_AndDoesNotDelete() { var (_, childId) = await SeedParentWithChildAsync(TaskStatus.Running); var hub = CreateHub(); await Assert.ThrowsAsync(() => hub.DeleteTask(childId, default)); await using var ctx = _db.CreateContext(); Assert.NotNull(await ctx.Tasks.FindAsync(childId)); } [Fact] public async Task DeleteTask_TaskWithChildren_ThrowsFriendlyForeignKeyMessage() { var (parentId, _) = await SeedParentWithChildAsync(TaskStatus.Idle); var hub = CreateHub(); var ex = await Assert.ThrowsAsync(() => hub.DeleteTask(parentId, default)); Assert.Contains("child tasks", ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task DeleteTask_Missing_Throws() { var hub = CreateHub(); await Assert.ThrowsAsync(() => hub.DeleteTask("does-not-exist", default)); } }