feat(mission-control): Handler-Run per Knopf abbrechen
Ein "Abandon run"-Knopf an Handler-Panes fragt HandlerRoundState.Get(handlerTaskId) nach den noch offenen (Queued/Running) Tasks des Laufs, bestaetigt mit Anzahl, cancelt jeden ueber den bestehenden CancelTaskAsync-Pfad und schliesst danach die Session -- ein Schritt statt Tile schliessen + Queue separat abbrechen. Ohne offene Tasks (oder ohne Round-Eintrag) schliesst die Aktion direkt, ohne Dialog. Nur an den beiden Handler-Panes sichtbar (IsListHandler), normale Task-/Ad-hoc- Panes bleiben unveraendert.
This commit is contained in:
@@ -138,6 +138,8 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public virtual Task ReportInteractiveSessionExitedAsync(string taskId, int exitCode, CancellationToken ct = default)
|
||||
=> Task.CompletedTask;
|
||||
public virtual Task<IReadOnlyList<HandlerOpenTaskDto>> GetHandlerOpenTasksAsync(string handlerTaskId, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<HandlerOpenTaskDto>>(Array.Empty<HandlerOpenTaskDto>());
|
||||
public virtual Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public virtual Task<LaunchSpec> GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
|
||||
@@ -966,4 +966,110 @@ public class MissionControlViewModelTests : IDisposable
|
||||
vm.ToggleLayoutCommand.Execute(null);
|
||||
Assert.False(vm.IsFocusMode);
|
||||
}
|
||||
|
||||
// ── Abandon run: single-action "stop session + cancel its still-open tasks" ────────────────
|
||||
|
||||
private sealed class RecordingAbandonWorker : StubWorkerClient
|
||||
{
|
||||
public IReadOnlyList<HandlerOpenTaskDto> OpenTasks { get; set; } = Array.Empty<HandlerOpenTaskDto>();
|
||||
public readonly List<string> Cancelled = new();
|
||||
|
||||
public override Task<IReadOnlyList<HandlerOpenTaskDto>> GetHandlerOpenTasksAsync(string handlerTaskId, CancellationToken ct = default)
|
||||
=> Task.FromResult(OpenTasks);
|
||||
|
||||
public override Task CancelTaskAsync(string taskId)
|
||||
{
|
||||
Cancelled.Add(taskId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// A plain OpenConPtySessionAsync pane (not a list handler) never shows/allows the action --
|
||||
// acceptance criterion 5 ("only at handler panes").
|
||||
[Fact]
|
||||
public async Task AbandonRun_OnNonHandlerPane_IsNotAvailable()
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
await vm.OpenConPtySessionAsync("t1");
|
||||
var pane = vm.ConPtySessions[0];
|
||||
|
||||
Assert.False(pane.IsListHandler);
|
||||
Assert.False(pane.AbandonRunCommand.CanExecute(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AbandonRun_ListHandlerPane_IsListHandlerTrue_CommandAvailable()
|
||||
{
|
||||
var worker = new RecordingAbandonWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
|
||||
var pane = Assert.Single(vm.ConPtySessions);
|
||||
|
||||
Assert.True(pane.IsListHandler);
|
||||
Assert.True(pane.AbandonRunCommand.CanExecute(null));
|
||||
}
|
||||
|
||||
// Acceptance criterion 4: no open tasks (or no round entry, which the fake models the same
|
||||
// way) closes the pane straight away -- no confirmation dialog.
|
||||
[Fact]
|
||||
public async Task AbandonRun_NoOpenTasks_ClosesPaneWithoutConfirming()
|
||||
{
|
||||
var worker = new RecordingAbandonWorker { OpenTasks = Array.Empty<HandlerOpenTaskDto>() };
|
||||
using var vm = BuildVm(worker);
|
||||
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
|
||||
var pane = Assert.Single(vm.ConPtySessions);
|
||||
var confirmCalled = false;
|
||||
vm.ConfirmAsync = _ => { confirmCalled = true; return Task.FromResult(true); };
|
||||
|
||||
pane.AbandonRunCommand.Execute(null);
|
||||
await Task.Delay(20);
|
||||
|
||||
Assert.False(confirmCalled);
|
||||
Assert.Empty(vm.ConPtySessions);
|
||||
}
|
||||
|
||||
// Acceptance criterion 3 (decline branch): declining leaves the session and its tasks
|
||||
// untouched.
|
||||
[Fact]
|
||||
public async Task AbandonRun_OpenTasks_ConfirmDeclined_LeavesSessionAndTasksUntouched()
|
||||
{
|
||||
var worker = new RecordingAbandonWorker
|
||||
{
|
||||
OpenTasks = new[] { new HandlerOpenTaskDto("t1", "First"), new HandlerOpenTaskDto("t2", "Second") },
|
||||
};
|
||||
using var vm = BuildVm(worker);
|
||||
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1", "t2" });
|
||||
var pane = Assert.Single(vm.ConPtySessions);
|
||||
string? confirmMessage = null;
|
||||
vm.ConfirmAsync = msg => { confirmMessage = msg; return Task.FromResult(false); };
|
||||
|
||||
pane.AbandonRunCommand.Execute(null);
|
||||
await Task.Delay(20);
|
||||
|
||||
Assert.NotNull(confirmMessage);
|
||||
Assert.Empty(worker.Cancelled);
|
||||
Assert.Single(vm.ConPtySessions);
|
||||
}
|
||||
|
||||
// Acceptance criteria 1-3 (accept branch): the count is confirmed, every open task is
|
||||
// cancelled, then the pane closes -- one action instead of two.
|
||||
[Fact]
|
||||
public async Task AbandonRun_OpenTasks_ConfirmAccepted_CancelsEachThenClosesPane()
|
||||
{
|
||||
var worker = new RecordingAbandonWorker
|
||||
{
|
||||
OpenTasks = new[] { new HandlerOpenTaskDto("t1", "First"), new HandlerOpenTaskDto("t2", "Second") },
|
||||
};
|
||||
using var vm = BuildVm(worker);
|
||||
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1", "t2" });
|
||||
var pane = Assert.Single(vm.ConPtySessions);
|
||||
vm.ConfirmAsync = _ => Task.FromResult(true);
|
||||
|
||||
pane.AbandonRunCommand.Execute(null);
|
||||
await Task.Delay(20);
|
||||
|
||||
Assert.Equal(new[] { "t1", "t2" }, worker.Cancelled);
|
||||
Assert.Empty(vm.ConPtySessions);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user