using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Online; using ClaudeDo.Worker.Tests.Infrastructure; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Online; public sealed class OnlineBacklogTests : IDisposable { private readonly DbFixture _db = new(); private readonly ClaudeDoDbContext _ctx; private readonly TaskRepository _tasks; private readonly ListRepository _lists; private string _listId = null!; public OnlineBacklogTests() { _ctx = _db.CreateContext(); _tasks = new TaskRepository(_ctx); _lists = new ListRepository(_ctx); } public void Dispose() { _ctx.Dispose(); _db.Dispose(); } private async Task SeedListAsync() { _listId = Guid.NewGuid().ToString(); await _lists.AddAsync(new ListEntity { Id = _listId, Name = "Test", CreatedAt = DateTime.UtcNow }); } private TaskEntity Make( TaskStatus status = TaskStatus.Idle, string? parentId = null, PlanningPhase planning = PlanningPhase.None, string? blockedById = null) => new() { Id = Guid.NewGuid().ToString(), ListId = _listId, Title = "T", Status = status, ParentTaskId = parentId, PlanningPhase = planning, BlockedByTaskId = blockedById, CreatedAt = DateTime.UtcNow, }; [Fact] public async Task CurrentAsync_Returns_OnlyIdleBacklogItems() { await SeedListAsync(); var idle = Make(TaskStatus.Idle); var queued = Make(TaskStatus.Queued); var running = Make(TaskStatus.Running); var done = Make(TaskStatus.Done); var failed = Make(TaskStatus.Failed); // Idle but with parent → planning child var parent = Make(TaskStatus.Idle); await _tasks.AddAsync(parent); var child = Make(TaskStatus.Idle, parentId: parent.Id); // Idle but with PlanningPhase var planningParent = Make(TaskStatus.Idle, planning: PlanningPhase.Active); // Idle but blocked await _tasks.AddAsync(idle); var blocker = Make(TaskStatus.Idle); await _tasks.AddAsync(blocker); var blocked = Make(TaskStatus.Idle, blockedById: blocker.Id); await _tasks.AddAsync(queued); await _tasks.AddAsync(running); await _tasks.AddAsync(done); await _tasks.AddAsync(failed); await _tasks.AddAsync(child); await _tasks.AddAsync(planningParent); await _tasks.AddAsync(blocked); var mirror = await OnlineBacklog.CurrentAsync(_tasks); // Only the plain idle task and the blocker (which itself is plain idle) should appear var ids = mirror.Select(m => m.Id).ToHashSet(); Assert.Contains(idle.Id, ids); Assert.Contains(blocker.Id, ids); Assert.Contains(parent.Id, ids); // parent with no parent itself is a backlog item Assert.DoesNotContain(queued.Id, ids); Assert.DoesNotContain(running.Id, ids); Assert.DoesNotContain(done.Id, ids); Assert.DoesNotContain(failed.Id, ids); Assert.DoesNotContain(child.Id, ids); Assert.DoesNotContain(planningParent.Id, ids); Assert.DoesNotContain(blocked.Id, ids); } [Fact] public void IsBacklogItem_Predicate_FiltersCorrectly() { Assert.True(OnlineBacklog.IsBacklogItem(new TaskEntity { Id = "1", ListId = "l", Title = "T", Status = TaskStatus.Idle, ParentTaskId = null, PlanningPhase = PlanningPhase.None, BlockedByTaskId = null, CreatedAt = DateTime.UtcNow, })); Assert.False(OnlineBacklog.IsBacklogItem(new TaskEntity { Id = "2", ListId = "l", Title = "T", Status = TaskStatus.Queued, ParentTaskId = null, PlanningPhase = PlanningPhase.None, BlockedByTaskId = null, CreatedAt = DateTime.UtcNow, })); Assert.False(OnlineBacklog.IsBacklogItem(new TaskEntity { Id = "3", ListId = "l", Title = "T", Status = TaskStatus.Idle, ParentTaskId = "p", PlanningPhase = PlanningPhase.None, BlockedByTaskId = null, CreatedAt = DateTime.UtcNow, })); Assert.False(OnlineBacklog.IsBacklogItem(new TaskEntity { Id = "4", ListId = "l", Title = "T", Status = TaskStatus.Idle, ParentTaskId = null, PlanningPhase = PlanningPhase.Active, BlockedByTaskId = null, CreatedAt = DateTime.UtcNow, })); Assert.False(OnlineBacklog.IsBacklogItem(new TaskEntity { Id = "5", ListId = "l", Title = "T", Status = TaskStatus.Idle, ParentTaskId = null, PlanningPhase = PlanningPhase.None, BlockedByTaskId = "b", CreatedAt = DateTime.UtcNow, })); } [Fact] public async Task CurrentAsync_EmptyDb_ReturnsEmpty() { await SeedListAsync(); var mirror = await OnlineBacklog.CurrentAsync(_tasks); Assert.Empty(mirror); } [Fact] public async Task CurrentAsync_MapsFieldsCorrectly() { await SeedListAsync(); var task = Make(TaskStatus.Idle); task.Description = "desc"; await _tasks.AddAsync(task); var mirror = await OnlineBacklog.CurrentAsync(_tasks); Assert.Single(mirror); Assert.Equal(task.Id, mirror[0].Id); Assert.Equal(_listId, mirror[0].ListId); Assert.Equal("T", mirror[0].Title); Assert.Equal("desc", mirror[0].Description); } }