From 74255ddc82e010d56b5d1ff3c850dfe3f8af1ece Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 23 Apr 2026 17:54:43 +0200 Subject: [PATCH] feat(data): TaskRepository.CreateChildAsync --- .../Repositories/TaskRepository.cs | 50 +++++++++++++++++++ .../TaskRepositoryPlanningTests.cs | 39 +++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/src/ClaudeDo.Data/Repositories/TaskRepository.cs b/src/ClaudeDo.Data/Repositories/TaskRepository.cs index 4fe24b4..98862b8 100644 --- a/src/ClaudeDo.Data/Repositories/TaskRepository.cs +++ b/src/ClaudeDo.Data/Repositories/TaskRepository.cs @@ -217,6 +217,56 @@ public sealed class TaskRepository .ToListAsync(ct); } + public async Task CreateChildAsync( + string parentId, + string title, + string? description, + IReadOnlyList? tagNames, + string? commitType, + CancellationToken ct = default) + { + var parent = await _context.Tasks.FirstOrDefaultAsync(t => t.Id == parentId, ct); + if (parent is null) + throw new InvalidOperationException($"Parent task {parentId} not found."); + + var maxSort = await _context.Tasks + .Where(t => t.ListId == parent.ListId) + .Select(t => (int?)t.SortOrder) + .MaxAsync(ct); + + var child = new TaskEntity + { + Id = Guid.NewGuid().ToString(), + ListId = parent.ListId, + Title = title, + Description = description, + Status = TaskStatus.Draft, + CreatedAt = DateTime.UtcNow, + CommitType = string.IsNullOrEmpty(commitType) ? parent.CommitType : commitType, + ParentTaskId = parentId, + SortOrder = (maxSort ?? -1) + 1, + }; + _context.Tasks.Add(child); + + if (tagNames is not null && tagNames.Count > 0) + { + foreach (var tagName in tagNames.Distinct(StringComparer.OrdinalIgnoreCase)) + { + var tag = await _context.Tags.FirstOrDefaultAsync(t => t.Name == tagName, ct); + if (tag is null) + { + tag = new TagEntity { Name = tagName }; + _context.Tags.Add(tag); + await _context.SaveChangesAsync(ct); + } + child.Tags.Add(tag); + } + } + + await _context.SaveChangesAsync(ct); + return child; + } + #endregion #region Queue selection diff --git a/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs index 7773957..4f29c07 100644 --- a/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs @@ -80,4 +80,43 @@ public sealed class TaskRepositoryPlanningTests : IDisposable 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)); + } }