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.Runner; using ClaudeDo.Worker.State; using ClaudeDo.Worker.Tests.Infrastructure; using ClaudeDo.Data.Git; using ClaudeDo.Worker.Config; using Microsoft.Extensions.Logging.Abstractions; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.External; public sealed class LifecycleMcpToolsTests : IDisposable { private readonly DbFixture _db = new(); private readonly ClaudeDoDbContext _ctx; private readonly TaskRepository _tasks; private readonly ListRepository _lists; public LifecycleMcpToolsTests() { _ctx = _db.CreateContext(); _tasks = new TaskRepository(_ctx); _lists = new ListRepository(_ctx); } public void Dispose() { _ctx.Dispose(); _db.Dispose(); } private LifecycleMcpTools BuildSut() { var cfg = new WorkerConfig { SandboxRoot = Path.Combine(Path.GetTempPath(), $"cd_{Guid.NewGuid():N}"), LogRoot = Path.Combine(Path.GetTempPath(), $"cdl_{Guid.NewGuid():N}"), }; var dbFactory = _db.CreateFactory(); var broadcaster = new HubBroadcaster(new CapturingHubContext()); var wtManager = new WorktreeManager(new GitService(), dbFactory, cfg, NullLogger.Instance); var state = TaskStateServiceBuilder.Build(dbFactory).State; var reset = new TaskResetService(dbFactory, wtManager, broadcaster, state, NullLogger.Instance); return new LifecycleMcpTools(_tasks, reset); } private async Task SeedTaskAsync(TaskStatus status) { var listId = Guid.NewGuid().ToString(); await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow }); 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; } [Fact] public async Task ResetFailedTask_OnFailed_ResetsToIdle() { var task = await SeedTaskAsync(TaskStatus.Failed); var sut = BuildSut(); await sut.ResetFailedTask(task.Id, CancellationToken.None); var loaded = await _tasks.GetByIdAsync(task.Id); Assert.Equal(TaskStatus.Idle, loaded!.Status); } [Fact] public async Task ResetFailedTask_OnNonFailed_Throws() { var task = await SeedTaskAsync(TaskStatus.Done); var sut = BuildSut(); await Assert.ThrowsAsync(() => sut.ResetFailedTask(task.Id, CancellationToken.None)); } [Fact] public async Task ResetFailedTask_NotFound_Throws() { var sut = BuildSut(); await Assert.ThrowsAsync(() => sut.ResetFailedTask("missing", CancellationToken.None)); } }