using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Refine; using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.Extensions.Logging.Abstractions; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Refine; public sealed class RefineRunnerTests : IDisposable { private readonly DbFixture _db = new(); private readonly ClaudeDoDbContext _ctx; public RefineRunnerTests() { _ctx = _db.CreateContext(); } public void Dispose() { _ctx.Dispose(); _db.Dispose(); } private async Task SeedListAsync() { var listId = Guid.NewGuid().ToString(); await new ListRepository(_ctx).AddAsync(new ListEntity { Id = listId, Name = "Test", CreatedAt = DateTime.UtcNow, WorkingDir = null, }); return listId; } private async Task SeedTaskAsync(string listId, TaskStatus status) { var task = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = listId, Title = "Test task", Status = status, CreatedAt = DateTime.UtcNow, }; await new TaskRepository(_ctx).AddAsync(task); return task; } private RefineRunner BuildRunner(RecordingClaudeProcess claude, RecordingRefineBroadcaster broadcaster) { return new RefineRunner( claude, _db.CreateFactory(), NullLogger.Instance, broadcaster); } [Fact] public async Task Refuses_when_task_not_idle() { var listId = await SeedListAsync(); var task = await SeedTaskAsync(listId, TaskStatus.Queued); var claude = new RecordingClaudeProcess(success: true); var broadcaster = new RecordingRefineBroadcaster(); var runner = BuildRunner(claude, broadcaster); var outcome = await runner.RefineAsync(task.Id, CancellationToken.None); Assert.False(outcome.Success); Assert.Equal(0, claude.CallCount); } [Fact] public async Task Idle_task_invokes_claude_once_and_brackets_with_events() { var listId = await SeedListAsync(); var task = await SeedTaskAsync(listId, TaskStatus.Idle); var claude = new RecordingClaudeProcess(success: true); var broadcaster = new RecordingRefineBroadcaster(); var runner = BuildRunner(claude, broadcaster); var outcome = await runner.RefineAsync(task.Id, CancellationToken.None); Assert.True(outcome.Success); Assert.Equal(1, claude.CallCount); Assert.Equal(1, broadcaster.StartedCount); Assert.Equal(1, broadcaster.FinishedCount); } } internal sealed class RecordingClaudeProcess : IClaudeProcess { private readonly bool _success; private int _callCount; public int CallCount => _callCount; public RecordingClaudeProcess(bool success) => _success = success; public Task RunAsync(IReadOnlyList arguments, string prompt, string workingDirectory, Func onStdoutLine, CancellationToken ct) { Interlocked.Increment(ref _callCount); var result = _success ? new RunResult { ExitCode = 0, ResultMarkdown = "ok" } : new RunResult { ExitCode = 1, ResultMarkdown = null }; return Task.FromResult(result); } } internal sealed class RecordingRefineBroadcaster : IRefineBroadcaster { private int _startedCount; private int _finishedCount; public int StartedCount => _startedCount; public int FinishedCount => _finishedCount; public Task RefineStartedAsync(string taskId) { Interlocked.Increment(ref _startedCount); return Task.CompletedTask; } public Task RefineFinishedAsync(string taskId, bool success, string? error) { Interlocked.Increment(ref _finishedCount); return Task.CompletedTask; } }