feat/planning-sessions-worker #7
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user