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:
mika kuns
2026-10-01 12:08:43 +02:00
co-authored by Claude Sonnet 5.5
parent 488c28a9d5
commit 2dbec543df
18 changed files with 539 additions and 18 deletions
@@ -31,7 +31,7 @@ public class TaskMergeServiceTests : IDisposable
}
private static (TaskMergeService svc, MergeRecordingClientProxy proxy) BuildService(
DbFixture db, IVerifyCommandRunner? verify = null)
DbFixture db, IVerifyCommandRunner? verify = null, RunningOperationRegistry? operations = null)
{
var fakeHub = new MergeRecordingHubContext();
var broadcaster = new HubBroadcaster(fakeHub);
@@ -42,7 +42,8 @@ public class TaskMergeServiceTests : IDisposable
broadcaster,
state,
verify ?? new VerifyCommandRunner(),
NullLogger<TaskMergeService>.Instance);
NullLogger<TaskMergeService>.Instance,
operations: operations);
return (svc, fakeHub.Proxy);
}
@@ -1335,6 +1336,62 @@ public class TaskMergeServiceTests : IDisposable
Assert.Equal(TaskStatus.WaitingForReview, updated!.Status);
}
[Fact]
public async Task RunningOperations_VerifyMerge_PresentDuringRunAndRemovedAfterSuccess()
{
var db = NewDb();
var (list, task) = await SeedListAndTask(db, workingDir: "/tmp", status: TaskStatus.WaitingForReview);
await SeedVerifyCommand(db, list.Id, "dotnet test");
var ops = new RunningOperationRegistry();
IReadOnlyList<RunningOperationDto>? during = null;
var fakeVerify = new FakeVerifyCommandRunner { OnRun = () => during = ops.Snapshot() };
var (svc, _) = BuildService(db, fakeVerify, ops);
await svc.ApproveAndMergeAsync(task.Id, "main", CancellationToken.None);
var op = Assert.Single(during!);
Assert.Equal(OperationKinds.VerifyMerge, op.Kind);
Assert.Equal(task.Id, op.TaskId);
Assert.Equal("merge-task", op.Title);
Assert.Empty(ops.Snapshot());
}
[Fact]
public async Task RunningOperations_VerifyMerge_RemovedWhenRunnerThrows()
{
var db = NewDb();
var (list, task) = await SeedListAndTask(db, workingDir: "/tmp", status: TaskStatus.WaitingForReview);
await SeedVerifyCommand(db, list.Id, "dotnet test");
var ops = new RunningOperationRegistry();
var fakeVerify = new FakeVerifyCommandRunner { ThrowOnRun = new InvalidOperationException("boom") };
var (svc, _) = BuildService(db, fakeVerify, ops);
var result = await svc.ApproveAndMergeAsync(task.Id, "main", CancellationToken.None);
Assert.Equal(TaskMergeService.StatusVerifyFailed, result.Status);
Assert.Empty(ops.Snapshot());
}
[Fact]
public async Task RunningOperations_VerifyBatch_PresentDuringRunAndRemovedAfter()
{
var db = NewDb();
var (list, task) = await SeedListAndTask(db, workingDir: "/tmp", status: TaskStatus.WaitingForReview);
await SeedVerifyCommand(db, list.Id, "dotnet test");
var ops = new RunningOperationRegistry();
IReadOnlyList<RunningOperationDto>? during = null;
var fakeVerify = new FakeVerifyCommandRunner { OnRun = () => during = ops.Snapshot() };
var (svc, _) = BuildService(db, fakeVerify, ops);
await svc.RunListVerifyAsync(list.Id, new[] { task.Id }, CancellationToken.None);
var op = Assert.Single(during!);
Assert.Equal(OperationKinds.VerifyBatch, op.Kind);
Assert.Equal(list.Id, op.ListId);
Assert.Equal("merge-test", op.Title);
Assert.Empty(ops.Snapshot());
}
[Fact]
public async Task ApproveAndMergeAsync_NoWorktreeAndVerifyPasses_MarksDone()
{
@@ -2208,9 +2265,15 @@ internal sealed class FakeVerifyCommandRunner : IVerifyCommandRunner
// report without the real verify command ever taking that long.
public TimeSpan? Delay { get; set; }
// Invoked at call time -- lets a test observe state while the verify run is "in flight".
public Action? OnRun { get; set; }
public Exception? ThrowOnRun { get; set; }
public async Task<VerifyCommandResult> RunAsync(string workingDir, string command, TimeSpan timeout, CancellationToken ct)
{
CallCount++;
OnRun?.Invoke();
if (ThrowOnRun is not null) throw ThrowOnRun;
CapturedWorkingDir = workingDir;
CapturedCommand = command;
if (FileToCheck is not null)