Merge claudedo/c52ba287db6e4a7c9bac38c30bcc21ec

This commit is contained in:
mika kuns
2026-08-06 13:50:41 +02:00
9 changed files with 299 additions and 8 deletions
@@ -3,6 +3,7 @@ using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.MissionControl;
using Microsoft.EntityFrameworkCore;
using Xunit;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
@@ -606,6 +607,102 @@ public class MissionControlViewModelTests : IDisposable
Assert.NotNull(error);
}
private sealed class BlockingSubmitWorker : StubWorkerClient
{
public int CallCount { get; private set; }
public readonly TaskCompletionSource<object?> Gate = new();
public override Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default)
{
CallCount++;
return Gate.Task;
}
}
[Fact]
public async Task SubmitForReview_RapidDoubleClick_OnlyCallsWorkerOnce()
{
var worker = new BlockingSubmitWorker();
using var vm = BuildVm(worker);
await vm.OpenConPtySessionAsync("t1");
var pane = vm.ConPtySessions[0];
pane.Terminal.IsRunning = true; // simulate a live hand-driven session
// Bypass CanExecute entirely -- Execute(null) is what a genuinely simultaneous
// double-click would still reach even if the button briefly disables itself.
pane.SubmitForReviewCommand.Execute(null);
pane.SubmitForReviewCommand.Execute(null);
Assert.Equal(1, worker.CallCount);
Assert.True(pane.IsSubmitPending);
worker.Gate.SetResult(null);
await Task.Delay(20);
Assert.Empty(vm.ConPtySessions);
}
[Fact]
public async Task SubmitForReview_Failure_ClearsIsSubmitPending_AllowingRetry()
{
var worker = new ThrowingSubmitWorker();
using var vm = BuildVm(worker);
await vm.OpenConPtySessionAsync("t1");
var pane = vm.ConPtySessions[0];
pane.Terminal.IsRunning = true;
string? error = null;
vm.ErrorReported += msg => error = msg;
pane.SubmitForReviewCommand.Execute(null);
Assert.NotNull(error);
Assert.False(pane.IsSubmitPending);
Assert.True(pane.SubmitForReviewCommand.CanExecute(null));
}
private sealed class ThrowingSubmitWorker : StubWorkerClient
{
public override Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default)
=> throw new InvalidOperationException("worker unreachable");
}
private static int SubscriberCount(ConPtyPaneViewModel pane, string eventFieldName)
{
var field = typeof(ConPtyPaneViewModel).GetField(eventFieldName,
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var del = (Delegate?)field!.GetValue(pane);
return del?.GetInvocationList().Length ?? 0;
}
[Fact]
public async Task CloseConPtySession_UnsubscribesSubmitForReviewRequested()
{
var worker = new FakeWorker();
using var vm = BuildVm(worker);
await vm.OpenConPtySessionAsync("t1");
var pane = vm.ConPtySessions[0];
Assert.Equal(1, SubscriberCount(pane, "SubmitForReviewRequested"));
pane.CloseCommand.Execute(null);
Assert.Equal(0, SubscriberCount(pane, "SubmitForReviewRequested"));
Assert.Equal(0, SubscriberCount(pane, "ErrorReported"));
}
[Fact]
public async Task Dispose_UnsubscribesSubmitForReviewRequested()
{
var worker = new FakeWorker();
var vm = BuildVm(worker);
await vm.OpenConPtySessionAsync("t1");
var pane = vm.ConPtySessions[0];
vm.Dispose();
Assert.Equal(0, SubscriberCount(pane, "SubmitForReviewRequested"));
}
[Fact]
public void ToggleLayoutCommand_FlipsIsFocusMode()
{