Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions mdl-examples/bug-tests/312-validate-skip-excluded-microflows.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
-- ============================================================================
-- Bug #312: Reference validation flagged broken calls inside excluded microflows
-- ============================================================================
--
-- Symptom (before fix):
-- `mxcli check --references` walked every microflow body — including ones
-- marked `@excluded` — and reported missing microflow / page / java-action
-- references in them. Excluded documents are not part of the build, and
-- Studio Pro tolerates dangling references in them, so this produced
-- false positives during agentic workflows that legitimately stash
-- broken intermediate state inside excluded scaffolding.
--
-- Root cause:
-- The validator collected references from every CreateMicroflowStmt
-- without checking the Excluded flag.
--
-- After fix:
-- `validate.go` skips reference collection for excluded microflows.
-- Included microflows are still validated normally, so the negative
-- case (typo in a real call) still fails the check.
--
-- Usage:
-- mxcli check --references mdl-examples/bug-tests/312-validate-skip-excluded-microflows.mdl -p app.mpr
-- The check must succeed: the excluded microflow's broken call to
-- BugTest312.NoSuchTarget is ignored.
--
-- To verify the negative case still triggers, remove `@excluded` from
-- the second microflow and re-run — `mxcli check --references` should
-- then report the missing reference.
-- ============================================================================

create module BugTest312;

create microflow BugTest312.MF_RealTarget ()
returns string as $msg
begin
return 'real target';
end;
/

-- Excluded scaffolding that contains a broken call. mxcli check --references
-- must NOT report this as an error.
@excluded
create microflow BugTest312.MF_ExcludedWithBrokenCall ()
returns string as $msg
begin
declare $msg string = empty;
$msg = call microflow BugTest312.NoSuchTarget();
return $msg;
end;
/

-- Negative control: an INCLUDED microflow whose call is valid. Together with
-- the excluded one above, the check confirms the validator still walks
-- non-excluded microflows.
create microflow BugTest312.MF_IncludedValid ()
returns string as $msg
begin
declare $msg string = empty;
$msg = call microflow BugTest312.MF_RealTarget();
return $msg;
end;
/
65 changes: 65 additions & 0 deletions mdl/executor/bugfix_regression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,3 +696,68 @@ func TestCallMicroflowUnknownResultTypeStillDeclaresVariable(t *testing.T) {
t.Fatal("expected Result to remain declared after unresolved call return type")
}
}

func TestValidateMicroflowReferencesSkipsExcludedMicroflow(t *testing.T) {
moduleID := model.ID("module-1")
backend := &mock.MockBackend{
IsConnectedFunc: func() bool { return true },
ListModulesFunc: func() ([]*model.Module, error) {
return []*model.Module{{
BaseElement: model.BaseElement{ID: moduleID},
Name: "SyntheticAudit",
}}, nil
},
ListMicroflowsFunc: func() ([]*microflows.Microflow, error) {
return nil, nil
},
}
ctx, _ := newMockCtx(t, withBackend(backend))

stmt := &ast.CreateMicroflowStmt{
Excluded: true,
Name: ast.QualifiedName{Module: "SyntheticAudit", Name: "ExcludedLegacyFlow"},
Body: []ast.MicroflowStatement{
&ast.CallMicroflowStmt{
MicroflowName: ast.QualifiedName{Module: "SyntheticAudit", Name: "DeletedScaffoldFlow"},
},
},
}

if err := validate(ctx, stmt); err != nil {
t.Fatalf("excluded microflow reference validation returned error: %v", err)
}
}

func TestValidateMicroflowReferencesReportsIncludedMissingMicroflow(t *testing.T) {
moduleID := model.ID("module-1")
backend := &mock.MockBackend{
IsConnectedFunc: func() bool { return true },
ListModulesFunc: func() ([]*model.Module, error) {
return []*model.Module{{
BaseElement: model.BaseElement{ID: moduleID},
Name: "SyntheticAudit",
}}, nil
},
ListMicroflowsFunc: func() ([]*microflows.Microflow, error) {
return nil, nil
},
}
ctx, _ := newMockCtx(t, withBackend(backend))

stmt := &ast.CreateMicroflowStmt{
Name: ast.QualifiedName{Module: "SyntheticAudit", Name: "IncludedFlow"},
Body: []ast.MicroflowStatement{
&ast.CallMicroflowStmt{
MicroflowName: ast.QualifiedName{Module: "SyntheticAudit", Name: "DeletedScaffoldFlow"},
},
},
}

err := validate(ctx, stmt)
if err == nil {
t.Fatal("expected missing microflow reference error")
}
if !strings.Contains(err.Error(), "microflow not found: SyntheticAudit.DeletedScaffoldFlow") {
t.Fatalf("unexpected validation error: %v", err)
}
}
6 changes: 6 additions & 0 deletions mdl/executor/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,12 @@ func validateMicroflowReferences(ctx *ExecContext, s *ast.CreateMicroflowStmt, s
if !ctx.Connected() || len(s.Body) == 0 {
return nil
}
if s.Excluded {
// Studio Pro allows excluded documents to keep stale references. Reference
// checks should not fail a roundtrip audit for microflows that are not part
// of the runnable app.
return nil
}

// Collect all references from the microflow body
refs := &microflowRefCollector{}
Expand Down