using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.External; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Lifecycle; using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Tests.Infrastructure; using ClaudeDo.Worker.Tests.Services; using ClaudeDo.Worker.Worktrees; using ClaudeDo.Worker.Config; using Microsoft.Extensions.Logging.Abstractions; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.External; public sealed class AddSubtaskToolTests : IDisposable { private readonly DbFixture _db = new(); private readonly ClaudeDoDbContext _ctx; private readonly TaskRepository _tasks; private readonly ListRepository _lists; public AddSubtaskToolTests() { _ctx = _db.CreateContext(); _tasks = new TaskRepository(_ctx); _lists = new ListRepository(_ctx); } public void Dispose() { _ctx.Dispose(); _db.Dispose(); } private async Task SeedListAsync() { var id = Guid.NewGuid().ToString(); await _lists.AddAsync(new ListEntity { Id = id, Name = "L", CreatedAt = DateTime.UtcNow }); return id; } private async Task SeedTaskAsync(string listId, TaskStatus status = TaskStatus.Idle) { var task = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = listId, Title = "t", Status = status, CreatedAt = DateTime.UtcNow, CommitType = "chore", }; await _tasks.AddAsync(task); return task; } private ExternalMcpService BuildSut() { var cfg = new WorkerConfig { SandboxRoot = Path.Combine(Path.GetTempPath(), $"cd_{Guid.NewGuid():N}"), LogRoot = Path.Combine(Path.GetTempPath(), $"cdl_{Guid.NewGuid():N}"), QueueBackstopIntervalMs = 50, }; var dbFactory = _db.CreateFactory(); var hubCtx = new FakeHubContext(); var broadcaster = new HubBroadcaster(hubCtx); var git = new ClaudeDo.Data.Git.GitService(); var wtManager = new WorktreeManager(git, dbFactory, cfg, NullLogger.Instance); var fake = new FakeClaudeProcess(); var argsBuilder = new ClaudeArgsBuilder(); var state = TaskStateServiceBuilder.Build(dbFactory).State; var runner = new TaskRunner(fake, dbFactory, broadcaster, wtManager, argsBuilder, cfg, NullLogger.Instance, state, new TaskRunTokenRegistry()); var waker = new ClaudeDo.Worker.Queue.QueueWaker(); var picker = new ClaudeDo.Worker.Queue.QueuePicker(dbFactory); var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger.Instance); var queue = new QueueService(dbFactory, runner, cfg, NullLogger.Instance, waker, picker, overrideSlot, state); var maintenance = new WorktreeMaintenanceService(dbFactory, git, NullLogger.Instance); var merge = new TaskMergeService(dbFactory, git, broadcaster, TaskStateServiceBuilder.Build(dbFactory).State, NullLogger.Instance); return new ExternalMcpService( _tasks, _lists, queue, broadcaster, state, git, dbFactory, maintenance, merge); } [Fact] public async Task AddSubtask_appends_row_with_next_order() { var listId = await SeedListAsync(); var task = await SeedTaskAsync(listId, TaskStatus.Idle); var sut = BuildSut(); await sut.AddSubtask(task.Id, "First step", null, CancellationToken.None); await sut.AddSubtask(task.Id, "Second step", null, CancellationToken.None); await using var verifyCtx = _db.CreateContext(); var subtasks = await new SubtaskRepository(verifyCtx).GetByTaskIdAsync(task.Id); Assert.Equal(2, subtasks.Count); Assert.Equal("First step", subtasks[0].Title); Assert.Equal("Second step", subtasks[1].Title); Assert.Equal(0, subtasks[0].OrderNum); Assert.Equal(1, subtasks[1].OrderNum); Assert.False(subtasks[0].Completed); Assert.False(subtasks[1].Completed); } [Fact] public async Task AddSubtask_refuses_running_task() { var listId = await SeedListAsync(); var task = await SeedTaskAsync(listId, TaskStatus.Running); var sut = BuildSut(); await Assert.ThrowsAsync( () => sut.AddSubtask(task.Id, "Should fail", null, CancellationToken.None)); } }