diff --git a/src/ClaudeDo.Worker/Runner/TaskRunner.cs b/src/ClaudeDo.Worker/Runner/TaskRunner.cs index 423ec493..a4712b48 100644 --- a/src/ClaudeDo.Worker/Runner/TaskRunner.cs +++ b/src/ClaudeDo.Worker/Runner/TaskRunner.cs @@ -311,6 +311,9 @@ public sealed class TaskRunner { var wtCtx = await _wtManager.CreateAsync(task, list, ct); await _broadcaster.WorkerLog($"Created worktree for \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow); + // The worktrees row was just inserted; without this the UI keeps showing the task + // as having no worktree until some unrelated event happens to refresh it. + await _broadcaster.WorktreeUpdated(task.Id); return new RunDirResult(wtCtx.WorktreePath, wtCtx, null); } catch (Exception ex) diff --git a/tests/ClaudeDo.Worker.Tests/Runner/QueueClaimTaskUpdatedBroadcastTests.cs b/tests/ClaudeDo.Worker.Tests/Runner/QueueClaimTaskUpdatedBroadcastTests.cs index ecee41dd..83b4e465 100644 --- a/tests/ClaudeDo.Worker.Tests/Runner/QueueClaimTaskUpdatedBroadcastTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Runner/QueueClaimTaskUpdatedBroadcastTests.cs @@ -121,4 +121,52 @@ public sealed class QueueClaimTaskUpdatedBroadcastTests : IDisposable releaseProcess.TrySetResult(); await runTask; } + + [Fact] + public async Task Creating_a_worktree_broadcasts_WorktreeUpdated() + { + string listId = Guid.NewGuid().ToString(), taskId = Guid.NewGuid().ToString(); + var repoDir = Path.Combine(_tempDir, "repo"); + Directory.CreateDirectory(repoDir); + + // A real git repo — Worker.Tests run real git by design. + await RunGitAsync(repoDir, "init"); + await RunGitAsync(repoDir, "config user.email t@t.t"); + await RunGitAsync(repoDir, "config user.name t"); + await File.WriteAllTextAsync(Path.Combine(repoDir, "a.txt"), "hi"); + await RunGitAsync(repoDir, "add a.txt"); + await RunGitAsync(repoDir, "commit -m init"); + + using (var ctx = _db.CreateContext()) + { + ctx.Lists.Add(new ListEntity { Id = listId, Name = "L", WorkingDir = repoDir, CreatedAt = DateTime.UtcNow }); + ctx.Tasks.Add(new TaskEntity + { + Id = taskId, ListId = listId, Title = "T", Status = TaskStatus.Running, + StartedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow, + }); + await ctx.SaveChangesAsync(); + } + + var fake = new FakeClaudeProcess((_, _, _, _, _) => + Task.FromResult(new RunResult { ExitCode = 0, ResultMarkdown = "ok" })); + var runner = BuildRunner(fake); + + using (var ctx = _db.CreateContext()) + await runner.RunAsync((await new TaskRepository(ctx).GetByIdAsync(taskId))!, "queue", + CancellationToken.None, alreadyClaimed: true); + + Assert.Contains(_hubContext.Proxy.Calls, + c => c.Method == "WorktreeUpdated" && (string)c.Args[0]! == taskId); + } + + private static async Task RunGitAsync(string dir, string args) + { + var psi = new System.Diagnostics.ProcessStartInfo("git", args) + { + WorkingDirectory = dir, RedirectStandardOutput = true, RedirectStandardError = true, + }; + using var p = System.Diagnostics.Process.Start(psi)!; + await p.WaitForExitAsync(); + } }