using FluentAssertions; using FreeCode.Tasks; using Xunit; using TaskStatus = FreeCode.Core.Enums.TaskStatus; namespace FreeCode.Tests.Integration; public sealed class TaskManagerTests { [Fact] public async Task BackgroundTaskManager_ShellTask_CompletesLifecycle() { await using var sut = new BackgroundTaskManager(); var states = new List(); sut.TaskStateChanged += (_, args) => states.Add(args.NewStatus); var processStartInfo = new System.Diagnostics.ProcessStartInfo { FileName = "/bin/zsh", Arguments = "-lc \"printf completed\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; var task = await sut.CreateShellTaskAsync("printf completed", processStartInfo); await WaitUntilAsync(() => sut.GetTask(task.TaskId)?.Status is TaskStatus.Completed or TaskStatus.Failed, TimeSpan.FromSeconds(5)); var updated = sut.GetTask(task.TaskId); updated.Should().NotBeNull(); updated!.Status.Should().Be(TaskStatus.Completed); states.Should().Contain(TaskStatus.Running); states.Should().Contain(TaskStatus.Completed); (await sut.GetTaskOutputAsync(task.TaskId)).Should().Contain("completed"); } [Fact] public async Task ShellTask_CapturesStdout() { await using var sut = new BackgroundTaskManager(); var task = await sut.CreateShellTaskAsync("printf test output", CreateShellProcess("printf 'test output'")); await WaitUntilAsync(() => sut.GetTask(task.TaskId)?.Status is TaskStatus.Completed or TaskStatus.Failed, TimeSpan.FromSeconds(5)); var shellTask = sut.GetTask(task.TaskId).Should().BeOfType().Subject; shellTask.Stdout.Should().Contain("test output"); } [Fact] public async Task ShellTask_CapturesStderr() { await using var sut = new BackgroundTaskManager(); var task = await sut.CreateShellTaskAsync("write stderr", CreateShellProcess("printf 'err' >&2")); await WaitUntilAsync(() => sut.GetTask(task.TaskId)?.Status is TaskStatus.Completed or TaskStatus.Failed, TimeSpan.FromSeconds(5)); var shellTask = sut.GetTask(task.TaskId).Should().BeOfType().Subject; shellTask.Stderr.Should().Contain("err"); } [Fact] public async Task ShellTask_ExitCode_Captured() { await using var sut = new BackgroundTaskManager(); var task = await sut.CreateShellTaskAsync("exit 42", CreateShellProcess("exit 42")); await WaitUntilAsync(() => sut.GetTask(task.TaskId)?.Status is TaskStatus.Completed or TaskStatus.Failed, TimeSpan.FromSeconds(5)); var shellTask = sut.GetTask(task.TaskId).Should().BeOfType().Subject; shellTask.ExitCode.Should().Be(42); } [Fact] public async Task StopTask_StopsRunningTask() { await using var sut = new BackgroundTaskManager(); var task = await sut.CreateShellTaskAsync("sleep 10", CreateShellProcess("sleep 10")); await WaitUntilAsync(() => sut.GetTask(task.TaskId)?.Status == TaskStatus.Running, TimeSpan.FromSeconds(5)); await sut.StopTaskAsync(task.TaskId); await WaitUntilAsync(() => sut.GetTask(task.TaskId)?.Status == TaskStatus.Stopped, TimeSpan.FromSeconds(5)); sut.GetTask(task.TaskId).Should().NotBeNull(); sut.GetTask(task.TaskId)!.Status.Should().Be(TaskStatus.Stopped); } [Fact] public async Task ListTasks_ReturnsAllTasks() { await using var sut = new BackgroundTaskManager(); var first = await sut.CreateShellTaskAsync("printf first", CreateShellProcess("printf first")); var second = await sut.CreateShellTaskAsync("printf second", CreateShellProcess("printf second")); await WaitUntilAsync(() => sut.ListTasks().Count >= 2, TimeSpan.FromSeconds(5)); sut.ListTasks().Select(task => task.TaskId).Should().Contain([first.TaskId, second.TaskId]); } [Fact] public async Task GetTask_ReturnsTaskById() { await using var sut = new BackgroundTaskManager(); var task = await sut.CreateShellTaskAsync("printf lookup", CreateShellProcess("printf lookup")); sut.GetTask(task.TaskId).Should().NotBeNull(); sut.GetTask(task.TaskId)!.TaskId.Should().Be(task.TaskId); } [Fact] public async Task GetTask_UnknownId_ReturnsNull() { await using var sut = new BackgroundTaskManager(); await Task.CompletedTask; sut.GetTask("missing-task").Should().BeNull(); } private static System.Diagnostics.ProcessStartInfo CreateShellProcess(string command) { return new System.Diagnostics.ProcessStartInfo { FileName = "/bin/zsh", Arguments = $"-lc \"{command}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; } private static async Task WaitUntilAsync(Func condition, TimeSpan timeout) { var deadline = DateTime.UtcNow + timeout; while (DateTime.UtcNow < deadline) { if (condition()) { return; } await Task.Delay(50); } throw new TimeoutException("Condition was not met before timeout."); } }