Skip to content

Commit 9918325

Browse files
hjothamendixclaude
andcommitted
fix: preserve @anchor on first statement of enum split cases
The enum split builder's case body loop never called stmtOwnAnchor on its statements, so any @anchor(from: …, to: …) authored on the first activity in a case silently dropped on exec. Describe → exec → describe stripped the FlowAnchor entirely because the split → first-case-activity flow carried only layout-default connection indices. Mirror the existing IF and inheritance-split case body loops by: - capturing thisAnchor = stmtOwnAnchor(stmt) at the top of each iteration; - applying it to the split → first-case-activity flow (the last flow that addGroupedEnumSplitFlows appended for the case value); - carrying prevAnchor across subsequent statements and applying applyUserAnchors(flow, prevAnchor, thisAnchor) on each intra-case flow. Test: TestEnumSplitBuilderPreservesFirstStatementAnchor asserts that @anchor(to: bottom) on the first case statement lands on the split → case flow's DestinationConnectionIndex. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bd66c18 commit 9918325

2 files changed

Lines changed: 76 additions & 2 deletions

File tree

mdl/executor/cmd_microflows_builder_actions.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,9 @@ func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID {
360360

361361
lastID := model.ID("")
362362
pendingCase := ""
363+
var prevAnchor *ast.FlowAnchors
363364
for _, stmt := range br.body {
365+
thisAnchor := stmtOwnAnchor(stmt)
364366
actID := fb.addStatement(stmt)
365367
if actID == "" {
366368
continue
@@ -371,14 +373,26 @@ func (fb *flowBuilder) addEnumSplit(s *ast.EnumSplitStmt) model.ID {
371373
}
372374
if lastID == "" {
373375
fb.addGroupedEnumSplitFlows(splitID, actID, br.values, i, splitX+SplitWidth+HorizontalSpacing/4, branchY)
376+
// The first statement in a case can carry @anchor(from:…,
377+
// to:…) that should apply to the split→firstActivity flow.
378+
// addGroupedEnumSplitFlows appends one flow per case value;
379+
// anchor the last one so `@anchor(to: top)` etc. round-trips
380+
// through describe → exec without silently dropping.
381+
if thisAnchor != nil && len(fb.flows) > 0 {
382+
applyUserAnchors(fb.flows[len(fb.flows)-1], nil, thisAnchor)
383+
}
374384
} else {
385+
var flow *microflows.SequenceFlow
375386
if pendingCase != "" {
376-
fb.flows = append(fb.flows, newHorizontalFlowWithCase(lastID, actID, pendingCase))
387+
flow = newHorizontalFlowWithCase(lastID, actID, pendingCase)
377388
pendingCase = ""
378389
} else {
379-
fb.flows = append(fb.flows, newHorizontalFlow(lastID, actID))
390+
flow = newHorizontalFlow(lastID, actID)
380391
}
392+
applyUserAnchors(flow, prevAnchor, thisAnchor)
393+
fb.flows = append(fb.flows, flow)
381394
}
395+
prevAnchor = thisAnchor
382396
if fb.nextConnectionPoint != "" {
383397
lastID = fb.nextConnectionPoint
384398
fb.nextConnectionPoint = ""

mdl/executor/cmd_microflows_builder_enum_split_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,63 @@ func enumSplitWithBranchCount(count int) *ast.EnumSplitStmt {
245245
Cases: cases,
246246
}
247247
}
248+
249+
// TestEnumSplitBuilderPreservesFirstStatementAnchor guards against silent
250+
// loss of @anchor(from:..., to:...) on the first statement inside an enum
251+
// split case. Before the fix the enum split builder never read
252+
// stmtOwnAnchor(stmt) for case bodies, so any round-tripped anchor dropped
253+
// on re-exec — describe → exec → describe lost the FlowAnchor entirely.
254+
func TestEnumSplitBuilderPreservesFirstStatementAnchor(t *testing.T) {
255+
fb := &flowBuilder{
256+
spacing: HorizontalSpacing,
257+
measurer: &layoutMeasurer{},
258+
}
259+
260+
// @anchor(to: bottom) on the first case statement — bottom is a
261+
// non-default destination anchor (AnchorSideBottom == 2) so we can
262+
// distinguish it from the layout default.
263+
anchor := &ast.FlowAnchors{
264+
From: ast.AnchorSideUnset,
265+
To: ast.AnchorSideBottom,
266+
}
267+
fb.addEnumSplit(&ast.EnumSplitStmt{
268+
Variable: "Status",
269+
Cases: []ast.EnumSplitCase{
270+
{
271+
Values: []string{"Open"},
272+
Body: []ast.MicroflowStatement{
273+
&ast.LogStmt{
274+
Level: ast.LogInfo,
275+
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "open"},
276+
Annotations: &ast.ActivityAnnotations{Anchor: anchor},
277+
},
278+
},
279+
},
280+
},
281+
})
282+
283+
var split *microflows.ExclusiveSplit
284+
for _, obj := range fb.objects {
285+
if s, ok := obj.(*microflows.ExclusiveSplit); ok {
286+
split = s
287+
break
288+
}
289+
}
290+
if split == nil {
291+
t.Fatal("expected ExclusiveSplit")
292+
}
293+
294+
var firstCaseFlow *microflows.SequenceFlow
295+
for _, f := range fb.flows {
296+
if f.OriginID == split.ID {
297+
firstCaseFlow = f
298+
}
299+
}
300+
if firstCaseFlow == nil {
301+
t.Fatal("expected split→case flow")
302+
}
303+
if firstCaseFlow.DestinationConnectionIndex != int(ast.AnchorSideBottom) {
304+
t.Errorf("DestinationConnectionIndex = %d, want %d — @anchor(to: bottom) was dropped",
305+
firstCaseFlow.DestinationConnectionIndex, int(ast.AnchorSideBottom))
306+
}
307+
}

0 commit comments

Comments
 (0)