using ClaudeDo.Data; using ClaudeDo.Worker.Config; using ClaudeDo.Worker.Git; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Planning; using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.State; using ClaudeDo.Worker.Tickets; 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 WakeCount, CountingQueueWaker Waker, RunCancellationRegistry RunCancels); public static Built Build( IDbContextFactory dbFactory, Func? mergeState = null, BaseDirtyChecker? baseDirtyChecker = null) { var hub = new CapturingHubContext(); var broadcaster = new HubBroadcaster(hub); var waker = new CountingQueueWaker(); var runCancels = new RunCancellationRegistry(NullLogger.Instance); // Unconfigured on purpose: no base URL, no PAT file at this (unused) path — IsConfigured // is false, so TicketStatusSync.SyncAsync short-circuits before touching the DB or the // network. Tests that care about ticket sync build TicketStatusSync themselves. var ticketConfig = new TicketSystemConfig( new WorkerConfig(), new TicketPatStore(new DpapiTokenStore(Path.Combine(Path.GetTempPath(), $"cdo-unused-pat-{Guid.NewGuid():N}")))); var ticketSync = new TicketStatusSync( ticketConfig, new TicketClientFactory(ticketConfig, new NoopHttpClientFactory(), NullLogger.Instance), dbFactory, NullLogger.Instance); TaskStateService? state = null; var chain = new PlanningChainCoordinator(dbFactory, () => state!); state = new TaskStateService( dbFactory, broadcaster, waker, chain, runCancels, mergeState ?? (() => NoActiveMergeState.Instance), baseDirtyChecker ?? new BaseDirtyChecker(new ClaudeDo.Data.Git.GitService(), NullLogger.Instance), NullLogger.Instance, ticketSync); return new Built(state, chain, hub, () => waker.Count, waker, runCancels); } } file sealed class NoActiveMergeState : IActiveMergeState { public static readonly NoActiveMergeState Instance = new(); public bool HasActiveMerge(string taskId) => false; } // Never actually invoked: TicketSystemConfig.IsConfigured is false in every TaskStateServiceBuilder // test, so TicketClientFactory.Create() returns null before this factory would be called. file sealed class NoopHttpClientFactory : IHttpClientFactory { public HttpClient CreateClient(string name) => throw new InvalidOperationException( "Unexpected: ticket sync is not configured in tests built via TaskStateServiceBuilder."); } public sealed class CountingQueueWaker : IQueueWaker { private int _count; public int Count => Volatile.Read(ref _count); public void Wake() => Interlocked.Increment(ref _count); }