using ClaudeDo.Data; 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().UseSqlite($"Data Source={_dbPath}").Options); private sealed class FakeWorker : StubWorkerClient { public IReadOnlyList Snapshot { get; set; } = Array.Empty(); /// When set, the snapshot call completes only once this source is resolved. public TaskCompletionSource? Gate { get; set; } public override async Task> 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 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; } }