126 lines
5.0 KiB
C#
126 lines
5.0 KiB
C#
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));
|
|
}
|
|
}
|