feat(review): submit interactive (ConPTY) work for review
An embedded ConPTY session leaves its worktree changed but never touches task status, so hand-driven work had no path into the review/merge flow. Add SubmitTaskForReview: commit the worktree (same auto-commit as a headless run), then transition Idle/Failed -> WaitingForReview via the new TaskStateService.SubmitInteractiveForReviewAsync. Approve then merges it. Surfaces: a 'Submit for review' button in the detail work console (shown for an Idle/Failed task with a worktree) and on the ConPTY Command Center pane header (task-based panes; closes the pane on success). Tests cover the new transition (Idle/Failed accepted, Running/Queued/Done/Review rejected).
This commit is contained in:
@@ -95,6 +95,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public virtual Task AbortConflictMergeAsync(string taskId) => Task.CompletedTask;
|
||||
public virtual Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public virtual Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public virtual Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public virtual Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public virtual Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||
|
||||
@@ -87,6 +87,38 @@ public sealed class ReviewTransitionTests : IDisposable
|
||||
Assert.Equal(TaskStatus.Queued, (await GetTaskAsync(id)).Status);
|
||||
}
|
||||
|
||||
// ─── SubmitInteractiveForReviewAsync ──────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData(TaskStatus.Idle)]
|
||||
[InlineData(TaskStatus.Failed)]
|
||||
public async Task SubmitInteractiveForReviewAsync_FromIdleOrFailed_TransitionsToWaitingForReview(TaskStatus from)
|
||||
{
|
||||
var id = await SeedTaskAsync(from);
|
||||
|
||||
var result = await _sut.SubmitInteractiveForReviewAsync(id, DateTime.UtcNow, default);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
var t = await GetTaskAsync(id);
|
||||
Assert.Equal(TaskStatus.WaitingForReview, t.Status);
|
||||
Assert.NotNull(t.FinishedAt);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TaskStatus.Running)]
|
||||
[InlineData(TaskStatus.Queued)]
|
||||
[InlineData(TaskStatus.Done)]
|
||||
[InlineData(TaskStatus.WaitingForReview)]
|
||||
public async Task SubmitInteractiveForReviewAsync_FromOtherStates_Rejects(TaskStatus from)
|
||||
{
|
||||
var id = await SeedTaskAsync(from);
|
||||
|
||||
var result = await _sut.SubmitInteractiveForReviewAsync(id, DateTime.UtcNow, default);
|
||||
|
||||
Assert.False(result.Ok);
|
||||
Assert.Equal(from, (await GetTaskAsync(id)).Status);
|
||||
}
|
||||
|
||||
// ─── ApproveReviewAsync ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -72,6 +72,7 @@ sealed class FakeWorkerClient : IWorkerClient
|
||||
public int PickUpInTerminalCalls { get; private set; }
|
||||
public string? LastPickUpTaskId { get; private set; }
|
||||
public Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default) { PickUpInTerminalCalls++; LastPickUpTaskId = taskId; return Task.CompletedTask; }
|
||||
public Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||
|
||||
Reference in New Issue
Block a user