using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.AspNetCore.SignalR; using Xunit; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Hub; public sealed class WorktreeStateHubTests : IDisposable { private readonly DbFixture _db = new(); public void Dispose() => _db.Dispose(); private WorkerHub CreateHub() { var broadcaster = new HubBroadcaster(new CapturingHubContext()); var hub = new WorkerHub( null!, null!, null!, null!, broadcaster, _db.CreateFactory(), null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore()); hub.Clients = new FakeHubCallerClients(new RecordingClientProxy()); hub.Context = new FakeHubCallerContext(); return hub; } private async Task SeedWorktreeAsync(WorktreeState initial) { using var ctx = _db.CreateContext(); var listId = Guid.NewGuid().ToString(); var taskId = Guid.NewGuid().ToString(); await new ListRepository(ctx).AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow, }); await new TaskRepository(ctx).AddAsync(new TaskEntity { Id = taskId, ListId = listId, Title = "T", Status = TaskStatus.Done, CreatedAt = DateTime.UtcNow, CommitType = "feat", }); await new WorktreeRepository(ctx).AddAsync(new WorktreeEntity { TaskId = taskId, Path = "/tmp/x", BranchName = "claudedo/x", BaseCommit = "deadbeef", State = initial, CreatedAt = DateTime.UtcNow, }); return taskId; } [Fact] public async Task SetWorktreeState_Active_To_Discarded_Succeeds() { var taskId = await SeedWorktreeAsync(WorktreeState.Active); var hub = CreateHub(); var ok = await hub.SetWorktreeState(taskId, WorktreeState.Discarded); Assert.True(ok); using var ctx = _db.CreateContext(); var row = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId); Assert.Equal(WorktreeState.Discarded, row!.State); } [Fact] public async Task SetWorktreeState_Merged_To_Active_Throws() { var taskId = await SeedWorktreeAsync(WorktreeState.Merged); var hub = CreateHub(); await Assert.ThrowsAsync(() => hub.SetWorktreeState(taskId, WorktreeState.Active)); using var ctx = _db.CreateContext(); var row = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId); Assert.Equal(WorktreeState.Merged, row!.State); } [Fact] public async Task SetWorktreeState_Discarded_To_Kept_Throws() { var taskId = await SeedWorktreeAsync(WorktreeState.Discarded); var hub = CreateHub(); await Assert.ThrowsAsync(() => hub.SetWorktreeState(taskId, WorktreeState.Kept)); } [Fact] public async Task SetWorktreeState_SameState_IsNoOp() { var taskId = await SeedWorktreeAsync(WorktreeState.Active); var hub = CreateHub(); var ok = await hub.SetWorktreeState(taskId, WorktreeState.Active); Assert.True(ok); } [Fact] public async Task SetWorktreeState_Missing_Throws() { var hub = CreateHub(); await Assert.ThrowsAsync(() => hub.SetWorktreeState("does-not-exist", WorktreeState.Discarded)); } }