using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Planning; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Planning; // Inline fakes — test isolation beats DRY; mirrors PlanningMcpServiceTests pattern. file sealed class E2EFakeHttpContextAccessor : IHttpContextAccessor { public HttpContext? HttpContext { get; set; } } file sealed class E2ENullHubClients : IHubClients { public IClientProxy All => E2ENullClientProxy.Instance; public IClientProxy AllExcept(IReadOnlyList excludedConnectionIds) => E2ENullClientProxy.Instance; public IClientProxy Client(string connectionId) => E2ENullClientProxy.Instance; public IClientProxy Clients(IReadOnlyList connectionIds) => E2ENullClientProxy.Instance; public IClientProxy Group(string groupName) => E2ENullClientProxy.Instance; public IClientProxy GroupExcept(string groupName, IReadOnlyList excludedConnectionIds) => E2ENullClientProxy.Instance; public IClientProxy Groups(IReadOnlyList groupNames) => E2ENullClientProxy.Instance; public IClientProxy User(string userId) => E2ENullClientProxy.Instance; public IClientProxy Users(IReadOnlyList userIds) => E2ENullClientProxy.Instance; } file sealed class E2ENullClientProxy : IClientProxy { public static readonly E2ENullClientProxy Instance = new(); public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) => Task.CompletedTask; } file sealed class E2EFakeHubContext : IHubContext { public IHubClients Clients { get; } = new E2ENullHubClients(); public IGroupManager Groups => throw new NotImplementedException(); } public sealed class PlanningEndToEndTests : IDisposable { private readonly DbFixture _db = new(); private readonly ClaudeDoDbContext _ctx; private readonly TaskRepository _tasks; private readonly ListRepository _lists; private readonly PlanningSessionManager _manager; private readonly DefaultHttpContext _httpContext; private readonly PlanningMcpContextAccessor _accessor; private readonly PlanningMcpService _svc; public PlanningEndToEndTests() { _ctx = _db.CreateContext(); _tasks = new TaskRepository(_ctx); _lists = new ListRepository(_ctx); var root = Path.Combine(Path.GetTempPath(), $"cd_e2e_{Guid.NewGuid():N}"); _manager = new PlanningSessionManager(_tasks, _lists, root); _httpContext = new DefaultHttpContext(); _accessor = new PlanningMcpContextAccessor(new E2EFakeHttpContextAccessor { HttpContext = _httpContext }); var broadcaster = new HubBroadcaster(new E2EFakeHubContext()); _svc = new PlanningMcpService(_tasks, _accessor, broadcaster); } public void Dispose() { _ctx.Dispose(); _db.Dispose(); } [Fact] public async Task StartThenCreateThenFinalize_FullFlow() { var listId = Guid.NewGuid().ToString(); var wd = Path.GetTempPath(); await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", WorkingDir = wd, CreatedAt = DateTime.UtcNow }); var parent = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = listId, Title = "Big Task", Status = TaskStatus.Manual, CreatedAt = DateTime.UtcNow, CommitType = "chore", }; await _tasks.AddAsync(parent); var startCtx = await _manager.StartAsync(parent.Id, CancellationToken.None); Assert.True(File.Exists(startCtx.Files.McpConfigPath)); // Wire the ambient context so _svc reads the correct parent _httpContext.Items["PlanningContext"] = new PlanningMcpContext { ParentTaskId = parent.Id }; await _svc.CreateChildTask("sub 1", null, null, null, CancellationToken.None); await _svc.CreateChildTask("sub 2", null, null, null, CancellationToken.None); var count = await _svc.Finalize(true, CancellationToken.None); Assert.Equal(2, count); var reload = await _tasks.GetByIdAsync(parent.Id); Assert.Equal(TaskStatus.Planned, reload!.Status); var kids = await _tasks.GetChildrenAsync(parent.Id); Assert.All(kids, k => Assert.Equal(TaskStatus.Manual, k.Status)); } }