Merge claudedo/3832008a0b5147bf8308407ed4bd1ded
This commit is contained in:
@@ -21,11 +21,14 @@ public class DiffViewerViewModelTests
|
||||
{
|
||||
public IReadOnlyList<SubtaskDiffDto> AggregateResult { get; set; } = Array.Empty<SubtaskDiffDto>();
|
||||
public CombinedDiffResultDto? CombinedResult { get; set; }
|
||||
public string? CombinedException { get; set; }
|
||||
|
||||
public override Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId) =>
|
||||
Task.FromResult(AggregateResult);
|
||||
public override Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) =>
|
||||
Task.FromResult(CombinedResult);
|
||||
CombinedException is not null
|
||||
? throw new Exception(CombinedException)
|
||||
: Task.FromResult(CombinedResult);
|
||||
}
|
||||
|
||||
// ── Files mode: commit-range guards (ported from DiffModal) ──
|
||||
@@ -193,4 +196,26 @@ public class DiffViewerViewModelTests
|
||||
Assert.NotNull(vm.CombinedWarning);
|
||||
Assert.NotEmpty(vm.CombinedWarning!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Planning_ToggleCombined_WhenWorkerThrows_ShowsExceptionMessage()
|
||||
{
|
||||
var fake = new FakePlanningWorker
|
||||
{
|
||||
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
|
||||
CombinedException = "planning task not found",
|
||||
};
|
||||
var vm = new DiffViewerViewModel(null!, fake);
|
||||
vm.ConfigurePlanning("plan-1", "main");
|
||||
await vm.LoadAsync();
|
||||
|
||||
vm.IsCombinedMode = true;
|
||||
|
||||
var deadline = DateTime.UtcNow.AddSeconds(5);
|
||||
while (DateTime.UtcNow < deadline && vm.IsLoadingCombined) await Task.Delay(10);
|
||||
|
||||
Assert.NotNull(vm.CombinedWarning);
|
||||
Assert.Contains("planning task not found", vm.CombinedWarning);
|
||||
Assert.Equal("", vm.DisplayedDiff);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.IO;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class FilesSettingsTabViewModelTests
|
||||
{
|
||||
public FilesSettingsTabViewModelTests()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales")))
|
||||
dir = Path.GetDirectoryName(dir);
|
||||
Loc.Current = new Localizer(
|
||||
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
|
||||
}
|
||||
|
||||
private sealed class ThrowingWorker : StubWorkerClient
|
||||
{
|
||||
public string ExceptionMessage { get; init; } = "permission denied copying agent files";
|
||||
public override Task<SeedResultDto?> RestoreDefaultAgentsAsync() =>
|
||||
throw new Exception(ExceptionMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RestoreDefaultAgents_WhenWorkerThrows_ShowsExceptionMessage_NotGenericOffline()
|
||||
{
|
||||
var worker = new ThrowingWorker();
|
||||
var vm = new FilesSettingsTabViewModel(worker);
|
||||
|
||||
await vm.RestoreDefaultAgentsCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Contains(worker.ExceptionMessage, vm.StatusMessage);
|
||||
Assert.False(vm.IsBusy);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,33 @@ public class NotesEditorViewModelTests
|
||||
public Task DeleteAsync(string id) { Store.RemoveAll(n => n.Id == id); return Task.CompletedTask; }
|
||||
}
|
||||
|
||||
private sealed class ThrowingNotes : INotesApi
|
||||
{
|
||||
public string ExceptionMessage { get; init; } = "worker offline";
|
||||
public Task<List<DailyNoteDto>> ListAsync(DateOnly day) => Task.FromResult(new List<DailyNoteDto>());
|
||||
public Task<DailyNoteDto?> AddAsync(DateOnly day, string text) => throw new Exception(ExceptionMessage);
|
||||
public Task UpdateAsync(string id, string text) => Task.CompletedTask;
|
||||
public Task DeleteAsync(string id) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddBullet_WhenApiThrows_RaisesErrorReported_AndKeepsDraftText()
|
||||
{
|
||||
var api = new ThrowingNotes();
|
||||
var vm = new NotesEditorViewModel(api);
|
||||
await vm.LoadDayAsync(new DateOnly(2026, 6, 1));
|
||||
|
||||
string? reportedError = null;
|
||||
vm.ErrorReported += msg => reportedError = msg;
|
||||
|
||||
vm.NewBulletText = "Standup vorbereitet";
|
||||
await vm.AddBulletCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Equal(api.ExceptionMessage, reportedError);
|
||||
Assert.Empty(vm.Bullets);
|
||||
Assert.Equal("Standup vorbereitet", vm.NewBulletText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddBullet_PersistsAndAppears_ForCurrentDay()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,14 @@ public class PrimeClaudeTabViewModelTests
|
||||
public Task DeleteAsync(Guid id) { Deletes.Add(id); return Task.CompletedTask; }
|
||||
}
|
||||
|
||||
private sealed class ThrowingApi : IPrimeScheduleApi
|
||||
{
|
||||
public string ExceptionMessage { get; init; } = "worker offline";
|
||||
public Task<List<PrimeScheduleDto>> ListAsync() => Task.FromResult(new List<PrimeScheduleDto>());
|
||||
public Task<PrimeScheduleDto?> UpsertAsync(PrimeScheduleDto dto) => throw new Exception(ExceptionMessage);
|
||||
public Task DeleteAsync(Guid id) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static PrimeScheduleDto Dto(Guid id, int days, TimeSpan time) =>
|
||||
new(id, days, time, true, null, null);
|
||||
|
||||
@@ -93,4 +101,19 @@ public class PrimeClaudeTabViewModelTests
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
Assert.Null(vm.Validate());
|
||||
}
|
||||
|
||||
// SettingsModalViewModel.Save() is the only place that catches Prime.SaveAsync's
|
||||
// failures (via a try/catch around the whole settings save) and surfaces them as
|
||||
// "Save failed: {message}". For that to work, SaveAsync must propagate the worker's
|
||||
// exception rather than swallow it.
|
||||
[Fact]
|
||||
public async Task Save_WhenApiThrows_PropagatesException()
|
||||
{
|
||||
var api = new ThrowingApi();
|
||||
var vm = new PrimeClaudeTabViewModel(api);
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<Exception>(() => vm.SaveAsync());
|
||||
Assert.Equal(api.ExceptionMessage, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.IO;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Modals;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class WorktreesOverviewModalErrorTests
|
||||
{
|
||||
public WorktreesOverviewModalErrorTests()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales")))
|
||||
dir = Path.GetDirectoryName(dir);
|
||||
Loc.Current = new Localizer(
|
||||
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
|
||||
}
|
||||
|
||||
private sealed class ThrowingWorker : StubWorkerClient
|
||||
{
|
||||
public string ExceptionMessage { get; init; } = "worktree is locked by another process";
|
||||
public override Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null) =>
|
||||
throw new Exception(ExceptionMessage);
|
||||
public override Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId) =>
|
||||
throw new Exception(ExceptionMessage);
|
||||
}
|
||||
|
||||
private static WorktreesOverviewModalViewModel NewVm(ThrowingWorker worker) =>
|
||||
new(worker, () => null!, new MergeCoordinator());
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupFinished_WhenWorkerThrows_ShowsExceptionMessage()
|
||||
{
|
||||
var worker = new ThrowingWorker();
|
||||
var vm = NewVm(worker);
|
||||
|
||||
await vm.CleanupFinishedCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.NotNull(vm.StatusMessage);
|
||||
Assert.Contains(worker.ExceptionMessage, vm.StatusMessage);
|
||||
Assert.False(vm.IsBusy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForceRemove_WhenWorkerThrows_ShowsExceptionMessage_AndKeepsRow()
|
||||
{
|
||||
var worker = new ThrowingWorker();
|
||||
var vm = NewVm(worker);
|
||||
var row = new WorktreeOverviewRowViewModel
|
||||
{
|
||||
TaskId = "task-1",
|
||||
TaskTitle = "Task 1",
|
||||
TaskStatus = ClaudeDo.Data.Models.TaskStatus.Idle,
|
||||
State = ClaudeDo.Data.Models.WorktreeState.Active,
|
||||
};
|
||||
vm.AddRowForTest(row);
|
||||
|
||||
await vm.ForceRemoveCommand.ExecuteAsync(row);
|
||||
|
||||
Assert.NotNull(vm.StatusMessage);
|
||||
Assert.Contains(worker.ExceptionMessage, vm.StatusMessage);
|
||||
Assert.Contains(row, vm.Rows);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.IO;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class WorktreesSettingsTabViewModelTests
|
||||
{
|
||||
public WorktreesSettingsTabViewModelTests()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales")))
|
||||
dir = Path.GetDirectoryName(dir);
|
||||
Loc.Current = new Localizer(
|
||||
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
|
||||
}
|
||||
|
||||
private sealed class ThrowingWorker : StubWorkerClient
|
||||
{
|
||||
public string ExceptionMessage { get; init; } = "disk full";
|
||||
public override Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null) =>
|
||||
throw new Exception(ExceptionMessage);
|
||||
public override Task<WorktreeResetDto?> ResetAllWorktreesAsync() =>
|
||||
throw new Exception(ExceptionMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupWorktrees_WhenWorkerThrows_ShowsExceptionMessage()
|
||||
{
|
||||
var worker = new ThrowingWorker();
|
||||
var vm = new WorktreesSettingsTabViewModel(worker);
|
||||
|
||||
await vm.CleanupWorktreesCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Contains(worker.ExceptionMessage, vm.StatusMessage);
|
||||
Assert.False(vm.IsBusy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfirmResetAll_WhenWorkerThrows_ShowsExceptionMessage()
|
||||
{
|
||||
var worker = new ThrowingWorker();
|
||||
var vm = new WorktreesSettingsTabViewModel(worker);
|
||||
|
||||
await vm.ConfirmResetAllCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Contains(worker.ExceptionMessage, vm.StatusMessage);
|
||||
Assert.False(vm.IsBusy);
|
||||
Assert.False(vm.ShowResetConfirm);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user