test(worker): cover WorktreeUpdated broadcast on interactive review submission
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user