Files
ClaudeDo/tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelNotificationsTests.cs
T
mika kuns df3e7ba0c7 fix(ui): Dequeue-X auf blockierten Queue-Zeilen anzeigen und Kette konsistent halten
CanRemoveFromQueue schloss IsWaiting (queued+blocked) faelschlich aus, sodass ein
Kettenglied in der Mitte nicht entfernbar war. Zusaetzlich unblockt
TaskStateService.DequeueToIdleAsync jetzt den Nachfolger eines dequeueten
Kettenglieds, sonst bliebe er dauerhaft auf einen nicht mehr laufenden
Vorgaenger blockiert.
2026-09-02 22:34:50 +02:00

465 lines
16 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;
// Characterization test for TaskRowViewModel's PropertyChanged fan-out. For every source
// property that today drives dependent notifications (via a handwritten partial OnXChanged),
// this pins down the exact SET of names raised when that property changes. Order is
// deliberately not asserted: a NotifyPropertyChangedFor-based rewrite raises the source
// property's own notification before its dependents instead of after, which is an unobservable
// reordering, not a behavior change. This test must stay green across that rewrite.
public class TaskRowViewModelNotificationsTests
{
private static HashSet<string?> Raised(TaskRowViewModel vm, System.Action mutate)
{
var raised = new HashSet<string?>();
vm.PropertyChanged += (_, e) => raised.Add(e.PropertyName);
mutate();
return raised;
}
[Fact]
public void StatusChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.Status = TaskStatus.Running);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.Status),
nameof(TaskRowViewModel.StatusChipClass),
nameof(TaskRowViewModel.StatusLabel),
nameof(TaskRowViewModel.ShowStatusChip),
nameof(TaskRowViewModel.IsRunning),
nameof(TaskRowViewModel.IsWaitingForReview),
nameof(TaskRowViewModel.IsParked),
nameof(TaskRowViewModel.IsQueued),
nameof(TaskRowViewModel.IsWaiting),
nameof(TaskRowViewModel.HasFailureReason),
nameof(TaskRowViewModel.FailureReasonTooltip),
nameof(TaskRowViewModel.StatusChipTooltip),
nameof(TaskRowViewModel.IsDraft),
nameof(TaskRowViewModel.IsPlanned),
nameof(TaskRowViewModel.CanOpenPlanningSession),
nameof(TaskRowViewModel.CanRemoveFromQueue),
nameof(TaskRowViewModel.CanSendToQueue),
nameof(TaskRowViewModel.CanRefine),
nameof(TaskRowViewModel.SendToQueueDisabledReason),
nameof(TaskRowViewModel.CancelDisabledReason),
nameof(TaskRowViewModel.RefineDisabledReason),
nameof(TaskRowViewModel.PlanningDisabledReason),
nameof(TaskRowViewModel.MarkAsDisabledReason),
}, raised);
}
[Fact]
public void ParentTaskIdChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.ParentTaskId = "parent");
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.ParentTaskId),
nameof(TaskRowViewModel.IsChild),
nameof(TaskRowViewModel.IsAgentSuggested),
nameof(TaskRowViewModel.ShowAsChild),
nameof(TaskRowViewModel.IsDraft),
nameof(TaskRowViewModel.IsPlanned),
nameof(TaskRowViewModel.CanSendToQueue),
nameof(TaskRowViewModel.CanOpenPlanningSession),
nameof(TaskRowViewModel.SendToQueueDisabledReason),
nameof(TaskRowViewModel.PlanningDisabledReason),
}, raised);
}
[Fact]
public void ParentInViewChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.ParentInView = false);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.ParentInView),
nameof(TaskRowViewModel.ShowAsChild),
nameof(TaskRowViewModel.IsDraft),
nameof(TaskRowViewModel.IsPlanned),
}, raised);
}
[Fact]
public void CreatedByChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.CreatedBy = "someone");
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.CreatedBy),
nameof(TaskRowViewModel.IsAgentSuggested),
}, raised);
}
[Fact]
public void ParentFinalizedChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.ParentFinalized = true);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.ParentFinalized),
nameof(TaskRowViewModel.IsDraft),
nameof(TaskRowViewModel.IsPlanned),
nameof(TaskRowViewModel.CanSendToQueue),
nameof(TaskRowViewModel.SendToQueueDisabledReason),
}, raised);
}
[Fact]
public void PlanningPhaseChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.PlanningPhase = PlanningPhase.Active);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.PlanningPhase),
nameof(TaskRowViewModel.IsPlanningParent),
nameof(TaskRowViewModel.PlanningBadge),
nameof(TaskRowViewModel.ShowStatusChip),
nameof(TaskRowViewModel.IsPlanActive),
nameof(TaskRowViewModel.IsPlanFinalized),
nameof(TaskRowViewModel.CanOpenPlanningSession),
nameof(TaskRowViewModel.CanResumeOrDiscardPlanning),
nameof(TaskRowViewModel.CanQueuePlan),
nameof(TaskRowViewModel.CanSendToQueue),
nameof(TaskRowViewModel.CanFinalizePlanning),
nameof(TaskRowViewModel.CanRefine),
nameof(TaskRowViewModel.SendToQueueDisabledReason),
nameof(TaskRowViewModel.RefineDisabledReason),
nameof(TaskRowViewModel.PlanningDisabledReason),
}, raised);
}
[Fact]
public void IsRefiningChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.IsRefining = true);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.IsRefining),
nameof(TaskRowViewModel.CanRefine),
nameof(TaskRowViewModel.RefineDisabledReason),
}, raised);
}
[Fact]
public void IsManualChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.IsManual = true);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.IsManual),
nameof(TaskRowViewModel.ManualBadge),
nameof(TaskRowViewModel.CanRefine),
nameof(TaskRowViewModel.CanSendToQueue),
nameof(TaskRowViewModel.CanOpenPlanningSession),
nameof(TaskRowViewModel.SendToQueueDisabledReason),
nameof(TaskRowViewModel.RefineDisabledReason),
nameof(TaskRowViewModel.PlanningDisabledReason),
}, raised);
}
[Fact]
public void HasInteractiveSessionChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.HasInteractiveSession = true);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.HasInteractiveSession),
nameof(TaskRowViewModel.IsParked),
nameof(TaskRowViewModel.StatusChipClass),
nameof(TaskRowViewModel.StatusLabel),
nameof(TaskRowViewModel.ShowStatusChip),
nameof(TaskRowViewModel.StatusChipTooltip),
nameof(TaskRowViewModel.CanSendToQueue),
nameof(TaskRowViewModel.SendToQueueDisabledReason),
nameof(TaskRowViewModel.CanDeleteTask),
nameof(TaskRowViewModel.DeleteDisabledReason),
}, raised);
}
[Fact]
public void FailureReasonChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.FailureReason = "error");
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.FailureReason),
nameof(TaskRowViewModel.HasFailureReason),
nameof(TaskRowViewModel.FailureReasonLabel),
nameof(TaskRowViewModel.FailureReasonTooltip),
nameof(TaskRowViewModel.StatusChipTooltip),
}, raised);
}
[Fact]
public void FailureTurnsUsedChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.FailureTurnsUsed = 5);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.FailureTurnsUsed),
nameof(TaskRowViewModel.FailureReasonTooltip),
}, raised);
}
[Fact]
public void FailureMaxTurnsChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.FailureMaxTurns = 10);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.FailureMaxTurns),
nameof(TaskRowViewModel.FailureReasonTooltip),
}, raised);
}
[Fact]
public void HasQueuedSubtasksChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.HasQueuedSubtasks = true);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.HasQueuedSubtasks),
nameof(TaskRowViewModel.CanRemoveFromQueue),
nameof(TaskRowViewModel.CanSendToQueue),
nameof(TaskRowViewModel.CanQueuePlan),
nameof(TaskRowViewModel.SendToQueueDisabledReason),
}, raised);
}
[Fact]
public void IsHoveredChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.IsHovered = true);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.IsHovered),
nameof(TaskRowViewModel.ShowRowActions),
}, raised);
}
[Fact]
public void IsSelectedChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.IsSelected = true);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.IsSelected),
nameof(TaskRowViewModel.ShowRowActions),
}, raised);
}
[Fact]
public void BlockedByTaskIdChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.BlockedByTaskId = "blocker");
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.BlockedByTaskId),
nameof(TaskRowViewModel.IsQueued),
nameof(TaskRowViewModel.IsWaiting),
nameof(TaskRowViewModel.StatusChipClass),
nameof(TaskRowViewModel.CanRemoveFromQueue),
}, raised);
}
[Fact]
public void HasPlanningChildrenChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.HasPlanningChildren = true);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.HasPlanningChildren),
nameof(TaskRowViewModel.IsPlanningParent),
nameof(TaskRowViewModel.CanQueuePlan),
}, raised);
}
[Fact]
public void NumberChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.Number = 5);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.Number),
nameof(TaskRowViewModel.ShowNumberBadge),
}, raised);
}
[Fact]
public void BranchChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.Branch = "main");
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.Branch),
nameof(TaskRowViewModel.HasBranch),
}, raised);
}
[Fact]
public void WorktreePathChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.WorktreePath = @"C:\repo\worktrees\t");
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.WorktreePath),
nameof(TaskRowViewModel.CanOpenWorktree),
nameof(TaskRowViewModel.OpenWorktreeDisabledReason),
}, raised);
}
[Fact]
public void WorktreeStateChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.WorktreeState = ClaudeDo.Data.Models.WorktreeState.Active);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.WorktreeState),
nameof(TaskRowViewModel.IsParked),
nameof(TaskRowViewModel.StatusLabel),
}, raised);
}
[Fact]
public void DoneChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.Done = true);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.Done),
nameof(TaskRowViewModel.IsOverdue),
nameof(TaskRowViewModel.CanAddToMyDay),
}, raised);
}
[Fact]
public void IsMyDayChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.IsMyDay = true);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.IsMyDay),
nameof(TaskRowViewModel.CanAddToMyDay),
}, raised);
}
[Fact]
public void ScheduledForChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.ScheduledFor = DateTime.UtcNow);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.ScheduledFor),
nameof(TaskRowViewModel.IsOverdue),
nameof(TaskRowViewModel.HasSchedule),
}, raised);
}
[Fact]
public void DiffAdditionsChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.DiffAdditions = 5);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.DiffAdditions),
nameof(TaskRowViewModel.HasDiff),
nameof(TaskRowViewModel.DiffAdditionsText),
}, raised);
}
[Fact]
public void DiffDeletionsChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.DiffDeletions = 5);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.DiffDeletions),
nameof(TaskRowViewModel.HasDiff),
nameof(TaskRowViewModel.DiffDeletionsText),
}, raised);
}
[Fact]
public void RoadblockCountChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.RoadblockCount = 3);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.RoadblockCount),
nameof(TaskRowViewModel.HasRoadblock),
nameof(TaskRowViewModel.RoadblockTooltip),
}, raised);
}
[Fact]
public void DropHintAboveChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.DropHintAbove = true);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.DropHintAbove),
nameof(TaskRowViewModel.IsDropTarget),
}, raised);
}
[Fact]
public void DropHintBelowChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.DropHintBelow = true);
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.DropHintBelow),
nameof(TaskRowViewModel.IsDropTarget),
}, raised);
}
[Fact]
public void CreationPhaseChanged_Notifies_Dependents()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = Raised(vm, () => vm.CreationPhase = "creating_worktree");
Assert.Equal(new HashSet<string?>
{
nameof(TaskRowViewModel.CreationPhase),
nameof(TaskRowViewModel.HasCreationPhase),
nameof(TaskRowViewModel.CreationPhaseLabel),
}, raised);
}
}