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.Services; // Slice 4 of the task-numbers feature: TaskMergeService's curated WorkerLog business events must // say "#" alongside the quoted title. public sealed class TaskMergeServiceTaskNumberTests : IDisposable { private readonly List _dbs = new(); private readonly List _repos = new(); private readonly List<(string repoDir, string wtPath)> _wtCleanups = new(); private DbFixture NewDb() { var d = new DbFixture(); _dbs.Add(d); return d; } private GitRepoFixture NewRepo() { var r = new GitRepoFixture(); _repos.Add(r); return r; } public void Dispose() { foreach (var (repoDir, wtPath) in _wtCleanups) { try { GitRepoFixture.RunGit(repoDir, "worktree", "remove", "--force", wtPath); } catch { } } foreach (var d in _dbs) try { d.Dispose(); } catch { } foreach (var r in _repos) try { r.Dispose(); } catch { } } [Fact] public async Task MergeAsync_Success_BroadcastsWorkerLog_ContainingTaskNumber() { if (!GitRepoFixture.IsGitAvailable()) return; var repo = NewRepo(); var db = NewDb(); var list = new ListEntity { Id = Guid.NewGuid().ToString(), Name = "merge-num-test", WorkingDir = repo.RepoDir, DefaultCommitType = "feat", CreatedAt = DateTime.UtcNow, }; var task = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = list.Id, Title = "merge-task", Number = 789, Status = TaskStatus.Done, CreatedAt = DateTime.UtcNow, }; using (var ctx = db.CreateContext()) { // Bypass TaskRepository.AddAsync — it allocates its own Number via // TaskNumberAllocator and would silently overwrite the one this test sets. ctx.Lists.Add(list); ctx.Tasks.Add(task); await ctx.SaveChangesAsync(); } var wtMgr = new WorktreeManager(new GitService(), db.CreateFactory(), new WorkerConfig { WorktreeRootStrategy = "sibling" }, NullLogger.Instance); var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None); _wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath)); File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "added.txt"), "new\n"); await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None); var hub = new CapturingHubContext(); var state = TaskStateServiceBuilder.Build(db.CreateFactory()).State; var svc = new TaskMergeService( db.CreateFactory(), new GitService(), new HubBroadcaster(hub), state, new VerifyCommandRunner(), NullLogger.Instance); var currentBranch = await new GitService().GetCurrentBranchAsync(repo.RepoDir); var result = await svc.MergeAsync(task.Id, currentBranch, removeWorktree: false, commitMessage: "Merge task", ct: CancellationToken.None); Assert.Equal("merged", result.Status); Assert.Contains(hub.Proxy.Calls, c => c.Method == "WorkerLog" && c.Args[0] is string s && s.Contains("Merged") && s.Contains("#789")); } }