diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index 7aa83e84..4ae3d840 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -450,10 +450,10 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString()); } - public Task ApproveReviewAsync(string taskId, string targetBranch) + public async Task ApproveReviewAsync(string taskId, string targetBranch) { LastApproveTarget = targetBranch; - return TryInvokeAsync("ApproveReview", taskId, targetBranch); + return await _hub.InvokeAsync("ApproveReview", taskId, targetBranch); } public Task PreviewMergeAsync(string taskId, string targetBranch) diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandReviewActionsTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandReviewActionsTests.cs index e5541022..aba181ae 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandReviewActionsTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandReviewActionsTests.cs @@ -137,4 +137,28 @@ public class DetailsIslandReviewActionsTests : IDisposable Assert.True(vm.ShowReviewDiffHint); Assert.False(vm.ApproveReviewCommand.CanExecute(null)); } + + private sealed class ThrowingWorkerClient : StubWorkerClient + { + public override bool IsConnected => true; + public string ExceptionMessage { get; init; } = "blocked: target working tree has uncommitted changes"; + public override Task ApproveReviewAsync(string taskId, string targetBranch) => + throw new Exception(ExceptionMessage); + } + + [Fact] + public async Task ApproveReview_WhenWorkerThrows_CallsShowErrorAsync() + { + var worker = new ThrowingWorkerClient(); + var vm = BuildVm(worker); + vm.Bind(new TaskRowViewModel { Id = "task-err-1", Status = TaskStatus.WaitingForReview }); + vm.Monitor.ApplyState(TaskStatus.WaitingForReview); + + string? reportedError = null; + vm.ShowErrorAsync = msg => { reportedError = msg; return Task.CompletedTask; }; + + await vm.ApproveReviewCommand.ExecuteAsync(null); + + Assert.Equal(worker.ExceptionMessage, reportedError); + } } diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandApproveReviewTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandApproveReviewTests.cs new file mode 100644 index 00000000..a670a3f8 --- /dev/null +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandApproveReviewTests.cs @@ -0,0 +1,66 @@ +using ClaudeDo.Data; +using ClaudeDo.Data.Models; +using ClaudeDo.Ui.Services; +using ClaudeDo.Ui.ViewModels.Islands; +using Microsoft.EntityFrameworkCore; +using TaskStatus = ClaudeDo.Data.Models.TaskStatus; + +namespace ClaudeDo.Ui.Tests.ViewModels; + +public class TasksIslandApproveReviewTests : IDisposable +{ + private readonly string _dbPath; + + public TasksIslandApproveReviewTests() + { + _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_approve_test_{Guid.NewGuid():N}.db"); + using var ctx = NewContext(); + ctx.Database.EnsureCreated(); + } + + public void Dispose() + { + try { File.Delete(_dbPath); } catch { } + try { File.Delete(_dbPath + "-wal"); } catch { } + try { File.Delete(_dbPath + "-shm"); } catch { } + } + + private ClaudeDoDbContext NewContext() + { + var opts = new DbContextOptionsBuilder() + .UseSqlite($"Data Source={_dbPath}") + .Options; + return new ClaudeDoDbContext(opts); + } + + private sealed class TestDbFactory : IDbContextFactory + { + private readonly Func _create; + public TestDbFactory(Func create) => _create = create; + public ClaudeDoDbContext CreateDbContext() => _create(); + } + + private sealed class ThrowingWorkerClient : StubWorkerClient + { + public string ExceptionMessage { get; init; } = "blocked: target working tree has uncommitted changes"; + public override Task ApproveReviewAsync(string taskId, string targetBranch) => + throw new Exception(ExceptionMessage); + } + + [Fact] + public async Task ApproveReview_WhenWorkerThrows_RaisesErrorReported() + { + var worker = new ThrowingWorkerClient(); + var factory = new TestDbFactory(NewContext); + var vm = new TasksIslandViewModel(factory, worker); + + string? reportedError = null; + vm.ErrorReported += msg => reportedError = msg; + + var row = new TaskRowViewModel { Id = "task-err-2", Status = TaskStatus.WaitingForReview }; + await vm.ApproveReviewCommand.ExecuteAsync(row); + + Assert.NotNull(reportedError); + Assert.Contains(worker.ExceptionMessage, reportedError); + } +}