Issue #3613800: A branch whose outgoing flows all evaluate false ends the run silently, with no incident

A token that reaches a node with outgoing flows none of which evaluate true was consumed with no successor, so the branch ended and, once it was the last live one, the instance completed: no incident, no status, no log entry. The run read exactly like a successful one.

What changes

WorkflowExecutor::proceed() now separates the two empty-successor cases. A node with no outgoing flows at all is untouched (a legitimate end, the same test advanceNode() keys its status write on). A node that has outgoing flows and took none of them is dead-lettered exactly like an unrecoverable advance failure: the token is claimed ACTIVE -> ERROR through the guarded transition and an incident carries the diagnostic, so the instance's other branches keep running and its completion is blocked until an operator resolves it.

The message names the node and every condition with the value it read:

Dead end at node n_validate: every outgoing flow condition evaluated false, so
the branch ends without reaching an end node. Evaluated: flow f_approve:
comparison validation.result == "approved" (saw NULL); flow f_reject:
comparison validation.result == "rejected" (saw NULL).

A condition tree is rendered down to the leaf that failed:

flow f_yes: any of [all of [comparison approved == TRUE (saw TRUE) [true],
comparison amount > 1000 (saw 500) [false]] [false], comparison vip == TRUE
(saw FALSE) [false]]

That comes from a new FlowConditionInterface::explain($variables): only the plugin knows which variables it read, so only it can say what it saw. FlowConditionBase supplies a generic rendering (the settings as they stand) plus a formatValue() that keeps NULL, "" and FALSE apart, so existing plugins need no change; comparison and count override it, and the composites render their children with their results. It is only called on the failure path, never while routing.

FlowEvaluator::explainDeadEnd() composes the message and also names the other way the successor set can empty: a split that took none of the live flows.

Policy

orchestra.settings.on_dead_end, alongside on_unrecoverable_failure: incident (the default) or complete for the old behavior, which still logs the same diagnostic as a warning rather than being silent. Absent config reads as incident, so no update hook is needed.

Notes

Skip on a dead-end incident re-evaluates the same flows, so unless the variables changed it dead-ends and reports again; Resume (correct the variable) and Cancel branch are the meaningful actions, and this is documented.

Coverage

New DeadEndBranchTest: all-false conditions raise the incident (the renamed-key shape from the issue, so the conditions read NULL) with the token in error and the instance still running; an end node completes with no incident; a split with a live flow is unaffected; a condition tree names the failing child; on_dead_end: complete ends quietly; cancelling the incident completes the run. ComparisonTest covers explain() across the operators, and ConditionalFlowsTest::testNoMatchEndsBranch becomes testNoMatchReportsDeadEnd.

Docs: incidents, troubleshooting (the symptom as it presents, a run that ended one step in), joins and splits, extending. No new translatable strings (the diagnostic is untranslated, like an exception message), so no .po change.

Merge request reports

Loading
Loading