Skip to content

Commit 4c3a2c0

Browse files
committed
fix: emit boolean case values for IF flows
Symptom: MPR validation can report duplicate output variables for values declared in mutually exclusive IF branches after exec builds the graph. A follow-up targeted audit also showed CE0079/CE0773 when the new BooleanCase values reached the writer. Root cause: IF sequence flows used EnumerationCase values with the strings "true" and "false". Switching the builder to BooleanCase exposed a second gap: the BSON parser/writer did not handle Microflows$BooleanCase and the writer silently fell back to NoCase. Fix: emit BooleanCase for true/false flow cases, keep EnumerationCase for real enum branch values, and teach the MPR parser/writer to roundtrip BooleanCase instead of degrading it to NoCase. Tests: make build, make test, make lint-go.
1 parent da6f250 commit 4c3a2c0

7 files changed

Lines changed: 107 additions & 17 deletions

mdl/executor/cmd_microflows_builder_enum_split_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func TestEnumSplitNestedEmptyThenBranchKeepsContinuationCase(t *testing.T) {
111111
if flow.OriginID != nestedSplitID {
112112
continue
113113
}
114-
if value, ok := enumCaseValue(flow); ok && value == "true" {
114+
if flowCaseString(flow.CaseValue) == "true" {
115115
if _, ok := objects[flow.DestinationID].(*microflows.ExclusiveMerge); ok {
116116
return
117117
}

mdl/executor/cmd_microflows_builder_flows.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -653,10 +653,7 @@ func newHorizontalFlow(originID, destinationID model.ID) *microflows.SequenceFlo
653653
// newHorizontalFlowWithCase creates a horizontal SequenceFlow with a boolean case value (for splits)
654654
func newHorizontalFlowWithCase(originID, destinationID model.ID, caseValue string) *microflows.SequenceFlow {
655655
flow := newHorizontalFlow(originID, destinationID)
656-
flow.CaseValue = microflows.EnumerationCase{
657-
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
658-
Value: caseValue, // "true" or "false" as string
659-
}
656+
flow.CaseValue = caseValueForFlow(caseValue)
660657
return flow
661658
}
662659

@@ -678,10 +675,27 @@ func newDownwardFlowWithCase(originID, destinationID model.ID, caseValue string)
678675
DestinationID: destinationID,
679676
OriginConnectionIndex: AnchorBottom, // Connect from bottom of origin (going down)
680677
DestinationConnectionIndex: AnchorLeft, // Connect to left side of destination
681-
CaseValue: microflows.EnumerationCase{
678+
CaseValue: caseValueForFlow(caseValue),
679+
}
680+
}
681+
682+
func caseValueForFlow(caseValue string) microflows.CaseValue {
683+
switch caseValue {
684+
case "true":
685+
return microflows.BooleanCase{
682686
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
683-
Value: caseValue, // "true" or "false" as string
684-
},
687+
Value: true,
688+
}
689+
case "false":
690+
return microflows.BooleanCase{
691+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
692+
Value: false,
693+
}
694+
default:
695+
return microflows.EnumerationCase{
696+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
697+
Value: caseValue,
698+
}
685699
}
686700
}
687701

mdl/executor/cmd_microflows_guard_pattern_test.go

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,11 @@ func TestBuilder_GuardPatternPreservesFalseBranchAnchor(t *testing.T) {
4747
oc := fb.buildFlowGraph(body, nil)
4848

4949
// Find the flow from the split to the tail log. It's the only one with
50-
// an EnumerationCase Value=="false" that doesn't target an EndEvent.
50+
// a BooleanCase Value==false that doesn't target an EndEvent.
5151
var found *microflows.SequenceFlow
5252
for _, f := range oc.Flows {
53-
cv, ok := f.CaseValue.(microflows.EnumerationCase)
54-
if !ok {
55-
if p, okp := f.CaseValue.(*microflows.EnumerationCase); okp {
56-
cv = *p
57-
ok = true
58-
}
59-
}
60-
if !ok || cv.Value != "false" {
53+
cv, ok := f.CaseValue.(microflows.BooleanCase)
54+
if !ok || cv.Value {
6155
continue
6256
}
6357
// Exclude flows pointing at an EndEvent.
@@ -85,3 +79,31 @@ func TestBuilder_GuardPatternPreservesFalseBranchAnchor(t *testing.T) {
8579
t.Errorf("destination: got %d, want %d (Top)", found.DestinationConnectionIndex, AnchorTop)
8680
}
8781
}
82+
83+
func TestCaseValueForFlowUsesBooleanCaseForBooleanBranches(t *testing.T) {
84+
for _, tc := range []struct {
85+
value string
86+
want bool
87+
}{
88+
{value: "true", want: true},
89+
{value: "false", want: false},
90+
} {
91+
got, ok := caseValueForFlow(tc.value).(microflows.BooleanCase)
92+
if !ok {
93+
t.Fatalf("caseValueForFlow(%q) = %T, want BooleanCase", tc.value, caseValueForFlow(tc.value))
94+
}
95+
if got.Value != tc.want {
96+
t.Fatalf("caseValueForFlow(%q).Value = %v, want %v", tc.value, got.Value, tc.want)
97+
}
98+
}
99+
}
100+
101+
func TestCaseValueForFlowKeepsEnumValuesAsEnumerationCase(t *testing.T) {
102+
got, ok := caseValueForFlow("Submitted").(microflows.EnumerationCase)
103+
if !ok {
104+
t.Fatalf("caseValueForFlow(enum) = %T, want EnumerationCase", caseValueForFlow("Submitted"))
105+
}
106+
if got.Value != "Submitted" {
107+
t.Fatalf("enum case value = %q, want Submitted", got.Value)
108+
}
109+
}

sdk/mpr/parser_microflow.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,13 @@ func parseCaseValue(raw any) microflows.CaseValue {
223223
Value: val,
224224
}
225225
}
226+
case "Microflows$BooleanCase":
227+
if val, ok := caseMap["Value"].(bool); ok {
228+
return &microflows.BooleanCase{
229+
BaseElement: model.BaseElement{ID: id},
230+
Value: val,
231+
}
232+
}
226233
}
227234
return nil
228235
}

sdk/mpr/parser_microflow_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,27 @@ func TestParseSequenceFlow_NewCaseValueEnumerationCase(t *testing.T) {
3535
}
3636
}
3737

38+
func TestParseSequenceFlow_NewCaseValueBooleanCase(t *testing.T) {
39+
flow := parseSequenceFlow(map[string]any{
40+
"$ID": "flow-1",
41+
"OriginPointer": "start-1",
42+
"DestinationPointer": "dest-1",
43+
"NewCaseValue": primitive.D{
44+
{Key: "$ID", Value: "case-1"},
45+
{Key: "$Type", Value: "Microflows$BooleanCase"},
46+
{Key: "Value", Value: false},
47+
},
48+
})
49+
50+
got, ok := flow.CaseValue.(*microflows.BooleanCase)
51+
if !ok {
52+
t.Fatalf("expected *BooleanCase, got %T", flow.CaseValue)
53+
}
54+
if got.Value {
55+
t.Fatal("expected false branch")
56+
}
57+
}
58+
3859
func TestParseSequenceFlow_NewCaseValueNoCase(t *testing.T) {
3960
flow := parseSequenceFlow(map[string]any{
4061
"$ID": "flow-1",

sdk/mpr/writer_microflow.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ func buildSequenceFlowCase(cv microflows.CaseValue) bson.D {
228228
cv = &c
229229
case microflows.NoCase:
230230
cv = &c
231+
case microflows.BooleanCase:
232+
cv = &c
231233
}
232234

233235
switch c := cv.(type) {
@@ -250,6 +252,16 @@ func buildSequenceFlowCase(cv microflows.CaseValue) bson.D {
250252
{Key: "$ID", Value: idToBsonBinary(id)},
251253
{Key: "$Type", Value: "Microflows$NoCase"},
252254
}
255+
case *microflows.BooleanCase:
256+
id := string(c.ID)
257+
if id == "" {
258+
id = generateUUID()
259+
}
260+
return bson.D{
261+
{Key: "$ID", Value: idToBsonBinary(id)},
262+
{Key: "$Type", Value: "Microflows$BooleanCase"},
263+
{Key: "Value", Value: c.Value},
264+
}
253265
}
254266
// Default: synthesise a NoCase document with a fresh ID.
255267
return bson.D{

sdk/mpr/writer_microflow_version_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,17 @@ func TestBuildSequenceFlowCase_NormalisesValueReceiver(t *testing.T) {
168168
t.Error("value and pointer NoCase must produce identical $Type")
169169
}
170170
}
171+
172+
func TestBuildSequenceFlowCase_BooleanCase(t *testing.T) {
173+
doc := buildSequenceFlowCase(microflows.BooleanCase{
174+
BaseElement: model.BaseElement{ID: "case-false"},
175+
Value: false,
176+
})
177+
178+
if got := bsonGetKey(doc, "$Type"); got != "Microflows$BooleanCase" {
179+
t.Fatalf("$Type = %v, want Microflows$BooleanCase", got)
180+
}
181+
if got := bsonGetKey(doc, "Value"); got != false {
182+
t.Fatalf("Value = %v, want false", got)
183+
}
184+
}

0 commit comments

Comments
 (0)