fix(claude-do): merge Fix ghost rows in Mission Control's "Running now" section (s
ClaudeDo-Task: 7b02a754-9472-4e2c-a6bc-02e50013eddb
This commit is contained in:
@@ -158,22 +158,26 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
var snapshot = await _worker.GetActiveOperationsAsync();
|
var snapshot = await _worker.GetActiveOperationsAsync();
|
||||||
RunningOperations.Clear();
|
RunningOperations.Clear();
|
||||||
foreach (var op in snapshot.OrderBy(o => o.StartedAt))
|
foreach (var op in snapshot.Where(o => !_endedOperationIds.Contains(o.Id)).OrderBy(o => o.StartedAt))
|
||||||
RunningOperations.Add(new RunningOperationViewModel(op));
|
RunningOperations.Add(new RunningOperationViewModel(op));
|
||||||
NotifyOperationsChanged();
|
NotifyOperationsChanged();
|
||||||
}
|
}
|
||||||
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("missionControl.operationsRefreshFailed", ex.Message)); }
|
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("missionControl.operationsRefreshFailed", ex.Message)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Ids already reported ended; guards against a late Started or a stale snapshot re-adding them. Op ids are unique, so it is never pruned (a few bytes per op).</summary>
|
||||||
|
private readonly HashSet<string> _endedOperationIds = new();
|
||||||
|
|
||||||
private void AddOperation(RunningOperationDto op)
|
private void AddOperation(RunningOperationDto op)
|
||||||
{
|
{
|
||||||
if (RunningOperations.Any(r => r.Id == op.Id)) return;
|
if (_endedOperationIds.Contains(op.Id) || RunningOperations.Any(r => r.Id == op.Id)) return;
|
||||||
RunningOperations.Add(new RunningOperationViewModel(op));
|
RunningOperations.Add(new RunningOperationViewModel(op));
|
||||||
NotifyOperationsChanged();
|
NotifyOperationsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveOperation(string id)
|
private void RemoveOperation(string id)
|
||||||
{
|
{
|
||||||
|
_endedOperationIds.Add(id);
|
||||||
var row = RunningOperations.FirstOrDefault(r => r.Id == id);
|
var row = RunningOperations.FirstOrDefault(r => r.Id == id);
|
||||||
if (row is null) return;
|
if (row is null) return;
|
||||||
RunningOperations.Remove(row);
|
RunningOperations.Remove(row);
|
||||||
|
|||||||
@@ -28,7 +28,13 @@ public class MissionControlRunningOperationsTests : IDisposable
|
|||||||
private sealed class FakeWorker : StubWorkerClient
|
private sealed class FakeWorker : StubWorkerClient
|
||||||
{
|
{
|
||||||
public IReadOnlyList<RunningOperationDto> Snapshot { get; set; } = Array.Empty<RunningOperationDto>();
|
public IReadOnlyList<RunningOperationDto> Snapshot { get; set; } = Array.Empty<RunningOperationDto>();
|
||||||
public override Task<IReadOnlyList<RunningOperationDto>> GetActiveOperationsAsync() => Task.FromResult(Snapshot);
|
/// <summary>When set, the snapshot call completes only once this source is resolved.</summary>
|
||||||
|
public TaskCompletionSource? Gate { get; set; }
|
||||||
|
public override async Task<IReadOnlyList<RunningOperationDto>> GetActiveOperationsAsync()
|
||||||
|
{
|
||||||
|
if (Gate is not null) await Gate.Task;
|
||||||
|
return Snapshot;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RunningOperationDto Op(string id, string kind, int? number, string title, string? listId = null) =>
|
private static RunningOperationDto Op(string id, string kind, int? number, string title, string? listId = null) =>
|
||||||
@@ -78,6 +84,35 @@ public class MissionControlRunningOperationsTests : IDisposable
|
|||||||
Assert.False(vm.HasRunningOperations);
|
Assert.False(vm.HasRunningOperations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EndedDuringPendingSnapshot_DoesNotReappear()
|
||||||
|
{
|
||||||
|
var gate = new TaskCompletionSource();
|
||||||
|
var worker = new FakeWorker { Snapshot = new[] { Op("a", OperationKinds.Merging, 1, "Task") }, Gate = gate };
|
||||||
|
var vm = BuildVm(worker);
|
||||||
|
|
||||||
|
var refresh = vm.RefreshOperationsAsync();
|
||||||
|
worker.RaiseOperationEnded("a");
|
||||||
|
gate.SetResult();
|
||||||
|
await refresh;
|
||||||
|
|
||||||
|
Assert.Empty(vm.RunningOperations);
|
||||||
|
Assert.False(vm.HasRunningOperations);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EndedBeforeStarted_LeavesNoRow()
|
||||||
|
{
|
||||||
|
var worker = new FakeWorker();
|
||||||
|
var vm = BuildVm(worker);
|
||||||
|
await vm.RefreshOperationsAsync();
|
||||||
|
|
||||||
|
worker.RaiseOperationEnded("a");
|
||||||
|
worker.RaiseOperationStarted(Op("a", OperationKinds.Merging, 1, "Task"));
|
||||||
|
|
||||||
|
Assert.Empty(vm.RunningOperations);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ConnectionRestored_ReplacesListWithFreshSnapshot()
|
public async Task ConnectionRestored_ReplacesListWithFreshSnapshot()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user