feat(worker): PlanningSessionManager.StartAsync
Add PlanningSessionFiles, PlanningSessionStartContext/ResumeContext DTOs, PlanningSessionManager.StartAsync (file scaffolding + status transition), and integration tests. Also fix migration discovery by adding [DbContext] attribute to all migration classes and switch DbFixture to EnsureCreated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,9 +10,9 @@ public sealed class DbFixture : IDisposable
|
||||
public DbFixture()
|
||||
{
|
||||
DbPath = Path.Combine(Path.GetTempPath(), $"claudedo_test_{Guid.NewGuid():N}.db");
|
||||
// Apply migrations so the schema is created.
|
||||
// EnsureCreated uses the current model directly — no Designer.cs needed.
|
||||
using var ctx = CreateContext();
|
||||
ctx.Database.Migrate();
|
||||
ctx.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public ClaudeDoDbContext CreateContext()
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
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<TaskEntity> 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<InvalidOperationException>(() =>
|
||||
_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<InvalidOperationException>(() =>
|
||||
_sut.StartAsync(child.Id, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user