using System.Globalization; using ItWorx.KnowledgeHub.Domain.Jobs; namespace ItWorx.KnowledgeHub.Domain.Tests; public sealed class BackgroundJobTests { private static BackgroundJob NewJob(int total = 2) => BackgroundJob.Create( Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), BackgroundJobType.CourseTagging, "{}", new string('a', 64), new string('b', 64), "correlation", DateTimeOffset.UtcNow, total); [Fact] public void A_retried_job_can_resume_and_finish_without_losing_its_original_start_time() { var job = NewJob(); var now = DateTimeOffset.Parse("2026-10-02T12:00:00Z", CultureInfo.InvariantCulture); job.Start(Guid.NewGuid(), now, now.AddMinutes(1)); job.ScheduleRetry(now.AddSeconds(1), now.AddSeconds(3), "worker_retry", "Tijdelijke fout."); Assert.Equal(BackgroundJobStatus.Queued, job.Status); Assert.Equal(0, job.Failed); job.Start(Guid.NewGuid(), now.AddSeconds(3), now.AddMinutes(2)); job.RecordOutcome(BackgroundJobItemState.Succeeded); job.RecordOutcome(BackgroundJobItemState.Succeeded); job.Complete(now.AddSeconds(4), "{\"complete\":true}"); Assert.Equal(BackgroundJobStatus.Succeeded, job.Status); Assert.Equal(now, job.StartedAt); Assert.Equal(2, job.Processed); Assert.Equal(2, job.Succeeded); Assert.Equal(100, job.Progress); Assert.Equal("{\"complete\":true}", job.ResultJson); } [Fact] public void The_worker_stops_after_three_retries_and_records_a_bounded_safe_failure() { var job = NewJob(); var now = DateTimeOffset.Parse("2026-10-02T12:00:00Z", CultureInfo.InvariantCulture); for (var attempt = 1; attempt <= 4; attempt++) { var attemptAt = now.AddSeconds(attempt * 10); job.Start(Guid.NewGuid(), attemptAt, attemptAt.AddMinutes(1)); job.ScheduleRetry(attemptAt, attemptAt.AddSeconds(1), "processing_retry", "Veilige foutmelding."); if (attempt < 4) Assert.Equal(BackgroundJobStatus.Queued, job.Status); } Assert.Equal(BackgroundJobStatus.Failed, job.Status); Assert.Equal(1, job.Failed); Assert.Equal(1, job.Processed); Assert.Equal(4, job.AttemptCount); Assert.True(job.ErrorsJson.Length < 4096); Assert.Contains("Veilige foutmelding", job.ErrorsJson, StringComparison.Ordinal); } [Fact] public void Cancellation_is_immediate_for_queued_jobs_and_cooperative_for_running_jobs() { var queued = NewJob(); var now = DateTimeOffset.UtcNow; queued.RequestCancellation(now); Assert.Equal(BackgroundJobStatus.Cancelled, queued.Status); Assert.True(queued.IsTerminal); var running = NewJob(); running.Start(Guid.NewGuid(), now, now.AddMinutes(1)); running.RequestCancellation(now.AddSeconds(1)); Assert.Equal(BackgroundJobStatus.Running, running.Status); Assert.True(running.CancelRequestedAt.HasValue); running.Cancel(now.AddSeconds(2)); Assert.Equal(BackgroundJobStatus.Cancelled, running.Status); Assert.Equal(0, running.Processed); } [Fact] public void Errors_are_capped_and_result_json_is_bounded() { var job = NewJob(30); for (var index = 0; index < 30; index++) job.RecordOutcome(BackgroundJobItemState.Failed, "page_failed", new string('x', 500), Guid.NewGuid()); job.Complete(DateTimeOffset.UtcNow, new string('z', 5000)); Assert.Equal(BackgroundJobStatus.Failed, job.Status); Assert.Null(job.ResultJson); Assert.Equal(BackgroundJob.MaxStoredErrors, System.Text.Json.JsonSerializer.Deserialize(job.ErrorsJson)!.Length); Assert.All(System.Text.Json.JsonSerializer.Deserialize(job.ErrorsJson)!, error => { Assert.Equal(BackgroundJob.MaxErrorMessageLength, error.Message.Length); Assert.Equal("page_failed", error.Code); }); } }