Files
ClaudeDo/tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelTests.cs
T
mika kuns 29171b104b fix(ui): failureReason "usage_limit" nachziehen + Worktree-Gate prüft die Platte
Zwei liegengebliebene Consumer aus den letzten beiden Commits:

- TaskRunner klassifiziert seit 07dd7570 "usage_limit", aber weder
  TaskRowViewModel.FailureReasonLabel noch vm.failureReason (de/en) noch
  die get_task-Tool-Beschreibung kannten den Wert — die UI zeigte
  "Grund unbekannt", das MCP-Doc listete weiterhin max_turns|timeout|error.
- TaskRowViewModel.CanOpenWorktree prüfte nur auf einen nicht-leeren
  String. Die Zeile behält den Path eines gemergten/verworfenen Worktrees,
  also war der Menüpunkt aktiv und Process.Start warf in den Footer.
  Jetzt zusätzlich Directory.Exists — dieselbe Prüfung, die
  WorktreesOverviewModalViewModel und MergeSectionViewModel schon machen.
2026-08-24 09:05:31 +02:00

159 lines
5.0 KiB
C#

using System.Collections.Generic;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.ViewModels.Islands;
using Xunit;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Tests.UiVm;
public class TaskRowViewModelTests
{
[Theory]
[InlineData(TaskStatus.Running, "running")]
[InlineData(TaskStatus.WaitingForReview, "review")]
[InlineData(TaskStatus.Failed, "error")]
[InlineData(TaskStatus.Done, "done")]
[InlineData(TaskStatus.Queued, "queued")]
[InlineData(TaskStatus.Idle, "idle")]
public void StatusChipClass_Maps_Correctly(TaskStatus s, string expected)
{
var vm = new TaskRowViewModel { Id = "t" };
vm.Status = s;
Assert.Equal(expected, vm.StatusChipClass);
}
[Fact]
public void Number_SetFromEntity_SurvivesUpdateFromEntity()
{
var entity = new TaskEntity
{
Id = "t", ListId = "l1", Title = "Task", Number = 123, CreatedAt = DateTime.UtcNow,
};
var vm = TaskRowViewModel.FromEntity(entity);
Assert.Equal(123, vm.Number);
// Simulates the TaskUpdated refresh path: the entity is re-fetched and the same
// row is updated in place rather than replaced.
entity.Title = "Task (edited)";
vm.UpdateFromEntity(entity);
Assert.Equal(123, vm.Number);
}
[Theory]
[InlineData(-1, false)]
[InlineData(0, false)]
[InlineData(1, true)]
[InlineData(42, true)]
public void ShowNumberBadge_Hides_For_Zero_And_Negative(int number, bool expected)
{
var vm = new TaskRowViewModel { Id = "t" };
vm.Number = number;
Assert.Equal(expected, vm.ShowNumberBadge);
}
[Fact]
public void ShowNumberBadge_Raises_PropertyChanged_On_Number_Change()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = new List<string?>();
vm.PropertyChanged += (_, e) => raised.Add(e.PropertyName);
vm.Number = 5;
Assert.Contains(nameof(TaskRowViewModel.ShowNumberBadge), raised);
}
[Fact]
public void CanOpenWorktree_True_And_ReasonNull_When_Folder_Exists()
{
var dir = Path.Combine(Path.GetTempPath(), $"cd_wt_{Guid.NewGuid():N}");
Directory.CreateDirectory(dir);
try
{
var vm = new TaskRowViewModel { Id = "t" };
vm.WorktreePath = dir;
Assert.True(vm.CanOpenWorktree);
Assert.Null(vm.OpenWorktreeDisabledReason);
}
finally { Directory.Delete(dir, true); }
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void CanOpenWorktree_False_And_ReasonSet_When_Path_Missing(string? path)
{
var vm = new TaskRowViewModel { Id = "t" };
vm.WorktreePath = path;
Assert.False(vm.CanOpenWorktree);
Assert.NotNull(vm.OpenWorktreeDisabledReason);
}
// A merged/discarded worktree leaves its recorded Path on the row long after the folder is
// gone — the menu entry has to gray out, not throw inside Process.Start.
[Fact]
public void CanOpenWorktree_False_When_Recorded_Folder_Is_Gone()
{
var vm = new TaskRowViewModel { Id = "t" };
vm.WorktreePath = Path.Combine(Path.GetTempPath(), $"cd_wt_gone_{Guid.NewGuid():N}");
Assert.False(vm.CanOpenWorktree);
Assert.NotNull(vm.OpenWorktreeDisabledReason);
}
[Fact]
public void OpenWorktreeGate_Raises_PropertyChanged_On_WorktreePath_Change()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = new List<string?>();
vm.PropertyChanged += (_, e) => raised.Add(e.PropertyName);
vm.WorktreePath = @"C:\repo\worktrees\t";
Assert.Contains(nameof(TaskRowViewModel.CanOpenWorktree), raised);
Assert.Contains(nameof(TaskRowViewModel.OpenWorktreeDisabledReason), raised);
}
[Fact]
public void WorktreePath_SetFromEntity_MapsFromWorktreePath()
{
var entity = new TaskEntity
{
Id = "t", ListId = "l1", Title = "Task", CreatedAt = DateTime.UtcNow,
Worktree = new WorktreeEntity
{
TaskId = "t", Path = @"C:\repo\worktrees\t", BranchName = "b",
BaseCommit = "abc123", CreatedAt = DateTime.UtcNow,
},
};
var vm = TaskRowViewModel.FromEntity(entity);
Assert.Equal(@"C:\repo\worktrees\t", vm.WorktreePath);
}
[Fact]
public void IsDropTarget_Follows_Either_DropHint_And_Raises_PropertyChanged()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = new List<string?>();
vm.PropertyChanged += (_, e) => raised.Add(e.PropertyName);
Assert.False(vm.IsDropTarget);
vm.DropHintAbove = true;
Assert.True(vm.IsDropTarget);
Assert.Contains(nameof(TaskRowViewModel.IsDropTarget), raised);
vm.DropHintAbove = false;
vm.DropHintBelow = true;
Assert.True(vm.IsDropTarget);
vm.DropHintBelow = false;
Assert.False(vm.IsDropTarget);
}
}