* test(bpmn): event subprocesses, dormant and listener-backed (#7933)
Both flavours, and the three responsibilities the host has for them. The
production diff is empty: the applier and binder need no event-subprocess
code path, which is what #7909 measured and what this pins.
A dormant catcher -- error or escalation -- rides the FaultSignal seam and the
escalation signal path, and reaches the host as an ordinary StartWork for its
body. A listener-backed one gets a second StartWork for its listenerBindingRef
at scope start and a CancelWorkSubtree for it when the scope completes. Both
already apply like any other command.
The host responsibilities, each with the failure it would otherwise hide:
- The listener is armed at scope start and observable in the scope's own ledger
before anything fires, not inferred from a fire that worked.
- A completing scope retires a still-armed listener. Pinned twice: at the root,
where Elsa's own container completion would cancel the child regardless and
only the scope's ledger tells the two apart, and inside a subprocess the
workflow outlives, where a listener left behind is one something could still
resume into.
- The start-element hint reaches the body and nothing else inherits it. The
body's only start event is event-defined, so a body that never received the
hint faults bpmn.start.none-available rather than starting somewhere
plausible; the ordinary subprocess inside it faults bpmn.start.unresolved-hint
if the hint travels where it must not. The scope's invocation correlation is
read back after the body has run work of its own, because the dictionary is
fixed for the scope's lifetime and the hint is read from it.
- A non-interrupting listener fired twice re-arms onto the slot the first fire
vacated, holding one live record and one bookmark at a time. This is the case
the completed-work-removed-before-the-interpreter-is-asked ordering exists
for, and it is now observable; BpmnHostInvariantTests points at it.
The library's declaration rules are pinned as refusals rather than gaps: a body
with more than one start event, a second error-triggered event subprocess in a
scope, and a non-interrupting error event subprocess are each refused when the
scope builds its graph, before any work starts. The last is additionally
dropped at import, with the rest of the document reading as written -- the
dropped body carries an undeclared serviceTask, so an import that still
succeeds is what proves the drop took its bindings with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(bpmn): pin the catch-all escalation event subprocess refusal
Two error-triggered event subprocesses per scope was pinned but its sibling
rule -- at most one code-less catch-all escalation event subprocess -- had
no test. Add TwoCatchAllEscalationEventSubprocesses_AreRefused, mirroring
the error refusal test, and let Escalation() build a code-less definition.
Also record in BpmnCommandApplier why CancelSubtreeAsync's explicit
subtree cancellation is redundant on the scope-completion path (Elsa's own
container-completion behaviour already covers it) while the ledger removal
above it is not, so a future reader does not "simplify" the ledger removal
away.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>