Skip to content

Commit cdd4443

Browse files
hjothamendixclaude
andcommitted
fixup: walk EnumSplitStmt cases in reference validation; pin BSON keys
Address review on PR #475. **B2 (real bug, fixed)** — `flowRefCollector.collectFromStatements` had `case *ast.EnumSplitStmt:` with an empty body immediately followed by `case *ast.InheritanceSplitStmt:` whose body walked `s.Cases` and `s.ElseBody`. Go type switches do not fall through, so the EnumSplitStmt case was a silent no-op: any microflow / page / java action / entity reference inside an enum-split case body or its else body escaped reference validation. Give EnumSplitStmt its own complete walk identical to the InheritanceSplitStmt one. Tests: `TestValidateMicroflowReferences_DescendsIntoEnumSplitCases` and `…IntoEnumSplitElse` pin that a missing microflow inside either branch is reported, so this regression cannot return. **B1 (review premise wrong, BSON keys pinned by tests)** — review asserted Studio Pro stores `CastAction` output under `OutputVariableName` and `InheritanceCase` entity reference under `Entity`. A BSON dump of the Control Centre app (Mendix 9.24) contradicts both: Microflows$CastAction: VariableName = Account ErrorHandlingType = Rollback Microflows$InheritanceCase: Value = Administration.Account The writer is correct as-is (`VariableName` for CastAction, `Value` for InheritanceCase). Rather than change correct code, pin the BSON shape with regression tests that fail loudly if anyone "fixes" the writer based on a faulty premise: - TestSerializeCastAction_UsesVariableNameFieldKey - TestBuildSequenceFlowCase_InheritanceCase_UsesValueFieldKey The parser already falls back to the alternative keys for forward compatibility (introduced in PR #365), so projects produced by future Mendix versions that ever switched naming would still parse cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 344707d commit cdd4443

3 files changed

Lines changed: 150 additions & 0 deletions

File tree

mdl/executor/validate.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,10 @@ func (c *flowRefCollector) collectFromStatements(stmts []ast.MicroflowStatement)
598598
c.collectFromStatements(s.ThenBody)
599599
c.collectFromStatements(s.ElseBody)
600600
case *ast.EnumSplitStmt:
601+
for _, cse := range s.Cases {
602+
c.collectFromStatements(cse.Body)
603+
}
604+
c.collectFromStatements(s.ElseBody)
601605
case *ast.InheritanceSplitStmt:
602606
for _, cse := range s.Cases {
603607
c.collectFromStatements(cse.Body)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package executor
4+
5+
import (
6+
"strings"
7+
"testing"
8+
9+
"github.com/mendixlabs/mxcli/mdl/ast"
10+
"github.com/mendixlabs/mxcli/mdl/backend/mock"
11+
"github.com/mendixlabs/mxcli/model"
12+
"github.com/mendixlabs/mxcli/sdk/microflows"
13+
)
14+
15+
// flowRefCollector.collectFromStatements must descend into EnumSplitStmt
16+
// case bodies and the else body. A regression in PR #475's first revision
17+
// left the EnumSplitStmt branch with an empty case body in a Go type
18+
// switch, so the loop walking case bodies was silently stolen by the
19+
// next case (InheritanceSplitStmt). The result was that microflow calls
20+
// inside any `case ... when ... then ...` branch escaped reference
21+
// validation.
22+
func TestValidateMicroflowReferences_DescendsIntoEnumSplitCases(t *testing.T) {
23+
moduleID := model.ID("module-1")
24+
backend := &mock.MockBackend{
25+
IsConnectedFunc: func() bool { return true },
26+
ListModulesFunc: func() ([]*model.Module, error) {
27+
return []*model.Module{{
28+
BaseElement: model.BaseElement{ID: moduleID},
29+
Name: "SyntheticAudit",
30+
}}, nil
31+
},
32+
ListMicroflowsFunc: func() ([]*microflows.Microflow, error) {
33+
return nil, nil
34+
},
35+
}
36+
ctx, _ := newMockCtx(t, withBackend(backend))
37+
38+
stmt := &ast.CreateMicroflowStmt{
39+
Name: ast.QualifiedName{Module: "SyntheticAudit", Name: "RouteByStatus"},
40+
Body: []ast.MicroflowStatement{
41+
&ast.EnumSplitStmt{
42+
Variable: "Status",
43+
Cases: []ast.EnumSplitCase{
44+
{
45+
Values: []string{"Open"},
46+
Body: []ast.MicroflowStatement{
47+
&ast.CallMicroflowStmt{
48+
MicroflowName: ast.QualifiedName{Module: "SyntheticAudit", Name: "MissingHandler"},
49+
},
50+
},
51+
},
52+
},
53+
},
54+
},
55+
}
56+
57+
err := validate(ctx, stmt)
58+
if err == nil {
59+
t.Fatal("expected reference error for microflow inside enum split case body")
60+
}
61+
if !strings.Contains(err.Error(), "microflow not found: SyntheticAudit.MissingHandler") {
62+
t.Fatalf("unexpected error: %v", err)
63+
}
64+
}
65+
66+
// And the else body of an EnumSplitStmt must also be walked.
67+
func TestValidateMicroflowReferences_DescendsIntoEnumSplitElse(t *testing.T) {
68+
moduleID := model.ID("module-1")
69+
backend := &mock.MockBackend{
70+
IsConnectedFunc: func() bool { return true },
71+
ListModulesFunc: func() ([]*model.Module, error) {
72+
return []*model.Module{{
73+
BaseElement: model.BaseElement{ID: moduleID},
74+
Name: "SyntheticAudit",
75+
}}, nil
76+
},
77+
ListMicroflowsFunc: func() ([]*microflows.Microflow, error) {
78+
return nil, nil
79+
},
80+
}
81+
ctx, _ := newMockCtx(t, withBackend(backend))
82+
83+
stmt := &ast.CreateMicroflowStmt{
84+
Name: ast.QualifiedName{Module: "SyntheticAudit", Name: "RouteByStatus"},
85+
Body: []ast.MicroflowStatement{
86+
&ast.EnumSplitStmt{
87+
Variable: "Status",
88+
ElseBody: []ast.MicroflowStatement{
89+
&ast.CallMicroflowStmt{
90+
MicroflowName: ast.QualifiedName{Module: "SyntheticAudit", Name: "MissingFallback"},
91+
},
92+
},
93+
},
94+
},
95+
}
96+
97+
err := validate(ctx, stmt)
98+
if err == nil {
99+
t.Fatal("expected reference error for microflow inside enum split else body")
100+
}
101+
if !strings.Contains(err.Error(), "microflow not found: SyntheticAudit.MissingFallback") {
102+
t.Fatalf("unexpected error: %v", err)
103+
}
104+
}

sdk/mpr/inheritance_roundtrip_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,45 @@ func TestCastAction_RoundtripVariableName(t *testing.T) {
6363
t.Fatalf("OutputVariable = %q, want SpecificInput", parsed.OutputVariable)
6464
}
6565
}
66+
67+
// TestSerializeCastAction_UsesVariableNameFieldKey pins the BSON field key
68+
// Studio Pro emits for Microflows$CastAction. Empirical evidence (BSON
69+
// dump of the Control Centre app on Mendix 9.24): Studio Pro stores the
70+
// output variable under "VariableName", not "OutputVariableName". The
71+
// parser still falls back to "VariableName" if "OutputVariableName" is
72+
// absent (set in PR #365 to handle real Studio Pro data); the writer
73+
// must keep emitting "VariableName" so projects we produce open cleanly
74+
// in Studio Pro.
75+
func TestSerializeCastAction_UsesVariableNameFieldKey(t *testing.T) {
76+
action := &microflows.CastAction{
77+
BaseElement: model.BaseElement{ID: "cast-1"},
78+
OutputVariable: "SpecificInput",
79+
}
80+
doc := serializeMicroflowAction(action)
81+
if got := bsonGetKey(doc, "VariableName"); got != "SpecificInput" {
82+
t.Fatalf("VariableName = %v, want SpecificInput", got)
83+
}
84+
if got := bsonGetKey(doc, "OutputVariableName"); got != nil {
85+
t.Fatalf("OutputVariableName = %v, want absent (Studio Pro uses VariableName)", got)
86+
}
87+
}
88+
89+
// TestBuildSequenceFlowCase_InheritanceCase_UsesValueFieldKey pins the
90+
// BSON field key for Microflows$InheritanceCase. Empirical evidence
91+
// (BSON dump of the Control Centre app, Mendix 9.24): the entity
92+
// reference is stored under "Value" as a qualified-name string
93+
// (e.g. "Administration.Account"), not "Entity". The parser already
94+
// falls back to "Entity" for forward compatibility, but the writer must
95+
// emit "Value" so output matches Studio Pro's authored shape.
96+
func TestBuildSequenceFlowCase_InheritanceCase_UsesValueFieldKey(t *testing.T) {
97+
doc := buildSequenceFlowCase(&microflows.InheritanceCase{
98+
BaseElement: model.BaseElement{ID: "case-1"},
99+
EntityQualifiedName: "Sample.SpecializedInput",
100+
})
101+
if got := bsonGetKey(doc, "Value"); got != "Sample.SpecializedInput" {
102+
t.Fatalf("Value = %v, want Sample.SpecializedInput", got)
103+
}
104+
if got := bsonGetKey(doc, "Entity"); got != nil {
105+
t.Fatalf("Entity = %v, want absent (Studio Pro uses Value)", got)
106+
}
107+
}

0 commit comments

Comments
 (0)