feat(worker): route standalone success to review and resume on re-queue
Standalone tasks now enter WaitingForReview on success; re-queued tasks carrying reviewer feedback resume the prior Claude session with that feedback as the next turn. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -117,12 +117,13 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var dbFactory = _db.CreateFactory();
|
||||
var wtManager = new WorktreeManager(new GitService(), dbFactory, cfg, NullLogger<WorktreeManager>.Instance);
|
||||
var argsBuilder = new ClaudeArgsBuilder();
|
||||
var state = TaskStateServiceBuilder.Build(dbFactory).State;
|
||||
var runner = new TaskRunner(fake, dbFactory, broadcaster, wtManager, argsBuilder, cfg,
|
||||
NullLogger<TaskRunner>.Instance, TaskStateServiceBuilder.Build(dbFactory).State);
|
||||
NullLogger<TaskRunner>.Instance, state);
|
||||
var waker = new ClaudeDo.Worker.Queue.QueueWaker();
|
||||
var picker = new ClaudeDo.Worker.Queue.QueuePicker(dbFactory);
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance);
|
||||
return new QueueService(dbFactory, runner, cfg, NullLogger<QueueService>.Instance, waker, picker, overrideSlot);
|
||||
return new QueueService(dbFactory, runner, cfg, NullLogger<QueueService>.Instance, waker, picker, overrideSlot, state);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -53,12 +53,13 @@ public sealed class QueueServiceSlotGuardTests : IDisposable
|
||||
var dbFactory = _db.CreateFactory();
|
||||
var wtManager = new WorktreeManager(new ClaudeDo.Data.Git.GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance);
|
||||
var argsBuilder = new ClaudeArgsBuilder();
|
||||
var state = TaskStateServiceBuilder.Build(dbFactory).State;
|
||||
var runner = new TaskRunner(fake, dbFactory, broadcaster, wtManager, argsBuilder, _cfg,
|
||||
NullLogger<TaskRunner>.Instance, TaskStateServiceBuilder.Build(dbFactory).State);
|
||||
NullLogger<TaskRunner>.Instance, state);
|
||||
_waker = new QueueWaker();
|
||||
var picker = new QueuePicker(dbFactory);
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance);
|
||||
var service = new QueueService(dbFactory, runner, _cfg, NullLogger<QueueService>.Instance, _waker, picker, overrideSlot);
|
||||
var service = new QueueService(dbFactory, runner, _cfg, NullLogger<QueueService>.Instance, _waker, picker, overrideSlot, state);
|
||||
return (service, fake);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,12 +54,13 @@ public sealed class QueueServiceTests : IDisposable
|
||||
var dbFactory = _db.CreateFactory();
|
||||
var wtManager = new WorktreeManager(new GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance);
|
||||
var argsBuilder = new ClaudeArgsBuilder();
|
||||
var state = TaskStateServiceBuilder.Build(dbFactory).State;
|
||||
var runner = new TaskRunner(fake, dbFactory, broadcaster, wtManager, argsBuilder, _cfg,
|
||||
NullLogger<TaskRunner>.Instance, TaskStateServiceBuilder.Build(dbFactory).State);
|
||||
NullLogger<TaskRunner>.Instance, state);
|
||||
_waker = new QueueWaker();
|
||||
var picker = new QueuePicker(dbFactory);
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance);
|
||||
var service = new QueueService(dbFactory, runner, _cfg, NullLogger<QueueService>.Instance, _waker, picker, overrideSlot);
|
||||
var service = new QueueService(dbFactory, runner, _cfg, NullLogger<QueueService>.Instance, _waker, picker, overrideSlot, state);
|
||||
return (service, fake);
|
||||
}
|
||||
|
||||
@@ -112,6 +113,58 @@ public sealed class QueueServiceTests : IDisposable
|
||||
await Assert.ThrowsAsync<KeyNotFoundException>(() => service.RunNow("nonexistent"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReQueuedReviewTask_ResumesSession_WithFeedbackPrompt_AndClearsFeedback()
|
||||
{
|
||||
var (listId, _) = await SeedListWithAgentTag();
|
||||
|
||||
string? capturedArgs = null;
|
||||
string? capturedPrompt = null;
|
||||
var done = new TaskCompletionSource();
|
||||
|
||||
var (service, _) = CreateService((prompt, _, args, _, _) =>
|
||||
{
|
||||
capturedPrompt = prompt;
|
||||
capturedArgs = args;
|
||||
done.TrySetResult();
|
||||
return Task.FromResult(new RunResult { ExitCode = 0, SessionId = "sess-2", ResultMarkdown = "ok" });
|
||||
});
|
||||
|
||||
// A task that was reviewed and rejected: Queued + ReviewFeedback, with a prior run carrying a session id.
|
||||
var task = new TaskEntity
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
ListId = listId,
|
||||
Title = "Reviewed task",
|
||||
Status = TaskStatus.Queued,
|
||||
ReviewFeedback = "fix the bug",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
await _taskRepo.AddAsync(task);
|
||||
await new TaskRunRepository(_ctx).AddAsync(new TaskRunEntity
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
TaskId = task.Id,
|
||||
RunNumber = 1,
|
||||
IsRetry = false,
|
||||
Prompt = "original",
|
||||
SessionId = "sess-1",
|
||||
StartedAt = DateTime.UtcNow.AddMinutes(-1),
|
||||
});
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
await service.StartAsync(cts.Token);
|
||||
_waker.Wake();
|
||||
await done.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
cts.Cancel();
|
||||
|
||||
Assert.Contains("--resume sess-1", capturedArgs);
|
||||
Assert.Equal("fix the bug", capturedPrompt);
|
||||
|
||||
var reloaded = await _taskRepo.GetByIdAsync(task.Id);
|
||||
Assert.Null(reloaded!.ReviewFeedback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Schedule_Filter_Skips_Future_Tasks()
|
||||
{
|
||||
@@ -254,7 +307,8 @@ public sealed class QueueServiceTests : IDisposable
|
||||
|
||||
var finalTask = await _taskRepo.GetByIdAsync(task.Id);
|
||||
Assert.NotNull(finalTask);
|
||||
Assert.Equal(TaskStatus.Done, finalTask.Status);
|
||||
// A standalone task that completes successfully now gates on review.
|
||||
Assert.Equal(TaskStatus.WaitingForReview, finalTask.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user