using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Tests.Infrastructure; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Repositories; public sealed class TaskRepositoryPlanningTests : IDisposable { private readonly DbFixture _db = new(); private readonly ClaudeDoDbContext _ctx; private readonly TaskRepository _tasks; private readonly ListRepository _lists; private readonly TagRepository _tags; public TaskRepositoryPlanningTests() { _ctx = _db.CreateContext(); _tasks = new TaskRepository(_ctx); _lists = new ListRepository(_ctx); _tags = new TagRepository(_ctx); } public void Dispose() { _ctx.Dispose(); _db.Dispose(); } private async Task CreateListAsync(string? id = null) { var listId = id ?? Guid.NewGuid().ToString(); await _lists.AddAsync(new ListEntity { Id = listId, Name = "Test List", CreatedAt = DateTime.UtcNow, }); return listId; } private TaskEntity MakeTask(string listId, TaskStatus status = TaskStatus.Manual, string? parentId = null) => new() { Id = Guid.NewGuid().ToString(), ListId = listId, Title = "t", Status = status, CreatedAt = DateTime.UtcNow, CommitType = "feat", ParentTaskId = parentId, }; [Fact] public async Task GetChildrenAsync_ReturnsOnlyDirectChildren_Sorted() { var listId = await CreateListAsync(); var parent = MakeTask(listId, TaskStatus.Planning); parent.Title = "parent"; await _tasks.AddAsync(parent); var childA = MakeTask(listId, TaskStatus.Draft, parentId: parent.Id); childA.Title = "a"; await _tasks.AddAsync(childA); childA.SortOrder = 1; await _tasks.UpdateAsync(childA); var childB = MakeTask(listId, TaskStatus.Draft, parentId: parent.Id); childB.Title = "b"; await _tasks.AddAsync(childB); childB.SortOrder = 0; await _tasks.UpdateAsync(childB); var unrelated = MakeTask(listId, TaskStatus.Manual); await _tasks.AddAsync(unrelated); var children = await _tasks.GetChildrenAsync(parent.Id); Assert.Equal(2, children.Count); Assert.Equal("b", children[0].Title); Assert.Equal("a", children[1].Title); } [Fact] public async Task CreateChildAsync_CreatesDraftUnderParent() { var listId = await CreateListAsync(); var parent = MakeTask(listId, TaskStatus.Planning); await _tasks.AddAsync(parent); var child = await _tasks.CreateChildAsync( parent.Id, title: "child title", description: "child desc", tagNames: new[] { "agent" }, commitType: "feat"); Assert.Equal(TaskStatus.Draft, child.Status); Assert.Equal(parent.Id, child.ParentTaskId); Assert.Equal(listId, child.ListId); Assert.Equal("child title", child.Title); Assert.Equal("child desc", child.Description); Assert.Equal("feat", child.CommitType); var loaded = await _tasks.GetByIdAsync(child.Id); Assert.NotNull(loaded); Assert.Equal(TaskStatus.Draft, loaded!.Status); var tags = await _tasks.GetTagsAsync(child.Id); Assert.Contains(tags, t => t.Name == "agent"); } [Fact] public async Task CreateChildAsync_ThrowsIfParentNotFound() { var listId = await CreateListAsync(); _ = listId; // just to create the DB await Assert.ThrowsAsync(() => _tasks.CreateChildAsync("nonexistent-parent-id", "t", null, null, null)); } [Fact] public async Task SetPlanningStartedAsync_ManualTask_TransitionsToPlanning() { var listId = await CreateListAsync(); var task = MakeTask(listId, TaskStatus.Manual); await _tasks.AddAsync(task); var result = await _tasks.SetPlanningStartedAsync(task.Id, "tok-abc"); Assert.NotNull(result); Assert.Equal(TaskStatus.Planning, result!.Status); Assert.Equal("tok-abc", result.PlanningSessionToken); var loaded = await _tasks.GetByIdAsync(task.Id); Assert.Equal(TaskStatus.Planning, loaded!.Status); Assert.Equal("tok-abc", loaded.PlanningSessionToken); } [Fact] public async Task SetPlanningStartedAsync_NonManualTask_ReturnsNull() { var listId = await CreateListAsync(); var task = MakeTask(listId, TaskStatus.Queued); await _tasks.AddAsync(task); var result = await _tasks.SetPlanningStartedAsync(task.Id, "tok-xyz"); Assert.Null(result); var loaded = await _tasks.GetByIdAsync(task.Id); Assert.Equal(TaskStatus.Queued, loaded!.Status); Assert.Null(loaded.PlanningSessionToken); } [Fact] public async Task UpdatePlanningSessionIdAsync_StoresClaudeSessionId() { var listId = await CreateListAsync(); var task = MakeTask(listId, TaskStatus.Manual); await _tasks.AddAsync(task); await _tasks.SetPlanningStartedAsync(task.Id, "tok"); await _tasks.UpdatePlanningSessionIdAsync(task.Id, "claude-session-42"); var loaded = await _tasks.GetByIdAsync(task.Id); Assert.Equal("claude-session-42", loaded!.PlanningSessionId); } [Fact] public async Task FindByPlanningTokenAsync_ReturnsTask_WhenTokenMatches() { var listId = await CreateListAsync(); var task = MakeTask(listId, TaskStatus.Manual); await _tasks.AddAsync(task); await _tasks.SetPlanningStartedAsync(task.Id, "unique-token-123"); var found = await _tasks.FindByPlanningTokenAsync("unique-token-123"); Assert.NotNull(found); Assert.Equal(task.Id, found!.Id); } [Fact] public async Task FindByPlanningTokenAsync_ReturnsNull_WhenTokenUnknown() { var found = await _tasks.FindByPlanningTokenAsync("no-such-token"); Assert.Null(found); } }