From 5df3429cbf10abacdb7c80eafd85b80938927070 Mon Sep 17 00:00:00 2001 From: Henrique Costa Date: Sat, 2 May 2026 18:31:31 +0200 Subject: [PATCH] fix: rejoin empty error handlers inside branch bodies Symptom: an empty custom error handler inside a structured branch could round-trip as an explicit `return;` handler even though the authored handler should continue to the next branch action. Root cause: the top-level flow builder routes pending empty error handlers when connecting one statement to the next, but IF and enum-split branch builders connected sequential branch statements directly and skipped that pending handler rejoin step. Fix: when branch builders connect a statement that owns an empty no-output custom handler to the next branch statement, run the same pending error-handler routing used by the top-level graph builder. Output-dependent handlers are left to the existing skip-var path so they still avoid unsafe success tails. Tests: added a synthetic IF-branch regression test; `go test ./mdl/executor -run 'EmptyNoOutputHandler.*Branch|EmptyNoOutputHandlerRejoins|OutputHandlerInReturningBranch' -v`; `make build`; `make lint-go`; `make test`. --- .../cmd_microflows_builder_actions.go | 5 +- .../cmd_microflows_builder_control.go | 10 +++- .../cmd_microflows_builder_terminal_test.go | 52 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/mdl/executor/cmd_microflows_builder_actions.go b/mdl/executor/cmd_microflows_builder_actions.go index b0f869fc..bfbd1d50 100644 --- a/mdl/executor/cmd_microflows_builder_actions.go +++ b/mdl/executor/cmd_microflows_builder_actions.go @@ -346,7 +346,7 @@ func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID { lastID := model.ID("") pendingCase := "" var prevAnchor *ast.FlowAnchors - for _, stmt := range br.body { + for j, stmt := range br.body { thisAnchor := stmtOwnAnchor(stmt) actID := fb.addStatement(stmt) if actID == "" { @@ -376,6 +376,9 @@ func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID { } applyUserAnchors(flow, prevAnchor, thisAnchor) fb.flows = append(fb.flows, flow) + if fb.emptyErrorHandlerFrom == lastID { + fb.addPendingErrorHandlerFlowForStatement(lastID, actID, stmt, statementsReferenceVar(br.body[j+1:], fb.errorHandlerSkipVar)) + } } prevAnchor = thisAnchor if fb.nextConnectionPoint != "" { diff --git a/mdl/executor/cmd_microflows_builder_control.go b/mdl/executor/cmd_microflows_builder_control.go index 11b3fd5a..24e51a36 100644 --- a/mdl/executor/cmd_microflows_builder_control.go +++ b/mdl/executor/cmd_microflows_builder_control.go @@ -122,7 +122,7 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID { var prevThenAnchor *ast.FlowAnchors var pendingThenCase string var pendingThenAnchor *ast.FlowAnchors - for _, stmt := range s.ThenBody { + for i, stmt := range s.ThenBody { thisAnchor := stmtOwnAnchor(stmt) actID := fb.addStatement(stmt) if actID != "" { @@ -152,6 +152,9 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID { } applyUserAnchors(flow, originAnchor, destAnchor) fb.flows = append(fb.flows, flow) + if fb.emptyErrorHandlerFrom == lastThenID { + fb.addPendingErrorHandlerFlowForStatement(lastThenID, actID, stmt, statementsReferenceVar(s.ThenBody[i+1:], fb.errorHandlerSkipVar)) + } } prevThenAnchor = thisAnchor // For nested compound statements, use their exit point @@ -209,7 +212,7 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID { var prevElseAnchor *ast.FlowAnchors var pendingElseCase string var pendingElseAnchor *ast.FlowAnchors - for _, stmt := range s.ElseBody { + for i, stmt := range s.ElseBody { thisAnchor := stmtOwnAnchor(stmt) actID := fb.addStatement(stmt) if actID != "" { @@ -237,6 +240,9 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID { } applyUserAnchors(flow, originAnchor, destAnchor) fb.flows = append(fb.flows, flow) + if fb.emptyErrorHandlerFrom == lastElseID { + fb.addPendingErrorHandlerFlowForStatement(lastElseID, actID, stmt, statementsReferenceVar(s.ElseBody[i+1:], fb.errorHandlerSkipVar)) + } } prevElseAnchor = thisAnchor // For nested compound statements, use their exit point diff --git a/mdl/executor/cmd_microflows_builder_terminal_test.go b/mdl/executor/cmd_microflows_builder_terminal_test.go index bf5982c2..1ba10672 100644 --- a/mdl/executor/cmd_microflows_builder_terminal_test.go +++ b/mdl/executor/cmd_microflows_builder_terminal_test.go @@ -777,6 +777,58 @@ func TestBuildFlowGraph_EmptyNoOutputHandlerRejoinsAtNextAction(t *testing.T) { t.Fatal("empty no-output handler should rejoin at the next action") } +func TestBuildFlowGraph_EmptyNoOutputHandlerInIfBranchRejoinsAtNextAction(t *testing.T) { + body := []ast.MicroflowStatement{ + &ast.IfStmt{ + Condition: &ast.VariableExpr{Name: "ShouldRefresh"}, + HasElse: true, + ThenBody: []ast.MicroflowStatement{ + &ast.CallMicroflowStmt{ + MicroflowName: ast.QualifiedName{Module: "Synthetic", Name: "RefreshCache"}, + ErrorHandling: &ast.ErrorHandlingClause{Type: ast.ErrorHandlingCustomWithoutRollback}, + }, + &ast.CallJavaActionStmt{ + OutputVariable: "ProcessedCount", + ActionName: ast.QualifiedName{Module: "Synthetic", Name: "CountProcessedItems"}, + }, + }, + ElseBody: []ast.MicroflowStatement{ + &ast.ReturnStmt{}, + }, + }, + } + + fb := &flowBuilder{posX: 100, posY: 100, spacing: HorizontalSpacing, measurer: &layoutMeasurer{}} + oc := fb.buildFlowGraph(body, nil) + + var callID, javaID model.ID + for _, obj := range oc.Objects { + activity, ok := obj.(*microflows.ActionActivity) + if !ok { + continue + } + switch action := activity.Action.(type) { + case *microflows.MicroflowCallAction: + if action.MicroflowCall != nil && action.MicroflowCall.Microflow == "Synthetic.RefreshCache" { + callID = activity.ID + } + case *microflows.JavaActionCallAction: + if action.ResultVariableName == "ProcessedCount" { + javaID = activity.ID + } + } + } + if callID == "" || javaID == "" { + t.Fatalf("expected no-output call and branch continuation; got call=%q java=%q", callID, javaID) + } + for _, flow := range oc.Flows { + if flow.IsErrorHandler && flow.OriginID == callID && flowPathExists(oc.Flows, flow.DestinationID, javaID) { + return + } + } + t.Fatal("empty no-output handler inside IF branch should rejoin at the next branch action") +} + func TestBuildFlowGraph_ErrorHandlerEmptyElseKeepsFalseCaseOnRejoin(t *testing.T) { body := []ast.MicroflowStatement{ &ast.CallMicroflowStmt{