feat(worker): PlanningSessionManager.ResumeAsync
This commit is contained in:
@@ -53,6 +53,25 @@ public sealed class PlanningSessionManager
|
||||
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()
|
||||
{
|
||||
var bytes = RandomNumberGenerator.GetBytes(32);
|
||||
|
||||
@@ -120,4 +120,43 @@ public sealed class PlanningSessionManagerTests : IDisposable
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user