Files
ClaudeDo/tests/ClaudeDo.Worker.Tests/Infrastructure/TaskStateServiceBuilder.cs
T
mika kuns 774f9d3d13 fix(worker): claim Running before creating run resources
TaskRunner.RunAsync created the worktree (PrepareRunDirectoryAsync)
before claiming Running via StartRunningAsync. RunNow dispatches by
task id with no atomic claim of their own, so a Queued task racing
the queue picker's atomic SQL claim could hit WorktreeManager's
branch-collision self-heal, which force-removes and recreates the
winner's live worktree mid-run.

Move the claim before any resource creation and bail out immediately
when it's rejected. OverrideSlotService.RunNow also fast-rejects a
task already Running in the DB (defense in depth). RunCancellationRegistry
now refuses (and logs) a double registration instead of silently
overwriting the first CTS, so a losing dispatch's cleanup can no
longer unregister the winner's cancellation token.
2026-08-06 13:33:23 +02:00

50 lines
1.6 KiB
C#

using ClaudeDo.Data;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Planning;
using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.State;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
namespace ClaudeDo.Worker.Tests.Infrastructure;
/// Test-only helper that wires TaskStateService and PlanningChainCoordinator
/// against a shared DB factory, breaking the Func cycle between them.
public static class TaskStateServiceBuilder
{
public sealed record Built(
TaskStateService State,
PlanningChainCoordinator Chain,
CapturingHubContext Hub,
Func<int> WakeCount,
CountingQueueWaker Waker,
RunCancellationRegistry RunCancels);
public static Built Build(IDbContextFactory<ClaudeDoDbContext> dbFactory)
{
var hub = new CapturingHubContext();
var broadcaster = new HubBroadcaster(hub);
var waker = new CountingQueueWaker();
var runCancels = new RunCancellationRegistry(NullLogger<RunCancellationRegistry>.Instance);
TaskStateService? state = null;
var chain = new PlanningChainCoordinator(dbFactory, () => state!);
state = new TaskStateService(
dbFactory,
broadcaster,
waker,
chain,
runCancels,
NullLogger<TaskStateService>.Instance);
return new Built(state, chain, hub, () => waker.Count, waker, runCancels);
}
}
public sealed class CountingQueueWaker : IQueueWaker
{
private int _count;
public int Count => Volatile.Read(ref _count);
public void Wake() => Interlocked.Increment(ref _count);
}