Files
ClaudeDo/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlRunningOperationsTests.cs
T
mika kunsandClaude Sonnet 5.5 46e9303da6 fix(mission-control): show post-merge verify as Verifying instead of In review
Derived from the VerifyMerge entry in RunningOperations (no new TaskStatus). The Running now row stays, reworded to 'Verifying after merge'.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 14:50:55 +02:00

169 lines
6.2 KiB
C#

using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace ClaudeDo.Ui.Tests.ViewModels;
public class MissionControlRunningOperationsTests : IDisposable
{
private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_mcops_{Guid.NewGuid():N}.db");
public MissionControlRunningOperationsTests()
{
using var ctx = NewContext();
ctx.Database.EnsureCreated();
}
public void Dispose()
{
foreach (var suffix in new[] { "", "-wal", "-shm" })
try { File.Delete(_dbPath + suffix); } catch { }
}
private ClaudeDoDbContext NewContext() =>
new(new DbContextOptionsBuilder<ClaudeDoDbContext>().UseSqlite($"Data Source={_dbPath}").Options);
private sealed class FakeWorker : StubWorkerClient
{
public IReadOnlyList<RunningOperationDto> Snapshot { get; set; } = Array.Empty<RunningOperationDto>();
/// <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) =>
new(id, kind, number is null ? null : "t-" + id, number, listId, title, DateTime.UtcNow.AddSeconds(-5));
private MissionControlViewModel BuildVm(FakeWorker worker) =>
new(new TestDbFactory(NewContext), worker, new UsagePillViewModel(worker));
[Fact]
public async Task Snapshot_LoadsRunningOperations_AndShowsActivity()
{
var worker = new FakeWorker
{
Snapshot = new[]
{
Op("a", OperationKinds.VerifyMerge, 12, "Fix login"),
Op("b", OperationKinds.VerifyBatch, null, "My list", listId: "l1"),
},
};
var vm = BuildVm(worker);
await vm.RefreshOperationsAsync();
Assert.Equal(new[] { "#12 Fix login", "My list" }, vm.RunningOperations.Select(r => r.Subject));
Assert.True(vm.HasRunningOperations);
Assert.True(vm.HasActivity);
}
[Fact]
public async Task StartedAndEndedEvents_UpdateTheList()
{
var worker = new FakeWorker();
var vm = BuildVm(worker);
await vm.RefreshOperationsAsync();
Assert.False(vm.HasRunningOperations);
worker.RaiseOperationStarted(Op("a", OperationKinds.Merging, 3, "Task"));
worker.RaiseOperationStarted(Op("a", OperationKinds.Merging, 3, "Task")); // duplicate ignored
worker.RaiseOperationStarted(Op("b", OperationKinds.Rebasing, 4, "Other"));
Assert.Equal(2, vm.RunningOperations.Count);
worker.RaiseOperationEnded("a");
Assert.Equal("b", Assert.Single(vm.RunningOperations).Id);
worker.RaiseOperationEnded("unknown");
worker.RaiseOperationEnded("b");
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]
public async Task ConnectionRestored_ReplacesListWithFreshSnapshot()
{
var worker = new FakeWorker { Snapshot = new[] { Op("a", OperationKinds.Merging, 1, "Old") } };
var vm = BuildVm(worker);
await vm.RefreshOperationsAsync();
worker.Snapshot = new[] { Op("c", OperationKinds.CreatingWorktree, 2, "New") };
await vm.RefreshOperationsAsync();
Assert.Equal("c", Assert.Single(vm.RunningOperations).Id);
}
[Fact]
public async Task VerifyMergeOperation_ShowsReviewTaskAsVerifying_UntilItEnds()
{
await using (var db = NewContext())
{
db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "rev1", ListId = "L1", Title = "review", Status = ClaudeDo.Data.Models.TaskStatus.WaitingForReview, CreatedAt = DateTime.UtcNow });
await db.SaveChangesAsync();
}
var worker = new FakeWorker();
using var vm = BuildVm(worker);
await vm.RefreshQueueAsync();
Assert.Equal(Loc.T("missionControl.review"), vm.Queued.Single().StatusLabel);
worker.RaiseOperationStarted(new RunningOperationDto("v1", OperationKinds.VerifyMerge, "rev1", 1, "L1", "review", DateTime.UtcNow));
await vm.RefreshQueueAsync();
Assert.Equal(Loc.T("missionControl.verifying"), vm.Queued.Single().StatusLabel);
Assert.Equal("StatusRunning", vm.Queued.Single().StatusColorKey);
worker.RaiseOperationEnded("v1");
await vm.RefreshQueueAsync();
Assert.Equal(Loc.T("missionControl.review"), vm.Queued.Single().StatusLabel);
}
[Fact]
public async Task TickOperations_UpdatesElapsed()
{
var worker = new FakeWorker();
var vm = BuildVm(worker);
var started = new DateTime(2026, 10, 1, 8, 0, 0, DateTimeKind.Utc);
worker.RaiseOperationStarted(new RunningOperationDto("a", OperationKinds.VerifyMerge, "t", 1, null, "T", started));
vm.TickOperations(started.AddSeconds(75));
Assert.Equal("01:15", vm.RunningOperations.Single().Elapsed);
await Task.CompletedTask;
}
}