From e2eaa0786d0277f55b73bb614251a4e4cf75f74d Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 17 Jul 2026 09:04:29 +0200 Subject: [PATCH] test(workflow): escalation drain loop reassigns then completes (S-14, refs #15) Failing tests for BeoordelingEscalatieProcessor: acquire the parked BeoordelingEscaleren jobs, reassign each instance to teamlead, complete the job, and leave a failed reassignment un-completed for redelivery. PumpOnceAsync is stubbed to throw so the suite compiles and fails on the new behaviour only. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../BeoordelingEscalatieProcessor.cs | 19 +++++ .../BeoordelingEscalatieProcessorTests.cs | 82 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 services/domain/Big.Infrastructure/BeoordelingEscalatieProcessor.cs create mode 100644 services/domain/Big.Tests/BeoordelingEscalatieProcessorTests.cs diff --git a/services/domain/Big.Infrastructure/BeoordelingEscalatieProcessor.cs b/services/domain/Big.Infrastructure/BeoordelingEscalatieProcessor.cs new file mode 100644 index 0000000..931ec53 --- /dev/null +++ b/services/domain/Big.Infrastructure/BeoordelingEscalatieProcessor.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; + +namespace Big.Infrastructure; + +/// +/// One poll tick of the beoordeling-escalation worker (S-14, ADR-0015): acquire the parked +/// BeoordelingEscaleren jobs — the tokens the 14-day boundary timer on Beoordelen spawns +/// — reassign each instance's still-open Beoordelen task to the teamlead, and complete the job. +/// A job that fails is logged and left un-completed so Flowable redelivers it (§8.6). Split out from +/// the hosted pump so the acquire→reassign→complete logic is unit-testable without a running host. +/// +public sealed class BeoordelingEscalatieProcessor( + IBeoordelingEscalatieClient client, + ILogger logger) +{ + /// Acquire and process up to escalations. Returns the number acquired. + public Task PumpOnceAsync(int maxJobs, CancellationToken ct = default) + => throw new NotImplementedException(); +} diff --git a/services/domain/Big.Tests/BeoordelingEscalatieProcessorTests.cs b/services/domain/Big.Tests/BeoordelingEscalatieProcessorTests.cs new file mode 100644 index 0000000..f39e10a --- /dev/null +++ b/services/domain/Big.Tests/BeoordelingEscalatieProcessorTests.cs @@ -0,0 +1,82 @@ +using Big.Application; +using Big.Infrastructure; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Big.Tests; + +// S-14 (#15): the escalation drain loop. Mirrors OpenZaakJobProcessor — acquire the parked +// BeoordelingEscaleren jobs, reassign each instance's Beoordelen task to teamlead, then complete the +// job. A job whose reassignment fails is logged and left un-completed for Flowable to redeliver (§8.6). +public class BeoordelingEscalatieProcessorTests +{ + /// A fake escalation client: scripts the jobs to acquire, records reassignments and + /// completions, and can be told to throw on reassigning a given instance. + private sealed class FakeEscalatieClient(params EscalatieJob[] jobs) : IBeoordelingEscalatieClient + { + public int AcquireCount { get; private set; } + public List Reassigned { get; } = []; + public List Completed { get; } = []; + public string? ThrowOnInstance { get; set; } + + public Task> AcquireBeoordelingEscalatieJobsAsync(int maxJobs, CancellationToken ct = default) + { + AcquireCount++; + return Task.FromResult>(jobs.Take(maxJobs).ToList()); + } + + public Task ReassignBeoordelingToTeamleadAsync(string processInstanceId, CancellationToken ct = default) + { + if (processInstanceId == ThrowOnInstance) + throw new InvalidOperationException("reassign failed"); + Reassigned.Add(processInstanceId); + return Task.CompletedTask; + } + + public Task CompleteBeoordelingEscalatieJobAsync(string jobId, CancellationToken ct = default) + { + Completed.Add(jobId); + return Task.CompletedTask; + } + } + + [Fact] + public async Task Acquires_an_escalation_reassigns_to_teamlead_and_completes_the_job() + { + var client = new FakeEscalatieClient(new EscalatieJob("job-9", "pi-1")); + + var acquired = await new BeoordelingEscalatieProcessor( + client, NullLogger.Instance).PumpOnceAsync(5); + + Assert.Equal(1, acquired); + Assert.Equal("pi-1", Assert.Single(client.Reassigned)); + Assert.Equal("job-9", Assert.Single(client.Completed)); + } + + [Fact] + public async Task A_failing_reassign_is_left_uncompleted_for_flowable_to_redeliver() + { + var client = new FakeEscalatieClient(new EscalatieJob("job-9", "pi-1")) { ThrowOnInstance = "pi-1" }; + var logger = new CapturingLogger(); + + var acquired = await new BeoordelingEscalatieProcessor(client, logger).PumpOnceAsync(5); + + Assert.Equal(1, acquired); + Assert.Empty(client.Completed); + var error = Assert.Single(logger.Entries, e => e.Level == LogLevel.Error); + Assert.Contains("job-9", error.Message); + } + + [Fact] + public async Task Does_nothing_but_poll_when_there_are_no_escalations() + { + var client = new FakeEscalatieClient(); + + var acquired = await new BeoordelingEscalatieProcessor( + client, NullLogger.Instance).PumpOnceAsync(5); + + Assert.Equal(0, acquired); + Assert.Equal(1, client.AcquireCount); + Assert.Empty(client.Completed); + } +}