feat(worker): report worktree-creation phase on OperationProgress
Fills the silent gap between Queued and the first agent output: WorktreeManager now broadcasts a "creating_worktree" phase (before the initial git worktree add and again for the self-heal retry section) through the existing OperationProgress channel, and the task row shows it via the pre-existing but unused ops.worker.creatingWorktree locale key until the next entity refresh clears it. Confirmed the 2026-08-07 triage finding still holds: TaskRunner already broadcasts WorktreeUpdated right after WorktreeManager.CreateAsync (TaskRunner.cs:342-346, :501) -- no second broadcast added there.
This commit is contained in:
@@ -3,6 +3,7 @@ using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Config;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.Runner;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@@ -76,6 +77,73 @@ public class WorktreeManagerTests : IDisposable
|
||||
Assert.Null(row.HeadCommit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_BroadcastsCreatingWorktreePhase()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var repo = CreateRepo();
|
||||
var (task, list) = MakeEntities(repo.RepoDir);
|
||||
var db = new DbFixture();
|
||||
_dbFixtures.Add(db);
|
||||
using (var seedCtx = db.CreateContext())
|
||||
{
|
||||
await new ListRepository(seedCtx).AddAsync(list);
|
||||
await new TaskRepository(seedCtx).AddAsync(task);
|
||||
}
|
||||
|
||||
var fakeHub = new CapturingHubContext();
|
||||
var broadcaster = new HubBroadcaster(fakeHub);
|
||||
var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
|
||||
var mgr = new WorktreeManager(
|
||||
new GitService(), db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance, broadcaster);
|
||||
|
||||
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
|
||||
|
||||
var progressCalls = fakeHub.Proxy.Calls.Where(c => c.Method == "OperationProgress").ToList();
|
||||
Assert.Single(progressCalls);
|
||||
Assert.Equal(task.Id, (string?)progressCalls[0].Args[0]);
|
||||
Assert.Equal(WorktreeManager.PhaseCreatingWorktree, (string?)progressCalls[0].Args[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_SelfHealPath_BroadcastsCreatingWorktreePhaseForEachSection()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var repo = CreateRepo();
|
||||
var (task, list) = MakeEntities(repo.RepoDir);
|
||||
var db = new DbFixture();
|
||||
_dbFixtures.Add(db);
|
||||
using (var seedCtx = db.CreateContext())
|
||||
{
|
||||
await new ListRepository(seedCtx).AddAsync(list);
|
||||
await new TaskRepository(seedCtx).AddAsync(task);
|
||||
}
|
||||
|
||||
// Pre-create the branch so `git worktree add -b` hits the "already exists" self-heal path.
|
||||
var branchName = $"claudedo/{task.Id.Replace("-", "")}";
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", branchName, repo.BaseCommit);
|
||||
|
||||
var fakeHub = new CapturingHubContext();
|
||||
var broadcaster = new HubBroadcaster(fakeHub);
|
||||
var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
|
||||
var mgr = new WorktreeManager(
|
||||
new GitService(), db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance, broadcaster);
|
||||
|
||||
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
|
||||
|
||||
// One message for the initial attempt, one for the self-heal retry section -- not a tick stream.
|
||||
var progressCalls = fakeHub.Proxy.Calls
|
||||
.Where(c => c.Method == "OperationProgress"
|
||||
&& (string?)c.Args[0] == task.Id
|
||||
&& (string?)c.Args[1] == WorktreeManager.PhaseCreatingWorktree)
|
||||
.ToList();
|
||||
Assert.Equal(2, progressCalls.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CommitIfChangedAsync_NoChanges_HeadCommitStaysNull()
|
||||
{
|
||||
|
||||
@@ -41,6 +41,7 @@ sealed class FakeWorkerClient : IWorkerClient
|
||||
public void RaiseTaskUpdated(string taskId) => TaskUpdatedEvent?.Invoke(taskId);
|
||||
public void RaiseWorktreeUpdated(string taskId) => WorktreeUpdatedEvent?.Invoke(taskId);
|
||||
public void RaiseTaskMessage(string taskId, string line) => TaskMessageEvent?.Invoke(taskId, line);
|
||||
public void RaiseOperationProgress(string opKey, string phase, int current, int total) => OperationProgressEvent?.Invoke(opKey, phase, current, total);
|
||||
|
||||
public Task RunNowAsync(string taskId) => Task.CompletedTask;
|
||||
public Task ContinueTaskAsync(string taskId, string followUpPrompt) => Task.CompletedTask;
|
||||
@@ -277,6 +278,32 @@ public class TasksIslandViewModelPlanningTests
|
||||
Assert.Equal(("t1", false), raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OperationProgress_CreatingWorktreePhase_SetsOnlyMatchingRow()
|
||||
{
|
||||
var row1 = MakeRow("t1", TaskStatus.Running);
|
||||
var row2 = MakeRow("t2", TaskStatus.Running);
|
||||
var (_, worker) = VmFactory.Create([row1, row2]);
|
||||
|
||||
worker.RaiseOperationProgress("t1", "creating_worktree", 0, 0);
|
||||
|
||||
Assert.True(row1.HasCreationPhase);
|
||||
Assert.False(row2.HasCreationPhase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OperationProgress_UnrelatedPhase_DoesNotSetCreationPhase()
|
||||
{
|
||||
// Same channel carries TaskMergeService's merge phases (opKey is also the task id) --
|
||||
// the row must only react to its own creation-phase token, not any phase for its id.
|
||||
var row = MakeRow("t1", TaskStatus.Running);
|
||||
var (_, worker) = VmFactory.Create([row]);
|
||||
|
||||
worker.RaiseOperationProgress("t1", "merging", 0, 0);
|
||||
|
||||
Assert.False(row.HasCreationPhase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToggleExpand_TogglesParentExpansion()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user