using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Planning; using ClaudeDo.Worker.Tests.Infrastructure; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Planning; public sealed class PlanningMcpServiceTests : IDisposable { private readonly DbFixture _db = new(); private readonly ClaudeDoDbContext _ctx; private readonly TaskRepository _tasks; private readonly ListRepository _lists; private readonly PlanningMcpService _sut; public PlanningMcpServiceTests() { _ctx = _db.CreateContext(); _tasks = new TaskRepository(_ctx); _lists = new ListRepository(_ctx); _sut = new PlanningMcpService(_tasks); } public void Dispose() { _ctx.Dispose(); _db.Dispose(); } private async Task SeedPlanningParentAsync() { var listId = Guid.NewGuid().ToString(); await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow }); var parent = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = listId, Title = "p", Status = TaskStatus.Manual, CreatedAt = DateTime.UtcNow, CommitType = "chore", }; await _tasks.AddAsync(parent); await _tasks.SetPlanningStartedAsync(parent.Id, "tok"); return (await _tasks.GetByIdAsync(parent.Id))!; } private static PlanningMcpContext Ctx(string parentId) => new() { ParentTaskId = parentId }; [Fact] public async Task CreateChildTask_CreatesDraft() { var parent = await SeedPlanningParentAsync(); var result = await _sut.CreateChildTask(Ctx(parent.Id), "My child", "desc", null, null, CancellationToken.None); Assert.Equal("Draft", result.Status); var child = await _tasks.GetByIdAsync(result.TaskId); Assert.Equal("My child", child!.Title); Assert.Equal(TaskStatus.Draft, child.Status); } [Fact] public async Task ListChildTasks_ReturnsOnlyThisParentsChildren() { var parent = await SeedPlanningParentAsync(); var other = await SeedPlanningParentAsync(); await _tasks.CreateChildAsync(parent.Id, "mine", null, null, null); await _tasks.CreateChildAsync(other.Id, "theirs", null, null, null); var list = await _sut.ListChildTasks(Ctx(parent.Id), CancellationToken.None); Assert.Single(list); Assert.Equal("mine", list[0].Title); } [Fact] public async Task UpdateChildTask_NotAChild_Throws() { var parent = await SeedPlanningParentAsync(); var other = await SeedPlanningParentAsync(); var otherChild = await _tasks.CreateChildAsync(other.Id, "x", null, null, null); await Assert.ThrowsAsync(() => _sut.UpdateChildTask(Ctx(parent.Id), otherChild.Id, "new", null, null, null, CancellationToken.None)); } [Fact] public async Task UpdateChildTask_NotDraft_Throws() { var parent = await SeedPlanningParentAsync(); var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null); await _tasks.FinalizePlanningAsync(parent.Id, queueAgentTasks: false); await Assert.ThrowsAsync(() => _sut.UpdateChildTask(Ctx(parent.Id), c.Id, "new", null, null, null, CancellationToken.None)); } [Fact] public async Task DeleteChildTask_RemovesDraft() { var parent = await SeedPlanningParentAsync(); var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null); await _sut.DeleteChildTask(Ctx(parent.Id), c.Id, CancellationToken.None); Assert.Null(await _tasks.GetByIdAsync(c.Id)); } [Fact] public async Task UpdatePlanningTask_SetsTitleAndDescription() { var parent = await SeedPlanningParentAsync(); await _sut.UpdatePlanningTask(Ctx(parent.Id), "new title", "new desc", CancellationToken.None); var loaded = await _tasks.GetByIdAsync(parent.Id); Assert.Equal("new title", loaded!.Title); Assert.Equal("new desc", loaded.Description); } [Fact] public async Task Finalize_PromotesDraftsAndInvalidatesToken() { var parent = await SeedPlanningParentAsync(); await _tasks.CreateChildAsync(parent.Id, "c1", null, null, null); await _tasks.CreateChildAsync(parent.Id, "c2", null, null, null); var count = await _sut.Finalize(Ctx(parent.Id), true, CancellationToken.None); Assert.Equal(2, count); var loaded = await _tasks.GetByIdAsync(parent.Id); Assert.Equal(TaskStatus.Planned, loaded!.Status); Assert.Null(loaded.PlanningSessionToken); } }