feat(worker): PlanningSessionManager.ResumeAsync

This commit is contained in:
mika kuns
2026-04-23 20:55:01 +02:00
parent b6bec1e63c
commit 84b0ba8670
2 changed files with 58 additions and 0 deletions

View File

@@ -53,6 +53,25 @@ public sealed class PlanningSessionManager
return new PlanningSessionStartContext(taskId, list.WorkingDir, files); return new PlanningSessionStartContext(taskId, list.WorkingDir, files);
} }
public async Task<PlanningSessionResumeContext> ResumeAsync(string taskId, CancellationToken ct)
{
var task = await _tasks.GetByIdAsync(taskId, ct)
?? throw new InvalidOperationException($"Task {taskId} not found.");
if (task.Status != TaskStatus.Planning)
throw new InvalidOperationException($"Task is in status {task.Status}; resume requires Planning.");
if (string.IsNullOrEmpty(task.PlanningSessionId))
throw new InvalidOperationException("No Claude session ID captured yet; cannot resume.");
var sessionDir = Path.Combine(_rootDirectory, taskId);
var mcpConfigPath = Path.Combine(sessionDir, "mcp.json");
if (!File.Exists(mcpConfigPath))
throw new InvalidOperationException($"Session directory missing: {sessionDir}");
var list = await _lists.GetByIdAsync(task.ListId, ct)
?? throw new InvalidOperationException($"List {task.ListId} not found.");
return new PlanningSessionResumeContext(taskId, list.WorkingDir, task.PlanningSessionId, mcpConfigPath);
}
private static string GenerateToken() private static string GenerateToken()
{ {
var bytes = RandomNumberGenerator.GetBytes(32); var bytes = RandomNumberGenerator.GetBytes(32);

View File

@@ -120,4 +120,43 @@ public sealed class PlanningSessionManagerTests : IDisposable
await Assert.ThrowsAsync<InvalidOperationException>(() => await Assert.ThrowsAsync<InvalidOperationException>(() =>
_sut.StartAsync(child.Id, CancellationToken.None)); _sut.StartAsync(child.Id, CancellationToken.None));
} }
[Fact]
public async Task ResumeAsync_ReturnsExistingSessionDetails()
{
var (listId, wd) = await SeedListAsync();
var parent = await SeedManualTaskAsync(listId);
var startCtx = await _sut.StartAsync(parent.Id, CancellationToken.None);
await _tasks.UpdatePlanningSessionIdAsync(parent.Id, "claude-session-42");
var resumeCtx = await _sut.ResumeAsync(parent.Id, CancellationToken.None);
Assert.Equal(parent.Id, resumeCtx.ParentTaskId);
Assert.Equal(wd, resumeCtx.WorkingDir);
Assert.Equal("claude-session-42", resumeCtx.ClaudeSessionId);
Assert.Equal(startCtx.Files.McpConfigPath, resumeCtx.McpConfigPath);
Assert.True(File.Exists(resumeCtx.McpConfigPath));
}
[Fact]
public async Task ResumeAsync_NotPlanning_Throws()
{
var (listId, _) = await SeedListAsync();
var parent = await SeedManualTaskAsync(listId);
// did not start
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_sut.ResumeAsync(parent.Id, CancellationToken.None));
}
[Fact]
public async Task ResumeAsync_NoClaudeSessionId_Throws()
{
var (listId, _) = await SeedListAsync();
var parent = await SeedManualTaskAsync(listId);
await _sut.StartAsync(parent.Id, CancellationToken.None);
// UpdatePlanningSessionIdAsync not called
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_sut.ResumeAsync(parent.Id, CancellationToken.None));
}
} }