diff --git a/mdl-examples/bug-tests/312-validate-skip-excluded-microflows.mdl b/mdl-examples/bug-tests/312-validate-skip-excluded-microflows.mdl new file mode 100644 index 00000000..c15bacbd --- /dev/null +++ b/mdl-examples/bug-tests/312-validate-skip-excluded-microflows.mdl @@ -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; +/ diff --git a/mdl/executor/bugfix_regression_test.go b/mdl/executor/bugfix_regression_test.go index 0e1ed366..563d542c 100644 --- a/mdl/executor/bugfix_regression_test.go +++ b/mdl/executor/bugfix_regression_test.go @@ -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) + } +} diff --git a/mdl/executor/validate.go b/mdl/executor/validate.go index 88f52f49..8621e2db 100644 --- a/mdl/executor/validate.go +++ b/mdl/executor/validate.go @@ -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 := µflowRefCollector{}