test(worker): cover WorktreeUpdated broadcast on interactive review submission

This commit is contained in:
mika kuns
2026-09-30 08:14:26 +02:00
parent 2558e02ba3
commit 52ba538339
@@ -0,0 +1,125 @@
using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Tests.Lifecycle;
// The detail pane gates its whole REVIEW/MERGE block (Open diff included) on the worktree row it
// last read, and it only re-reads that on WorktreeUpdated -- TaskUpdated refreshes the task row
// alone. An interactive session creates/commits its worktree entirely outside the autonomous
// pipeline, so without a WorktreeUpdated here the task lands in WaitingForReview with no diff
// button at all until it is re-selected.
public sealed class InteractiveReviewSubmissionServiceTests : IDisposable
{
private readonly DbFixture _db = new();
private readonly ClaudeDoDbContext _ctx;
private readonly List<GitRepoFixture> _repos = new();
private readonly CapturingHubContext _hub = new();
public InteractiveReviewSubmissionServiceTests() => _ctx = _db.CreateContext();
public void Dispose()
{
foreach (var r in _repos) r.Dispose();
_ctx.Dispose();
_db.Dispose();
}
private GitRepoFixture CreateRepo()
{
var f = new GitRepoFixture();
_repos.Add(f);
return f;
}
private InteractiveReviewSubmissionService BuildSut()
{
var cfg = new WorkerConfig
{
SandboxRoot = Path.Combine(Path.GetTempPath(), $"cd_{Guid.NewGuid():N}"),
LogRoot = Path.Combine(Path.GetTempPath(), $"cdl_{Guid.NewGuid():N}"),
};
var git = new GitService();
var dbFactory = _db.CreateFactory();
var wtManager = new WorktreeManager(git, dbFactory, cfg, NullLogger<WorktreeManager>.Instance);
var state = TaskStateServiceBuilder.Build(dbFactory).State;
return new InteractiveReviewSubmissionService(
dbFactory, git, state, wtManager, new HubBroadcaster(_hub));
}
private async Task<(TaskEntity Task, ListEntity List)> SeedAsync(string workingDir, string? handlerBaseCommit)
{
var lists = new ListRepository(_ctx);
var list = new ListEntity { Id = Guid.NewGuid().ToString(), Name = "L", WorkingDir = workingDir, CreatedAt = DateTime.UtcNow };
await lists.AddAsync(list);
var task = new TaskEntity
{
Id = Guid.NewGuid().ToString(), ListId = list.Id, Title = "t",
Status = TaskStatus.Idle, HandlerBaseCommit = handlerBaseCommit,
CreatedAt = DateTime.UtcNow, CommitType = "chore",
};
await new TaskRepository(_ctx).AddAsync(task);
return (task, list);
}
private bool BroadcastWorktreeUpdatedFor(string taskId) =>
_hub.Proxy.Calls.Any(c => c.Method == "WorktreeUpdated" && c.Args.Length > 0 && (string?)c.Args[0] == taskId);
[Fact]
public async Task Submit_WorktreeTask_BroadcastsWorktreeUpdated()
{
if (!GitRepoFixture.IsGitAvailable()) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
var (task, list) = await SeedAsync(repo.RepoDir, handlerBaseCommit: null);
var sut = BuildSut();
var cfg = new WorkerConfig
{
SandboxRoot = Path.Combine(Path.GetTempPath(), $"cd_{Guid.NewGuid():N}"),
LogRoot = Path.Combine(Path.GetTempPath(), $"cdl_{Guid.NewGuid():N}"),
};
var wtManager = new WorktreeManager(new GitService(), _db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);
var wt = await wtManager.CreateAsync(task, list, CancellationToken.None);
File.WriteAllText(Path.Combine(wt.WorktreePath, "hand-driven.txt"), "content");
await sut.SubmitAsync(task.Id, CancellationToken.None);
Assert.True(BroadcastWorktreeUpdatedFor(task.Id));
}
[Fact]
public async Task Submit_WorktreeLessHandlerTask_BroadcastsWorktreeUpdated()
{
if (!GitRepoFixture.IsGitAvailable()) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
var (task, _) = await SeedAsync(repo.RepoDir, handlerBaseCommit: repo.BaseCommit);
File.WriteAllText(Path.Combine(repo.RepoDir, "handled.txt"), "content");
GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "handled");
await BuildSut().SubmitAsync(task.Id, CancellationToken.None);
Assert.True(BroadcastWorktreeUpdatedFor(task.Id));
}
[Fact]
public async Task Submit_RejectedTask_DoesNotBroadcastWorktreeUpdated()
{
var (task, _) = await SeedAsync(Path.GetTempPath(), handlerBaseCommit: null);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
BuildSut().SubmitAsync(task.Id, CancellationToken.None));
Assert.False(BroadcastWorktreeUpdatedFor(task.Id));
}
}