feat(mission-control): show running Worker operations in a Running now section
Registry of active verify/merge/rebase/worktree-creation operations in the Worker, GetActiveOperations snapshot + OperationStarted/OperationEnded hub events, and a Running now section in Mission Control with ticking elapsed time. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
488c28a9d5
commit
2dbec543df
@@ -37,6 +37,8 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public event Action<string>? RefineStartedEvent;
|
||||
public event Action<string, bool, string?>? RefineFinishedEvent;
|
||||
public event Action<string, string, int, int>? OperationProgressEvent;
|
||||
public event Action<RunningOperationDto>? OperationStartedEvent;
|
||||
public event Action<string>? OperationEndedEvent;
|
||||
public event Action<string, string>? PlanningMergeStartedEvent;
|
||||
public event Action<string, string>? PlanningSubtaskMergedEvent;
|
||||
public event Action<string, string, IReadOnlyList<string>, bool>? PlanningMergeConflictEvent;
|
||||
@@ -67,6 +69,8 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public void RaisePlanningMergeConflict(string planningTaskId, string subtaskId, IReadOnlyList<string> files, bool externallyDriven)
|
||||
=> PlanningMergeConflictEvent?.Invoke(planningTaskId, subtaskId, files, externallyDriven);
|
||||
public void RaiseOperationProgress(string opKey, string phase, int current, int total) => OperationProgressEvent?.Invoke(opKey, phase, current, total);
|
||||
public void RaiseOperationStarted(RunningOperationDto op) => OperationStartedEvent?.Invoke(op);
|
||||
public void RaiseOperationEnded(string id) => OperationEndedEvent?.Invoke(id);
|
||||
public void RaisePlanningMergeStarted(string planningTaskId, string targetBranch) => PlanningMergeStartedEvent?.Invoke(planningTaskId, targetBranch);
|
||||
public void RaisePlanningMergeAborted(string planningTaskId, string? reason = null) => PlanningMergeAbortedEvent?.Invoke(planningTaskId, reason);
|
||||
public void RaisePlanningCompleted(string planningTaskId) => PlanningCompletedEvent?.Invoke(planningTaskId);
|
||||
@@ -156,6 +160,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public virtual Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public virtual Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0);
|
||||
public virtual Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId) => Task.FromResult<MergeTargetsDto?>(null);
|
||||
public virtual Task<IReadOnlyList<RunningOperationDto>> GetActiveOperationsAsync() => Task.FromResult<IReadOnlyList<RunningOperationDto>>(Array.Empty<RunningOperationDto>());
|
||||
public virtual Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync() => Task.FromResult<IReadOnlyList<WorkerLogEntry>>(Array.Empty<WorkerLogEntry>());
|
||||
public virtual Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId)
|
||||
=> Task.FromResult<IReadOnlyList<SubtaskDiffDto>>(Array.Empty<SubtaskDiffDto>());
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
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<ClaudeDoDbContext>().UseSqlite($"Data Source={_dbPath}").Options);
|
||||
|
||||
private sealed class FakeWorker : StubWorkerClient
|
||||
{
|
||||
public IReadOnlyList<RunningOperationDto> Snapshot { get; set; } = Array.Empty<RunningOperationDto>();
|
||||
public override Task<IReadOnlyList<RunningOperationDto>> GetActiveOperationsAsync() => Task.FromResult(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 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user