Skip to content

Commit 839eb7d

Browse files
committed
fix: keep output handlers away from declare dependencies
Symptom: a custom error handler for an output-producing action could rejoin before a later DECLARE whose initial value referenced that output variable. Studio Pro then rejected the roundtripped microflow because the output variable is not in scope on the error-handler path. Root cause: skip-variable routing collected references from many statement kinds, but omitted DECLARE initial values. Fix: include DECLARE initial values in statement reference analysis so handlers wait for, or terminate before, output-dependent declarations. Tests: add a graph-level regression that verifies the error-handler path cannot reach an output-dependent DECLARE, plus the existing custom-handler routing tests via make build and make test.
1 parent f31ed84 commit 839eb7d

2 files changed

Lines changed: 74 additions & 0 deletions

File tree

mdl/executor/cmd_microflows_builder_flows.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,8 @@ func statementsReferenceVar(stmts []ast.MicroflowStatement, varName string) bool
319319
func statementVarRefs(stmt ast.MicroflowStatement) []string {
320320
var refs []string
321321
switch s := stmt.(type) {
322+
case *ast.DeclareStmt:
323+
refs = append(refs, exprVarRefs(s.InitialValue)...)
322324
case *ast.ReturnStmt:
323325
refs = append(refs, exprVarRefs(s.Value)...)
324326
case *ast.LogStmt:

mdl/executor/cmd_microflows_builder_terminal_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,78 @@ func TestBuildFlowGraph_EmptyOutputHandlerTerminatesBeforeOutputDependentTail(t
276276
}
277277
}
278278

279+
func TestBuildFlowGraph_OutputHandlerTerminatesBeforeDeclareReferencingOutput(t *testing.T) {
280+
body := []ast.MicroflowStatement{
281+
&ast.CallMicroflowStmt{
282+
OutputVariable: "CreatedRecord",
283+
MicroflowName: ast.QualifiedName{Module: "SampleSync", Name: "CreateRecord"},
284+
ErrorHandling: &ast.ErrorHandlingClause{
285+
Type: ast.ErrorHandlingCustomWithoutRollback,
286+
Body: []ast.MicroflowStatement{
287+
&ast.LogStmt{Level: ast.LogError, Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "create failed"}},
288+
},
289+
},
290+
},
291+
&ast.DeclareStmt{
292+
Variable: "SuccessMessage",
293+
Type: ast.DataType{Kind: ast.TypeString},
294+
InitialValue: &ast.BinaryExpr{
295+
Left: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "Created "},
296+
Operator: "+",
297+
Right: &ast.AttributePathExpr{Variable: "CreatedRecord", Path: []string{"Name"}},
298+
},
299+
},
300+
}
301+
302+
if !statementReferencesVar(body[1], "CreatedRecord") {
303+
t.Fatal("DECLARE initial values must be visible to custom handler skip-var routing")
304+
}
305+
306+
fb := &flowBuilder{posX: 100, posY: 100, spacing: HorizontalSpacing, measurer: &layoutMeasurer{}}
307+
oc := fb.buildFlowGraph(body, nil)
308+
309+
var callID, declareID model.ID
310+
endIDs := map[model.ID]bool{}
311+
for _, obj := range oc.Objects {
312+
switch o := obj.(type) {
313+
case *microflows.ActionActivity:
314+
switch action := o.Action.(type) {
315+
case *microflows.MicroflowCallAction:
316+
if action.ResultVariableName == "CreatedRecord" {
317+
callID = o.ID
318+
}
319+
case *microflows.CreateVariableAction:
320+
if action.VariableName == "SuccessMessage" {
321+
declareID = o.ID
322+
}
323+
}
324+
case *microflows.EndEvent:
325+
endIDs[o.ID] = true
326+
}
327+
}
328+
if callID == "" || declareID == "" || len(endIDs) == 0 {
329+
t.Fatalf("expected call, output-dependent declare, and end event; got call=%q declare=%q ends=%v", callID, declareID, endIDs)
330+
}
331+
332+
var errorFlowTerminates bool
333+
for _, flow := range oc.Flows {
334+
if !flow.IsErrorHandler || flow.OriginID != callID {
335+
continue
336+
}
337+
if flowPathExists(oc.Flows, flow.DestinationID, declareID) {
338+
t.Fatal("custom handler must not rejoin before a DECLARE that reads the missing output")
339+
}
340+
for endID := range endIDs {
341+
if flow.DestinationID == endID || flowPathExists(oc.Flows, flow.DestinationID, endID) {
342+
errorFlowTerminates = true
343+
}
344+
}
345+
}
346+
if !errorFlowTerminates {
347+
t.Fatal("custom handler should terminate at an EndEvent before the output-dependent declare")
348+
}
349+
}
350+
279351
func TestBuildFlowGraph_EmptyNoOutputHandlerRejoinsAtNextAction(t *testing.T) {
280352
body := []ast.MicroflowStatement{
281353
&ast.CallMicroflowStmt{

0 commit comments

Comments
 (0)