using ClaudeDo.Worker.Lifecycle; using ModelContextProtocol; using Xunit; namespace ClaudeDo.Worker.Tests.Lifecycle; // A synchronously-collecting IProgress: Progress marshals through the SynchronizationContext // captured at construction, which is unreliable to assert on immediately in a test. file sealed class SyncProgress : IProgress { public readonly List Reports = new(); public void Report(T value) => Reports.Add(value); } public sealed class ProgressReporterTests { [Fact] public async Task RunAsync_NoProgressOrOnTick_ReturnsWorkResultWithoutTicking() { var result = await ProgressReporter.RunAsync(Task.FromResult(42), TimeSpan.FromMilliseconds(10), null, "message"); Assert.Equal(42, result); } [Fact] public async Task RunAsync_SlowWork_ReportsProgressOnEachTick() { var progress = new SyncProgress(); var work = Task.Delay(250).ContinueWith(_ => "done"); var result = await ProgressReporter.RunAsync(work, TimeSpan.FromMilliseconds(50), progress, "waiting"); Assert.Equal("done", result); Assert.NotEmpty(progress.Reports); Assert.All(progress.Reports, r => Assert.Contains("waiting", r.Message)); } [Fact] public void ReportItem_ReportsMessageAndProgressTotalForEachItem() { var progress = new SyncProgress(); for (var i = 1; i <= 3; i++) ProgressReporter.ReportItem(progress, "cleaning worktrees", i, 3); Assert.Equal(3, progress.Reports.Count); Assert.Equal("cleaning worktrees (1/3)", progress.Reports[0].Message); Assert.Equal("cleaning worktrees (2/3)", progress.Reports[1].Message); Assert.Equal("cleaning worktrees (3/3)", progress.Reports[2].Message); Assert.Equal(3, progress.Reports[2].Total); } [Fact] public void ReportItem_NoProgress_DoesNotThrow() { ProgressReporter.ReportItem(null, "cleaning worktrees", 1, 3); } }