* TaskRepository.UpdateAsync defensively detaches any locally tracked entity with the same Id before attaching the patched copy, preventing EF identity conflicts when callers load via AsNoTracking and write back through the same DbContext (surfaced by ExternalMcpService UpdateTask integration tests). * TasksIslandViewModel auto-collapse now only fires for Finalized planning parents that are not yet Done. Active-phase parents stay expanded while the user is editing the plan, and Done parents stay expanded so all completed children land in CompletedItems alongside the parent. * Update three Ui.Tests fakes (ConflictResolution, PlanningDiff, DetailsIslandPlanning) to implement the two new IWorkerClient members (OpenInteractiveTerminalAsync, QueuePlanningSubtasksAsync). * Rewrite StreamLineFormatterTests to exercise the current assistant/user/result/system message format instead of the legacy stream_event parsing that was removed in the formatter rewrite. * Align AppSettingsRepository seed-default assertion with the permission-mode default that flipped from bypassPermissions to auto. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
166 lines
7.1 KiB
C#
166 lines
7.1 KiB
C#
using System.ComponentModel;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Ui.Services;
|
|
using ClaudeDo.Ui.ViewModels.Planning;
|
|
|
|
namespace ClaudeDo.Ui.Tests.ViewModels;
|
|
|
|
public class PlanningDiffViewModelTests
|
|
{
|
|
private sealed class FakePlanningWorker : IWorkerClient
|
|
{
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
public event Action<string, string, DateTime>? TaskStartedEvent;
|
|
public event Action<string, string, string, DateTime>? TaskFinishedEvent;
|
|
public event Action<string>? TaskUpdatedEvent;
|
|
public event Action<string>? WorktreeUpdatedEvent;
|
|
public event Action<string, string>? TaskMessageEvent;
|
|
public event Action<string, string>? PlanningMergeStartedEvent;
|
|
public event Action<string, string>? PlanningSubtaskMergedEvent;
|
|
public event Action<string, string, IReadOnlyList<string>>? PlanningMergeConflictEvent;
|
|
public event Action<string>? PlanningMergeAbortedEvent;
|
|
public event Action<string>? PlanningCompletedEvent;
|
|
|
|
public bool IsConnected => false;
|
|
|
|
public IReadOnlyList<SubtaskDiffDto> AggregateResult { get; set; } = Array.Empty<SubtaskDiffDto>();
|
|
public CombinedDiffResultDto? CombinedResult { get; set; }
|
|
|
|
public Task WakeQueueAsync() => Task.CompletedTask;
|
|
public Task RunNowAsync(string taskId) => Task.CompletedTask;
|
|
public Task ContinueTaskAsync(string taskId, string followUpPrompt) => Task.CompletedTask;
|
|
public Task ResetTaskAsync(string taskId) => Task.CompletedTask;
|
|
public Task CancelTaskAsync(string taskId) => Task.CompletedTask;
|
|
public Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
|
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
|
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
|
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
|
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
|
public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
|
public Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) => Task.CompletedTask;
|
|
public Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0);
|
|
public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId) => Task.FromResult<MergeTargetsDto?>(null);
|
|
public Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId) =>
|
|
Task.FromResult(AggregateResult);
|
|
public Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) =>
|
|
Task.FromResult(CombinedResult);
|
|
public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask;
|
|
public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask;
|
|
public Task AbortPlanningMergeAsync(string planningTaskId) => Task.CompletedTask;
|
|
public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
|
public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InitializeAsync_PopulatesSubtasks()
|
|
{
|
|
var fake = new FakePlanningWorker
|
|
{
|
|
AggregateResult = new[]
|
|
{
|
|
new SubtaskDiffDto("s1", "First", "branch-1", "base1", "head1", "+1 -0", "diff1"),
|
|
new SubtaskDiffDto("s2", "Second", "branch-2", "base2", "head2", "+2 -1", "diff2"),
|
|
}
|
|
};
|
|
var vm = new PlanningDiffViewModel(fake, "plan-1", "main");
|
|
|
|
await vm.InitializeAsync();
|
|
|
|
Assert.Equal(2, vm.Subtasks.Count);
|
|
Assert.Equal(vm.Subtasks[0], vm.SelectedSubtask);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectingSubtask_InGroupedMode_SetsDisplayedDiff()
|
|
{
|
|
var fake = new FakePlanningWorker
|
|
{
|
|
AggregateResult = new[]
|
|
{
|
|
new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A"),
|
|
new SubtaskDiffDto("s2", "Second", "b2", "base2", "head2", null, "DIFF-B"),
|
|
}
|
|
};
|
|
var vm = new PlanningDiffViewModel(fake, "plan-1", "main");
|
|
await vm.InitializeAsync();
|
|
|
|
vm.SelectedSubtask = vm.Subtasks[1];
|
|
|
|
Assert.Equal("DIFF-B", vm.DisplayedDiff);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ToggleCombined_Success_DisplaysUnifiedDiff()
|
|
{
|
|
var fake = new FakePlanningWorker
|
|
{
|
|
AggregateResult = new[]
|
|
{
|
|
new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A"),
|
|
},
|
|
CombinedResult = new CombinedDiffResultDto(true, "integration-branch", "COMBINED-DIFF", null, null),
|
|
};
|
|
var vm = new PlanningDiffViewModel(fake, "plan-1", "main");
|
|
await vm.InitializeAsync();
|
|
|
|
vm.IsCombinedMode = true;
|
|
|
|
// Wait for the async toggle command to complete
|
|
var deadline = DateTime.UtcNow.AddSeconds(5);
|
|
while (DateTime.UtcNow < deadline && vm.IsLoadingCombined)
|
|
await Task.Delay(10);
|
|
|
|
Assert.Equal("COMBINED-DIFF", vm.DisplayedDiff);
|
|
Assert.Null(vm.CombinedWarning);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ToggleCombined_Conflict_ShowsWarning()
|
|
{
|
|
var fake = new FakePlanningWorker
|
|
{
|
|
AggregateResult = new[]
|
|
{
|
|
new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A"),
|
|
},
|
|
CombinedResult = new CombinedDiffResultDto(false, null, null, "subtask-42", new[] { "a.cs", "b.cs" }),
|
|
};
|
|
var vm = new PlanningDiffViewModel(fake, "plan-1", "main");
|
|
await vm.InitializeAsync();
|
|
|
|
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("subtask-42", vm.CombinedWarning);
|
|
Assert.Contains("2 files", vm.CombinedWarning);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ToggleCombined_HubReturnsNull_ShowsError()
|
|
{
|
|
var fake = new FakePlanningWorker
|
|
{
|
|
AggregateResult = new[]
|
|
{
|
|
new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A"),
|
|
},
|
|
CombinedResult = null,
|
|
};
|
|
var vm = new PlanningDiffViewModel(fake, "plan-1", "main");
|
|
await vm.InitializeAsync();
|
|
|
|
vm.IsCombinedMode = true;
|
|
|
|
var deadline = DateTime.UtcNow.AddSeconds(5);
|
|
while (DateTime.UtcNow < deadline && vm.IsLoadingCombined)
|
|
await Task.Delay(10);
|
|
|
|
Assert.NotNull(vm.CombinedWarning);
|
|
Assert.NotEmpty(vm.CombinedWarning!);
|
|
}
|
|
}
|