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
@@ -0,0 +1,55 @@
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.Lifecycle;
|
||||
using ClaudeDo.Worker.Tests.Services;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Lifecycle;
|
||||
|
||||
public class RunningOperationRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Begin_AddsEntry_DisposeRemovesIt_AndIsIdempotent()
|
||||
{
|
||||
var ops = new RunningOperationRegistry();
|
||||
|
||||
var scope = ops.Begin(OperationKinds.Merging, "title", taskId: "t1", taskNumber: 7);
|
||||
var op = Assert.Single(ops.Snapshot());
|
||||
Assert.Equal(OperationKinds.Merging, op.Kind);
|
||||
Assert.Equal("t1", op.TaskId);
|
||||
Assert.Equal(7, op.TaskNumber);
|
||||
|
||||
scope.Dispose();
|
||||
scope.Dispose();
|
||||
Assert.Empty(ops.Snapshot());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Entry_IsRemovedWhenTheOperationThrows()
|
||||
{
|
||||
var ops = new RunningOperationRegistry();
|
||||
|
||||
Action act = () =>
|
||||
{
|
||||
using var _ = ops.Begin(OperationKinds.Rebasing, "title", taskId: "t1");
|
||||
Assert.Single(ops.Snapshot());
|
||||
throw new InvalidOperationException("boom");
|
||||
};
|
||||
Assert.Throws<InvalidOperationException>(act);
|
||||
|
||||
Assert.Empty(ops.Snapshot());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BeginAndDispose_BroadcastStartedAndEnded()
|
||||
{
|
||||
var hub = new MergeRecordingHubContext();
|
||||
var ops = new RunningOperationRegistry(new HubBroadcaster(hub));
|
||||
|
||||
var scope = ops.Begin(OperationKinds.CreatingWorktree, "title", taskId: "t1");
|
||||
var id = ops.Snapshot().Single().Id;
|
||||
scope.Dispose();
|
||||
|
||||
Assert.Collection(hub.Proxy.Calls,
|
||||
c => { Assert.Equal("OperationStarted", c.Method); Assert.Equal(id, ((RunningOperationDto)c.Args[0]!).Id); },
|
||||
c => { Assert.Equal("OperationEnded", c.Method); Assert.Equal(id, c.Args[0]); });
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user