using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Planning; using ClaudeDo.Worker.Tests.Infrastructure; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Planning; public sealed class PlanningSessionManagerTests : IDisposable { private readonly DbFixture _db = new(); private readonly ClaudeDoDbContext _ctx; private readonly TaskRepository _tasks; private readonly ListRepository _lists; private readonly string _rootDir; private readonly PlanningSessionManager _sut; public PlanningSessionManagerTests() { _ctx = _db.CreateContext(); _tasks = new TaskRepository(_ctx); _lists = new ListRepository(_ctx); _rootDir = Path.Combine(Path.GetTempPath(), $"cd_planning_{Guid.NewGuid():N}"); _sut = new PlanningSessionManager(_tasks, _lists, _rootDir); } public void Dispose() { _ctx.Dispose(); _db.Dispose(); try { Directory.Delete(_rootDir, recursive: true); } catch { /* ignore */ } } private async Task<(string listId, string workingDir)> SeedListAsync() { var listId = Guid.NewGuid().ToString(); var wd = Path.Combine(Path.GetTempPath(), $"cd_wd_{Guid.NewGuid():N}"); Directory.CreateDirectory(wd); await _lists.AddAsync(new ListEntity { Id = listId, Name = "Test", WorkingDir = wd, CreatedAt = DateTime.UtcNow, }); return (listId, wd); } private async Task SeedManualTaskAsync(string listId) { var t = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = listId, Title = "Brainstorm auth", Description = "- review tokens\n- plan rollout", Status = TaskStatus.Manual, CreatedAt = DateTime.UtcNow, CommitType = "feat", }; await _tasks.AddAsync(t); return t; } [Fact] public async Task StartAsync_CreatesSessionFiles_AndTransitionsTaskToPlanning() { var (listId, wd) = await SeedListAsync(); var parent = await SeedManualTaskAsync(listId); var ctx = await _sut.StartAsync(parent.Id, CancellationToken.None); Assert.Equal(parent.Id, ctx.ParentTaskId); Assert.Equal(wd, ctx.WorkingDir); Assert.True(File.Exists(ctx.Files.McpConfigPath)); Assert.True(File.Exists(ctx.Files.SystemPromptPath)); Assert.True(File.Exists(ctx.Files.InitialPromptPath)); var mcp = await File.ReadAllTextAsync(ctx.Files.McpConfigPath); Assert.Contains("\"type\": \"http\"", mcp); Assert.Contains("Bearer ", mcp); var initial = await File.ReadAllTextAsync(ctx.Files.InitialPromptPath); Assert.Contains("Brainstorm auth", initial); Assert.Contains("review tokens", initial); var loaded = await _tasks.GetByIdAsync(parent.Id); Assert.Equal(TaskStatus.Planning, loaded!.Status); Assert.NotNull(loaded.PlanningSessionToken); } [Fact] public async Task StartAsync_TaskNotManual_Throws() { var (listId, _) = await SeedListAsync(); var queuedTask = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = listId, Title = "x", Status = TaskStatus.Queued, CreatedAt = DateTime.UtcNow, CommitType = "feat", }; await _tasks.AddAsync(queuedTask); await Assert.ThrowsAsync(() => _sut.StartAsync(queuedTask.Id, CancellationToken.None)); } [Fact] public async Task StartAsync_ChildTask_Throws() { var (listId, _) = await SeedListAsync(); var parent = await SeedManualTaskAsync(listId); await _tasks.SetPlanningStartedAsync(parent.Id, "t"); var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null); await Assert.ThrowsAsync(() => _sut.StartAsync(child.Id, CancellationToken.None)); } }