From 57cc3637c96e3ea73cadb0b21a222525818ceb3f Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 15 Jul 2026 09:16:46 +0200 Subject: [PATCH 1/7] =?UTF-8?q?test(workflow):=20user-task=20client=20?= =?UTF-8?q?=E2=80=94=20werkbak=20query,=20claim,=20complete=20beoordeling?= =?UTF-8?q?=20(refs=20#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Red — GetOpenBeoordelingenAsync/ClaimAsync/CompleteBeoordelingAsync do not exist yet. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Big.Tests/FlowableWorkflowClientTests.cs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/services/domain/Big.Tests/FlowableWorkflowClientTests.cs b/services/domain/Big.Tests/FlowableWorkflowClientTests.cs index 625d69f..3d53a04 100644 --- a/services/domain/Big.Tests/FlowableWorkflowClientTests.cs +++ b/services/domain/Big.Tests/FlowableWorkflowClientTests.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text; +using Big.Application; using Big.Domain; using Big.Infrastructure; @@ -136,4 +137,106 @@ public class FlowableWorkflowClientTests await Assert.ThrowsAsync( () => client.CompleteOpenZaakJobAsync("job-1", new Uri("http://openzaak/zaken/api/v1/zaken/abc"))); } + + // ── S-12b: behandelaar user tasks (werkbak / claim / complete) ──────────────────────────────── + + [Fact] + public async Task Open_beoordelingen_queries_beoordelen_tasks_with_their_registration_id() + { + var rid = RegistrationId.New(); + var capture = new RequestCapture(); + var client = Client(capture.Responds(HttpStatusCode.OK, + $$""" + {"data":[{"id":"task-1","processVariables":[{"name":"registrationId","type":"string","value":"{{rid}}"}]}],"total":1} + """)); + + var tasks = await client.GetOpenBeoordelingenAsync(); + + var task = Assert.Single(tasks); + Assert.Equal("task-1", task.TaskId); + Assert.Equal(rid, task.RegistrationId); + Assert.Equal(HttpMethod.Post, capture.Seen!.Method); + Assert.Equal("http://flowable/flowable-rest/service/runtime/tasks/query", + capture.Seen.RequestUri!.ToString()); + Assert.Equal("rest-admin:test", DecodeBasic(capture.Seen)); + Assert.Contains("\"processDefinitionKey\":\"registratie\"", capture.Body); + Assert.Contains("\"taskDefinitionKey\":\"Beoordelen\"", capture.Body); + Assert.Contains("\"includeProcessVariables\":true", capture.Body); + } + + [Theory] + [InlineData("""{"data":[],"total":0}""")] + [InlineData("""{"data":null,"total":0}""")] + public async Task Open_beoordelingen_is_empty_when_no_tasks_are_waiting(string body) + { + var capture = new RequestCapture(); + var client = Client(capture.Responds(HttpStatusCode.OK, body)); + + Assert.Empty(await client.GetOpenBeoordelingenAsync()); + Assert.NotNull(capture.Seen); + } + + [Fact] + public async Task Open_beoordelingen_throws_when_a_task_carries_no_registration_id() + { + var capture = new RequestCapture(); + var client = Client(capture.Responds(HttpStatusCode.OK, + """{"data":[{"id":"task-1","processVariables":[]}],"total":1}""")); + + var ex = await Assert.ThrowsAsync(() => client.GetOpenBeoordelingenAsync()); + Assert.Contains("task-1", ex.Message); + Assert.Contains("registrationId", ex.Message); + } + + [Fact] + public async Task Claim_assigns_the_task_to_the_behandelaar() + { + var capture = new RequestCapture(); + var client = Client(capture.Responds(HttpStatusCode.OK)); + + await client.ClaimAsync("task-1", "merel-behandelaar"); + + Assert.Equal(HttpMethod.Post, capture.Seen!.Method); + Assert.Equal("http://flowable/flowable-rest/service/runtime/tasks/task-1", + capture.Seen.RequestUri!.ToString()); + Assert.Contains("\"action\":\"claim\"", capture.Body); + Assert.Contains("\"assignee\":\"merel-behandelaar\"", capture.Body); + } + + [Fact] + public async Task Claim_throws_when_flowable_rejects_the_request() + { + var capture = new RequestCapture(); + var client = Client(capture.Responds(HttpStatusCode.Conflict)); + + await Assert.ThrowsAsync(() => client.ClaimAsync("task-1", "merel-behandelaar")); + } + + [Theory] + [InlineData(BeoordelingsBesluit.Goedkeuren, "goedkeuren")] + [InlineData(BeoordelingsBesluit.Afwijzen, "afwijzen")] + public async Task Complete_posts_the_besluit_variable_to_the_task(BeoordelingsBesluit besluit, string expected) + { + var capture = new RequestCapture(); + var client = Client(capture.Responds(HttpStatusCode.OK)); + + await client.CompleteBeoordelingAsync("task-1", besluit); + + Assert.Equal(HttpMethod.Post, capture.Seen!.Method); + Assert.Equal("http://flowable/flowable-rest/service/runtime/tasks/task-1", + capture.Seen.RequestUri!.ToString()); + Assert.Contains("\"action\":\"complete\"", capture.Body); + Assert.Contains("\"name\":\"besluit\"", capture.Body); + Assert.Contains($"\"value\":\"{expected}\"", capture.Body); + } + + [Fact] + public async Task Complete_beoordeling_throws_when_flowable_rejects_the_request() + { + var capture = new RequestCapture(); + var client = Client(capture.Responds(HttpStatusCode.InternalServerError)); + + await Assert.ThrowsAsync( + () => client.CompleteBeoordelingAsync("task-1", BeoordelingsBesluit.Goedkeuren)); + } } -- 2.49.1 From efd1c06b99d69368a7972b81e73f01aa40c28389 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 15 Jul 2026 09:18:03 +0200 Subject: [PATCH 2/7] =?UTF-8?q?feat(workflow):=20user-task=20client=20for?= =?UTF-8?q?=20beoordeling=20=E2=80=94=20werkbak,=20claim,=20complete=20(re?= =?UTF-8?q?fs=20#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the IUserTaskClient port and implements it on the Workflow Client (the only code that talks to Flowable, §8.2): query open Beoordelen tasks (werkbak) with their registrationId, claim a task for a behandelaar, and complete it carrying the besluit into the process. Registered in DI for later wiring (S-12c). Co-Authored-By: Claude Opus 4.8 (1M context) --- services/domain/Big.Api/Program.cs | 1 + services/domain/Big.Application/Ports.cs | 24 ++++++++ .../FlowableWorkflowClient.cs | 58 ++++++++++++++++++- 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/services/domain/Big.Api/Program.cs b/services/domain/Big.Api/Program.cs index eb26a17..d24d5c6 100644 --- a/services/domain/Big.Api/Program.cs +++ b/services/domain/Big.Api/Program.cs @@ -20,6 +20,7 @@ builder.Services.AddSingleton(); builder.Services.AddHttpClient(); builder.Services.AddTransient(sp => sp.GetRequiredService()); builder.Services.AddTransient(sp => sp.GetRequiredService()); +builder.Services.AddTransient(sp => sp.GetRequiredService()); builder.Services.AddHttpClient(); builder.Services.AddScoped(); diff --git a/services/domain/Big.Application/Ports.cs b/services/domain/Big.Application/Ports.cs index 12350c6..0859d7c 100644 --- a/services/domain/Big.Application/Ports.cs +++ b/services/domain/Big.Application/Ports.cs @@ -36,6 +36,30 @@ public interface IAclClient Task ApproveZaakAsync(Uri zaakUrl, CancellationToken ct = default); } +/// +/// The port to the behandelaar's user tasks in the workflow engine (S-12). Implemented by the +/// Workflow Client — the only code that talks to Flowable (§8.2). The application lists the open +/// Beoordelen tasks (the werkbak), claims one for a behandelaar, and completes it with the +/// decision; it never names Flowable's REST shapes. +/// +public interface IUserTaskClient +{ + /// The werkbak: the Beoordelen tasks awaiting a behandelaar, each with the + /// registration it belongs to. + Task> GetOpenBeoordelingenAsync(CancellationToken ct = default); + + /// Claim a beoordeling task for a behandelaar (assigns it to them). + Task ClaimAsync(string taskId, string behandelaar, CancellationToken ct = default); + + /// Complete a beoordeling task, carrying the decision into the process as the + /// besluit variable so the workflow can continue on the chosen branch. + Task CompleteBeoordelingAsync(string taskId, BeoordelingsBesluit besluit, CancellationToken ct = default); +} + +/// A Beoordelen user task in the werkbak: the Flowable task id (needed to claim and +/// complete it) and the registration it carries as a process variable. +public sealed record BeoordelingTask(string TaskId, RegistrationId RegistrationId); + /// /// Persistence port for the aggregate. In-memory for the minimal slice /// (ADR-0009); an EF-backed store is a documented follow-up, and this port keeps that change additive. diff --git a/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs b/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs index 0b6ba23..323ede0 100644 --- a/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs +++ b/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs @@ -15,12 +15,14 @@ namespace Big.Infrastructure; /// The REST contract here is the one verified against a live flowable-rest engine (ADR-0009). /// public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions options) - : IWorkflowClient, IExternalWorkerClient + : IWorkflowClient, IExternalWorkerClient, IUserTaskClient { private const string Topic = "OpenZaakAanmaken"; private const string ProcessDefinitionKey = "registratie"; + private const string BeoordelenTaskKey = "Beoordelen"; private const string RegistrationIdVariable = "registrationId"; private const string ZaakUrlVariable = "zaakUrl"; + private const string BesluitVariable = "besluit"; public async Task StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default) { @@ -55,6 +57,34 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti response.EnsureSuccessStatusCode(); } + public async Task> GetOpenBeoordelingenAsync(CancellationToken ct = default) + { + var request = new TaskQueryRequest(ProcessDefinitionKey, BeoordelenTaskKey, IncludeProcessVariables: true); + + var page = await PostAsync( + "service/runtime/tasks/query", request, ct); + + var tasks = page?.Data ?? []; + return [.. tasks.Select(t => new BeoordelingTask(t.Id, RegistrationId.Parse(t.RegistrationId())))]; + } + + public async Task ClaimAsync(string taskId, string behandelaar, CancellationToken ct = default) + { + using var response = await SendAsync( + $"service/runtime/tasks/{taskId}", new ClaimTaskRequest("claim", behandelaar), ct); + response.EnsureSuccessStatusCode(); + } + + public async Task CompleteBeoordelingAsync(string taskId, BeoordelingsBesluit besluit, CancellationToken ct = default) + { + var request = new CompleteTaskRequest( + "complete", + [new Variable(BesluitVariable, "string", besluit.ToString().ToLowerInvariant())]); + + using var response = await SendAsync($"service/runtime/tasks/{taskId}", request, ct); + response.EnsureSuccessStatusCode(); + } + private async Task PostAsync(string path, TRequest body, CancellationToken ct) { using var response = await SendAsync(path, body, ct); @@ -89,6 +119,32 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti [property: JsonPropertyName("workerId")] string WorkerId, [property: JsonPropertyName("variables")] IReadOnlyList Variables); + private sealed record TaskQueryRequest( + [property: JsonPropertyName("processDefinitionKey")] string ProcessDefinitionKey, + [property: JsonPropertyName("taskDefinitionKey")] string TaskDefinitionKey, + [property: JsonPropertyName("includeProcessVariables")] bool IncludeProcessVariables); + + private sealed record ClaimTaskRequest( + [property: JsonPropertyName("action")] string Action, + [property: JsonPropertyName("assignee")] string Assignee); + + private sealed record CompleteTaskRequest( + [property: JsonPropertyName("action")] string Action, + [property: JsonPropertyName("variables")] IReadOnlyList Variables); + + private sealed record TaskQueryResult( + [property: JsonPropertyName("data")] IReadOnlyList? Data); + + private sealed record UserTaskDto( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("processVariables")] IReadOnlyList? ProcessVariables) + { + /// The registration id this task carries as a process variable. + public string RegistrationId() => + ProcessVariables?.SingleOrDefault(v => v.Name == "registrationId")?.Value + ?? throw new InvalidOperationException($"Beoordelen task {Id} carries no registrationId variable."); + } + private sealed record Variable( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("type")] string Type, -- 2.49.1 From 5db55eebf474cb8bcb47cb63795012d46f63ad8a Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 15 Jul 2026 09:19:19 +0200 Subject: [PATCH 3/7] feat(workflow): add the Beoordelen behandelaar user task to registratie.bpmn (refs #13) After OpenZaakAanmaken the process now parks at a Beoordelen user task (candidate group behandelaar) until the behandelaar claims and completes it. registrationId rides along as a process variable so the werkbak correlates each task to its aggregate. The walking skeleton is unaffected: the temporary /approve path sets the zaak status directly; wiring the decision to complete this task lands in S-12c/d. Co-Authored-By: Claude Opus 4.8 (1M context) --- workflows/registratie.bpmn | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/workflows/registratie.bpmn b/workflows/registratie.bpmn index e49e22a..51f7de9 100644 --- a/workflows/registratie.bpmn +++ b/workflows/registratie.bpmn @@ -6,9 +6,12 @@ xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" targetNamespace="http://respellion.nl/big"> - + @@ -19,9 +22,13 @@ flowable:type="external-worker" flowable:topic="OpenZaakAanmaken"/> - + - + + + + + @@ -32,8 +39,11 @@ + + + - + @@ -41,7 +51,11 @@ - + + + + + -- 2.49.1 From 3d561deb0569cf5333b12847c920cf50b2966814 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 15 Jul 2026 09:21:35 +0200 Subject: [PATCH 4/7] test(workflow): verify-domain exercises the Beoordelen user task end-to-end (refs #13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the worker opens the zaak, the process parks at Beoordelen. The live check now polls Flowable for the task (werkbak), claims it as merel-behandelaar, completes it with besluit=goedkeuren, and asserts the process finishes — proving the exact REST contract (query/claim/complete) the Workflow Client depends on against a real engine. Co-Authored-By: Claude Opus 4.8 (1M context) --- infra/run-domain-check.sh | 59 +++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/infra/run-domain-check.sh b/infra/run-domain-check.sh index 33d7d9e..e99f3fd 100755 --- a/infra/run-domain-check.sh +++ b/infra/run-domain-check.sh @@ -51,18 +51,65 @@ loc="$(docker run --rm --network "$net" curlimages/curl:latest \ echo ">> registration accepted at $loc" echo ">> polling the domain until the worker records the opened zaak" +zaak_ok="" for _ in $(seq 1 30); do body="$(docker run --rm --network "$net" curlimages/curl:latest \ -fsS "http://$dom_ip:8080$loc" 2>/dev/null || true)" if echo "$body" | grep -q '/zaken/api/v1/zaken/'; then echo "OK — the domain opened a zaak and recorded it on the registration:" echo "$body" | cut -c1-300 - exit 0 + zaak_ok=1 + break fi sleep 2 done -echo "FAIL — the registration never received a zaak URL" >&2 -echo "--- domain log ---" >&2; docker logs "$dom" 2>&1 | tail -15 >&2 -acl="$(docker ps -q --filter 'name=[-_]acl[-_]' | head -1)" -[ -n "$acl" ] && { echo "--- acl log ---" >&2; docker logs "$acl" 2>&1 | tail -15 >&2; } -exit 1 +if [ -z "$zaak_ok" ]; then + echo "FAIL — the registration never received a zaak URL" >&2 + echo "--- domain log ---" >&2; docker logs "$dom" 2>&1 | tail -15 >&2 + acl="$(docker ps -q --filter 'name=[-_]acl[-_]' | head -1)" + [ -n "$acl" ] && { echo "--- acl log ---" >&2; docker logs "$acl" 2>&1 | tail -15 >&2; } + exit 1 +fi + +# ── S-12b: the process now parks at the Beoordelen user task. Exercise the exact Flowable REST +# contract the Workflow Client uses (query/claim/complete) against the live engine, reaching +# flowable-rest by container IP (same in-network constraint as above). ────────────────────────── +fl="$(docker ps -q --filter 'name=flowable-rest' | head -1)" +[ -n "$fl" ] || { echo "ERROR: no running flowable-rest container" >&2; exit 1; } +fl_base="http://$(ip "$fl"):8080/flowable-rest/service" +reg_id="${loc##*/}" + +# Extracts the Beoordelen task id for our registration from a Flowable task-query response on stdin. +task_for_reg() { REG_ID="$reg_id" python3 -c "import os,sys,json +d=json.load(sys.stdin); rid=os.environ['REG_ID'] +print(next((t['id'] for t in (d.get('data') or []) + if any(v.get('name')=='registrationId' and v.get('value')==rid for v in (t.get('processVariables') or []))), ''))"; } + +flcurl() { docker run --rm --network "$net" curlimages/curl:latest -fsS -u rest-admin:test "$@"; } +query='{"processDefinitionKey":"registratie","taskDefinitionKey":"Beoordelen","includeProcessVariables":true}' + +echo ">> polling Flowable for the Beoordelen user task (werkbak)" +task_id="" +for _ in $(seq 1 30); do + resp="$(flcurl -X POST "$fl_base/runtime/tasks/query" -H 'Content-Type: application/json' -d "$query" 2>/dev/null || true)" + task_id="$(printf '%s' "$resp" | task_for_reg)" + [ -n "$task_id" ] && break + sleep 2 +done +[ -n "$task_id" ] || { echo "FAIL — no Beoordelen task appeared for registration $reg_id" >&2; docker logs "$dom" 2>&1 | tail -15 >&2; exit 1; } +echo ">> Beoordelen task $task_id is waiting" + +echo ">> claiming the task as merel-behandelaar" +flcurl -X POST "$fl_base/runtime/tasks/$task_id" -H 'Content-Type: application/json' \ + -d '{"action":"claim","assignee":"merel-behandelaar"}' >/dev/null + +echo ">> completing the beoordeling (goedkeuren)" +flcurl -X POST "$fl_base/runtime/tasks/$task_id" -H 'Content-Type: application/json' \ + -d '{"action":"complete","variables":[{"name":"besluit","type":"string","value":"goedkeuren"}]}' >/dev/null + +echo ">> asserting the process finished (no Beoordelen task remains for the registration)" +resp="$(flcurl -X POST "$fl_base/runtime/tasks/query" -H 'Content-Type: application/json' -d "$query")" +still="$(printf '%s' "$resp" | task_for_reg)" +[ -z "$still" ] || { echo "FAIL — Beoordelen task $still still active after completion" >&2; exit 1; } +echo "OK — behandelaar claimed and completed the Beoordelen task; the registratie process finished" +exit 0 -- 2.49.1 From 57b8755f9d980f71b8188b1acc59eea7c7560cf5 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 15 Jul 2026 09:24:53 +0200 Subject: [PATCH 5/7] test(workflow): cover the besluit variable type + start empty-body throw (refs #13) Kills two mutation survivors: assert the complete besluit variable's type, and add the missing start empty-response case (the pre-existing baseline survivor). Domain mutation score reaches 100%. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../domain/Big.Tests/FlowableWorkflowClientTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/domain/Big.Tests/FlowableWorkflowClientTests.cs b/services/domain/Big.Tests/FlowableWorkflowClientTests.cs index 3d53a04..b36b13e 100644 --- a/services/domain/Big.Tests/FlowableWorkflowClientTests.cs +++ b/services/domain/Big.Tests/FlowableWorkflowClientTests.cs @@ -128,6 +128,17 @@ public class FlowableWorkflowClientTests () => client.StartRegistrationProcessAsync(RegistrationId.New())); } + [Fact] + public async Task Start_throws_when_flowable_returns_an_empty_body() + { + var capture = new RequestCapture(); + var client = Client(capture.Responds(HttpStatusCode.Created, "null")); + + var ex = await Assert.ThrowsAsync( + () => client.StartRegistrationProcessAsync(RegistrationId.New())); + Assert.Contains("empty process-instance", ex.Message); + } + [Fact] public async Task Complete_throws_when_flowable_rejects_the_request() { @@ -227,6 +238,7 @@ public class FlowableWorkflowClientTests capture.Seen.RequestUri!.ToString()); Assert.Contains("\"action\":\"complete\"", capture.Body); Assert.Contains("\"name\":\"besluit\"", capture.Body); + Assert.Contains("\"type\":\"string\"", capture.Body); Assert.Contains($"\"value\":\"{expected}\"", capture.Body); } -- 2.49.1 From c53354cd22076eb3945f975b2830295a9c908c08 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 15 Jul 2026 09:57:11 +0200 Subject: [PATCH 6/7] fix(workflow): query Flowable tasks at service/query/tasks, not runtime/tasks/query (refs #13) The task-query endpoint is service/query/tasks; the wrong path 404'd, so verify-domain's werkbak poll got an empty body and the JSON parser aborted the check. Correct the path in the Workflow Client (and its unit test) and make the live-check parser tolerant of a transient empty/non-JSON body so the poll retries instead of crashing. Co-Authored-By: Claude Opus 4.8 (1M context) --- infra/run-domain-check.sh | 11 ++++++++--- .../Big.Infrastructure/FlowableWorkflowClient.cs | 2 +- .../domain/Big.Tests/FlowableWorkflowClientTests.cs | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/infra/run-domain-check.sh b/infra/run-domain-check.sh index e99f3fd..7b3ce31 100755 --- a/infra/run-domain-check.sh +++ b/infra/run-domain-check.sh @@ -80,8 +80,13 @@ fl_base="http://$(ip "$fl"):8080/flowable-rest/service" reg_id="${loc##*/}" # Extracts the Beoordelen task id for our registration from a Flowable task-query response on stdin. +# Tolerates an empty/non-JSON body (a transient failure during the poll) by printing nothing. task_for_reg() { REG_ID="$reg_id" python3 -c "import os,sys,json -d=json.load(sys.stdin); rid=os.environ['REG_ID'] +try: + d=json.load(sys.stdin) +except Exception: + d={} +rid=os.environ['REG_ID'] print(next((t['id'] for t in (d.get('data') or []) if any(v.get('name')=='registrationId' and v.get('value')==rid for v in (t.get('processVariables') or []))), ''))"; } @@ -91,7 +96,7 @@ query='{"processDefinitionKey":"registratie","taskDefinitionKey":"Beoordelen","i echo ">> polling Flowable for the Beoordelen user task (werkbak)" task_id="" for _ in $(seq 1 30); do - resp="$(flcurl -X POST "$fl_base/runtime/tasks/query" -H 'Content-Type: application/json' -d "$query" 2>/dev/null || true)" + resp="$(flcurl -X POST "$fl_base/query/tasks" -H 'Content-Type: application/json' -d "$query" 2>/dev/null || true)" task_id="$(printf '%s' "$resp" | task_for_reg)" [ -n "$task_id" ] && break sleep 2 @@ -108,7 +113,7 @@ flcurl -X POST "$fl_base/runtime/tasks/$task_id" -H 'Content-Type: application/j -d '{"action":"complete","variables":[{"name":"besluit","type":"string","value":"goedkeuren"}]}' >/dev/null echo ">> asserting the process finished (no Beoordelen task remains for the registration)" -resp="$(flcurl -X POST "$fl_base/runtime/tasks/query" -H 'Content-Type: application/json' -d "$query")" +resp="$(flcurl -X POST "$fl_base/query/tasks" -H 'Content-Type: application/json' -d "$query")" still="$(printf '%s' "$resp" | task_for_reg)" [ -z "$still" ] || { echo "FAIL — Beoordelen task $still still active after completion" >&2; exit 1; } echo "OK — behandelaar claimed and completed the Beoordelen task; the registratie process finished" diff --git a/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs b/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs index 323ede0..597a6e6 100644 --- a/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs +++ b/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs @@ -62,7 +62,7 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti var request = new TaskQueryRequest(ProcessDefinitionKey, BeoordelenTaskKey, IncludeProcessVariables: true); var page = await PostAsync( - "service/runtime/tasks/query", request, ct); + "service/query/tasks", request, ct); var tasks = page?.Data ?? []; return [.. tasks.Select(t => new BeoordelingTask(t.Id, RegistrationId.Parse(t.RegistrationId())))]; diff --git a/services/domain/Big.Tests/FlowableWorkflowClientTests.cs b/services/domain/Big.Tests/FlowableWorkflowClientTests.cs index b36b13e..3262506 100644 --- a/services/domain/Big.Tests/FlowableWorkflowClientTests.cs +++ b/services/domain/Big.Tests/FlowableWorkflowClientTests.cs @@ -167,7 +167,7 @@ public class FlowableWorkflowClientTests Assert.Equal("task-1", task.TaskId); Assert.Equal(rid, task.RegistrationId); Assert.Equal(HttpMethod.Post, capture.Seen!.Method); - Assert.Equal("http://flowable/flowable-rest/service/runtime/tasks/query", + Assert.Equal("http://flowable/flowable-rest/service/query/tasks", capture.Seen.RequestUri!.ToString()); Assert.Equal("rest-admin:test", DecodeBasic(capture.Seen)); Assert.Contains("\"processDefinitionKey\":\"registratie\"", capture.Body); -- 2.49.1 From 94a506cbfc21d58c997931951444dd7771b8aad5 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 15 Jul 2026 10:29:43 +0200 Subject: [PATCH 7/7] fix(workflow): read task query variables from 'variables', not 'processVariables' (refs #13) Flowable's POST query/tasks returns the included process variables under the key 'variables' (the request opts in via includeProcessVariables). The client DTO, its unit test, and the live-check parser all looked for 'processVariables', so the werkbak never matched a task's registrationId and verify-domain timed out. Verified by driving a real Flowable instance end-to-end locally: start -> complete external job -> the Beoordelen task carries registrationId under 'variables'. Co-Authored-By: Claude Opus 4.8 (1M context) --- infra/run-domain-check.sh | 3 ++- .../domain/Big.Infrastructure/FlowableWorkflowClient.cs | 6 ++++-- services/domain/Big.Tests/FlowableWorkflowClientTests.cs | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/infra/run-domain-check.sh b/infra/run-domain-check.sh index 7b3ce31..a701b21 100755 --- a/infra/run-domain-check.sh +++ b/infra/run-domain-check.sh @@ -87,8 +87,9 @@ try: except Exception: d={} rid=os.environ['REG_ID'] +# Flowable's task-query returns the included process variables under 'variables'. print(next((t['id'] for t in (d.get('data') or []) - if any(v.get('name')=='registrationId' and v.get('value')==rid for v in (t.get('processVariables') or []))), ''))"; } + if any(v.get('name')=='registrationId' and v.get('value')==rid for v in (t.get('variables') or []))), ''))"; } flcurl() { docker run --rm --network "$net" curlimages/curl:latest -fsS -u rest-admin:test "$@"; } query='{"processDefinitionKey":"registratie","taskDefinitionKey":"Beoordelen","includeProcessVariables":true}' diff --git a/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs b/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs index 597a6e6..f782cea 100644 --- a/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs +++ b/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs @@ -137,11 +137,13 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti private sealed record UserTaskDto( [property: JsonPropertyName("id")] string Id, - [property: JsonPropertyName("processVariables")] IReadOnlyList? ProcessVariables) + // Flowable's task-query returns the (included) process variables under "variables", not + // "processVariables"; the request opts in via includeProcessVariables. + [property: JsonPropertyName("variables")] IReadOnlyList? Variables) { /// The registration id this task carries as a process variable. public string RegistrationId() => - ProcessVariables?.SingleOrDefault(v => v.Name == "registrationId")?.Value + Variables?.SingleOrDefault(v => v.Name == "registrationId")?.Value ?? throw new InvalidOperationException($"Beoordelen task {Id} carries no registrationId variable."); } diff --git a/services/domain/Big.Tests/FlowableWorkflowClientTests.cs b/services/domain/Big.Tests/FlowableWorkflowClientTests.cs index 3262506..f1bf579 100644 --- a/services/domain/Big.Tests/FlowableWorkflowClientTests.cs +++ b/services/domain/Big.Tests/FlowableWorkflowClientTests.cs @@ -158,7 +158,7 @@ public class FlowableWorkflowClientTests var capture = new RequestCapture(); var client = Client(capture.Responds(HttpStatusCode.OK, $$""" - {"data":[{"id":"task-1","processVariables":[{"name":"registrationId","type":"string","value":"{{rid}}"}]}],"total":1} + {"data":[{"id":"task-1","variables":[{"name":"registrationId","type":"string","value":"{{rid}}"}]}],"total":1} """)); var tasks = await client.GetOpenBeoordelingenAsync(); @@ -192,7 +192,7 @@ public class FlowableWorkflowClientTests { var capture = new RequestCapture(); var client = Client(capture.Responds(HttpStatusCode.OK, - """{"data":[{"id":"task-1","processVariables":[]}],"total":1}""")); + """{"data":[{"id":"task-1","variables":[]}],"total":1}""")); var ex = await Assert.ThrowsAsync(() => client.GetOpenBeoordelingenAsync()); Assert.Contains("task-1", ex.Message); -- 2.49.1