Skip to content

Commit cdfb0d0

Browse files
committed
fixup: add tests and bug-test script for PR #262
- Add `cmd_microflows_builder_terminal_test.go` covering lastStmtIsReturn across all terminal-statement shapes: plain return, RaiseErrorStmt, IF without ELSE (non-terminal), IF with both branches returning, else-if chains, mixed return/raise, and LOOP (never terminal per BREAK semantics). - Add `mdl-examples/bug-tests/262-terminal-nested-if-no-dangling-merge.mdl` reproducer per CLAUDE.md checklist.
1 parent f850a37 commit cdfb0d0

2 files changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
-- ============================================================================
2+
-- Bug #262: Treat terminal nested IF as returning in flow builder
3+
-- ============================================================================
4+
--
5+
-- Symptom (before fix):
6+
-- A microflow whose body ends in `if { return } else { return }` was not
7+
-- detected as terminal. The outer flow builder created a merge node and a
8+
-- dangling continuation edge that pointed at no activity. Studio Pro raised
9+
-- "KeyNotFoundException" / "Sequence contains no matching element" on open.
10+
--
11+
-- After fix:
12+
-- lastStmtIsReturn recurses through IfStmt via the new isTerminalStmt helper,
13+
-- so an IF with an ELSE where both branches return is treated as terminal —
14+
-- no merge emitted, no dangling edge.
15+
--
16+
-- Usage:
17+
-- mxcli exec mdl-examples/bug-tests/262-terminal-nested-if-no-dangling-merge.mdl -p app.mpr
18+
-- Open in Studio Pro — no dangling edge, flow graph is clean.
19+
-- ============================================================================
20+
21+
create module BugTest262;
22+
23+
create microflow BugTest262.MF_BothBranchesReturn (
24+
$input: string
25+
)
26+
returns string as $result
27+
begin
28+
declare $result string = empty;
29+
30+
if $input = 'a' then
31+
return 'was a';
32+
else
33+
return 'not a';
34+
end if;
35+
end;
36+
/
37+
38+
create microflow BugTest262.MF_ElseIfChainAllReturn (
39+
$input: string
40+
)
41+
returns string as $result
42+
begin
43+
declare $result string = empty;
44+
45+
if $input = 'a' then
46+
return 'a';
47+
elsif $input = 'b' then
48+
return 'b';
49+
else
50+
return 'other';
51+
end if;
52+
end;
53+
/
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package executor
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/mdl/ast"
9+
)
10+
11+
func TestLastStmtIsReturn_EmptyBody(t *testing.T) {
12+
if lastStmtIsReturn(nil) {
13+
t.Error("empty body must not be terminal")
14+
}
15+
}
16+
17+
func TestLastStmtIsReturn_PlainReturn(t *testing.T) {
18+
body := []ast.MicroflowStatement{&ast.ReturnStmt{}}
19+
if !lastStmtIsReturn(body) {
20+
t.Error("body ending in ReturnStmt must be terminal")
21+
}
22+
}
23+
24+
func TestLastStmtIsReturn_RaiseError(t *testing.T) {
25+
body := []ast.MicroflowStatement{&ast.RaiseErrorStmt{}}
26+
if !lastStmtIsReturn(body) {
27+
t.Error("body ending in RaiseErrorStmt must be terminal")
28+
}
29+
}
30+
31+
func TestLastStmtIsReturn_IfWithoutElse_NotTerminal(t *testing.T) {
32+
body := []ast.MicroflowStatement{
33+
&ast.IfStmt{ThenBody: []ast.MicroflowStatement{&ast.ReturnStmt{}}},
34+
}
35+
if lastStmtIsReturn(body) {
36+
t.Error("IF without ELSE must not be terminal (false path falls through)")
37+
}
38+
}
39+
40+
func TestLastStmtIsReturn_IfBothBranchesReturn_Terminal(t *testing.T) {
41+
body := []ast.MicroflowStatement{
42+
&ast.IfStmt{
43+
ThenBody: []ast.MicroflowStatement{&ast.ReturnStmt{}},
44+
ElseBody: []ast.MicroflowStatement{&ast.ReturnStmt{}},
45+
},
46+
}
47+
if !lastStmtIsReturn(body) {
48+
t.Error("IF/ELSE where both branches return must be terminal")
49+
}
50+
}
51+
52+
func TestLastStmtIsReturn_IfOnlyThenReturns_NotTerminal(t *testing.T) {
53+
body := []ast.MicroflowStatement{
54+
&ast.IfStmt{
55+
ThenBody: []ast.MicroflowStatement{&ast.ReturnStmt{}},
56+
ElseBody: []ast.MicroflowStatement{&ast.LogStmt{}}, // non-terminal
57+
},
58+
}
59+
if lastStmtIsReturn(body) {
60+
t.Error("IF/ELSE where only THEN terminates must not be terminal")
61+
}
62+
}
63+
64+
func TestLastStmtIsReturn_NestedIfChain_Terminal(t *testing.T) {
65+
// if { return } else if { return } else { return }
66+
inner := &ast.IfStmt{
67+
ThenBody: []ast.MicroflowStatement{&ast.ReturnStmt{}},
68+
ElseBody: []ast.MicroflowStatement{&ast.ReturnStmt{}},
69+
}
70+
outer := &ast.IfStmt{
71+
ThenBody: []ast.MicroflowStatement{&ast.ReturnStmt{}},
72+
ElseBody: []ast.MicroflowStatement{inner},
73+
}
74+
if !lastStmtIsReturn([]ast.MicroflowStatement{outer}) {
75+
t.Error("else-if chain where every terminal branch returns must be terminal")
76+
}
77+
}
78+
79+
func TestLastStmtIsReturn_RaiseErrorMixed_Terminal(t *testing.T) {
80+
body := []ast.MicroflowStatement{
81+
&ast.IfStmt{
82+
ThenBody: []ast.MicroflowStatement{&ast.ReturnStmt{}},
83+
ElseBody: []ast.MicroflowStatement{&ast.RaiseErrorStmt{}},
84+
},
85+
}
86+
if !lastStmtIsReturn(body) {
87+
t.Error("IF/ELSE with return on one side and raise error on the other must be terminal")
88+
}
89+
}
90+
91+
func TestLastStmtIsReturn_LoopNotTerminal(t *testing.T) {
92+
// A LOOP whose body only returns is still non-terminal — BREAK can exit.
93+
body := []ast.MicroflowStatement{
94+
&ast.LoopStmt{Body: []ast.MicroflowStatement{&ast.ReturnStmt{}}},
95+
}
96+
if lastStmtIsReturn(body) {
97+
t.Error("LOOP must never be terminal (BREAK path)")
98+
}
99+
}

0 commit comments

Comments
 (0)