From 0782ba574b26edb8d904be0b84ca754261ec351e Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 23 Apr 2026 23:31:01 +0200 Subject: [PATCH] test(worker): planning session end-to-end --- .../Planning/PlanningEndToEndTests.cs | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/ClaudeDo.Worker.Tests/Planning/PlanningEndToEndTests.cs diff --git a/tests/ClaudeDo.Worker.Tests/Planning/PlanningEndToEndTests.cs b/tests/ClaudeDo.Worker.Tests/Planning/PlanningEndToEndTests.cs new file mode 100644 index 0000000..4451cde --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/Planning/PlanningEndToEndTests.cs @@ -0,0 +1,109 @@ +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)); + } +}