From 1035d8814816ef037df20f62763822bc66dfb141 Mon Sep 17 00:00:00 2001 From: tgolembiewski Date: Fri, 25 Sep 2026 16:54:27 +0200 Subject: [PATCH] fix(lint): read an action activity's error handling from the action CONV013 reported "uses '' error handling instead of Custom" on every Java action, REST and web service call, including ones with `on error { ... }`, and CONV014 never reported `on error continue` on an action. Mendix stores error handling on the action (Microflows$*Action.ErrorHandlingType) and the reader puts it there; both rules read the activity-level field, which the reader leaves empty. DESCRIBE was right because getActionErrorHandlingType reads the action (the reflection lookup from mendixlabs/mxcli#1078). Move that lookup into sdk/microflows as ActionActivity.ErrorHandling() and ActionErrorHandlingType(), and use it in both rules and in the describer, so the two cannot drift apart again. DESCRIBE output is unchanged. Measured on a blank 11.12.1 app with four Java calls (mdl-examples/bug-tests/lint-action-error-handling.mdl): before, CONV013 on all four with '' and no CONV014; after, CONV013 on the unhandled (Rollback) and continue calls only, and CONV014 on the continue call. mendixlabs/mxcli#1202 --- .../skills/fix-issue/findings/mdl-other.jsonl | 1 + CHANGELOG.md | 2 + .../bug-tests/lint-action-error-handling.mdl | 64 +++++++++++++++++++ mdl/executor/cmd_microflows_show_helpers.go | 38 +---------- .../microflow_error_handler_authoring_test.go | 2 +- .../microflow_error_handler_roundtrip_test.go | 27 -------- mdl/linter/rules/conv_error_handling.go | 11 ++-- mdl/linter/rules/conv_error_handling_test.go | 40 ++++++++++++ sdk/microflows/error_handling.go | 49 ++++++++++++++ sdk/microflows/error_handling_test.go | 60 +++++++++++++++++ 10 files changed, 225 insertions(+), 69 deletions(-) create mode 100644 mdl-examples/bug-tests/lint-action-error-handling.mdl create mode 100644 sdk/microflows/error_handling.go create mode 100644 sdk/microflows/error_handling_test.go diff --git a/.claude/skills/fix-issue/findings/mdl-other.jsonl b/.claude/skills/fix-issue/findings/mdl-other.jsonl index a271382b25..741f80b2c0 100644 --- a/.claude/skills/fix-issue/findings/mdl-other.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-other.jsonl @@ -70,3 +70,4 @@ {"area":"mdl/executor","date":"2026-09-18","symptom":"A page parameter passed as an argument to a nanoflow/microflow BUTTON action is not wired — Studio Pro reports CE1571 \"No argument has been selected for parameter 'X' and no default is available\" on opening the page, while `mx check`, `mxcli check --references` and `mxcli lint` are all clean. Reported as an asymmetry: of two arguments, the one matching the enclosing dataview's DataSource 'works' and the other does not","cause":"Mendix stores a flow argument in one of TWO slots of Forms$MicroflowParameterMapping / Forms$NanoflowParameterMapping: a reference to a page parameter, snippet parameter or page variable goes in `Variable` as a Forms$PageVariable; a literal or expression goes in `Expression`. mxcli only ever wrote `Expression: \"$Name\"`, which binds nothing. The read side was wrong in the mirror image — the three action describers and flowSourceArgs looked for a `Name` key on that sub-document, which Forms$PageVariable does not have","file":"`sdk/pages/pages_widgets_action.go` (VariableKind on both mapping types), `mdl/executor/cmd_pages_flow_args.go` (new: classifyFlowArgValue + pageVariableArgValue), `mdl/executor/cmd_pages_builder_v3.go` (3 of the 4 copies of the $-rule), `mdl/backend/modelsdk/widget_write.go` (bindParameterMappingValue), `mdl/executor/cmd_pages_describe_output.go` + `cmd_pages_describe_datasource.go` (read)","insight":"**The reported asymmetry is a red herring — both arguments were written identically and NEITHER was bound.** Studio Pro supplies a default for the one that is the dataview's object and reports the other; 'and no default is available' in CE1571 says exactly that. Time spent on why $Dto worked is wasted. **mxbuild is not a detector here**: `mx check` on the reported project is 0 errors before AND after the fix, so the usual two-copies-of-a-real-project run proves nothing and the reporter is right that it only shows in Studio Pro. **Get the reference from a Marketplace .mpk — it contains a whole Studio Pro-authored `project.mpr`**: `mxcli marketplace download --output x.mpk && unzip -o x.mpk project.mpr`, then `mxcli bson dump` it. A blank app is useless for this (every mapping list in it is empty); Workflow Commons 4.11.0 gave 101 flow parameter mappings, of which 95 bind through Variable and 6 through Expression — and all 6 of those are Boolean literals, so the $-prefixed Expression mxcli wrote occurs ZERO times. `marketplace install` refuses that package (javasource path guard), so extract rather than install. **The PageVariable slot follows what the name refers to** (PageParameter 20, SnippetParameter 58, Widget 17) — a snippet is the COMMON case, not the corner, and `paramScope` is the right oracle because it holds only entity-typed parameters, which is the same set Mendix binds this way. **Leave $currentObject alone**: no reference for the bare form was measured and show_page already depends on the context object being inferred (MDL-PAGEARG01), so changing it on a guess risks the case that works. **The read bug hid the write bug**: describe printed `Action: microflow M.F` with no arguments for Studio Pro content, so a round-trip looked lossless and the missing binding never showed up as a diff","refs":["mendixlabs/mxcli#1140","mendixlabs/mxcli#835"],"ce":["CE1571"]} {"area": "mdl/versions", "date": "2026-09-21", "symptom": "A version gate copied from the issue text (\"Workflow Groups are GA from Mendix 11.6\") is wrong by four minors", "cause": "Mendix's release notes date the FEATURE's general availability; the metamodel floor is when the type and its property were introduced, and that is what decides whether the document loads. `Settings$WorkflowGroup` and `WorkflowsProjectSettingsPart.groups` are both `introduced: \"11.2.0\"`", "file": "`sdk/versions/mendix-11.yaml` (`workflows.groups`)", "insight": "The arbiter for a metamodel floor is the Model SDK's own StructureVersionInfo: `npm pack mendixmodelsdk && tar xzf \u2026 && grep -n '' package/src/gen/.js`, then read BOTH the class's `versionInfo.introduced` and its `properties..introduced` \u2014 a property can arrive later than its type. Release notes, proposal text and a number already written down in this repo are all downstream of it (same trap as mendixlabs/mxcli#1121). Corroborate it against two real projects rather than trusting one source: `mxcli new` at a version either side of the floor and diff the document's keys \u2014 an 11.1.0 workflows settings part has no `Groups` key at all, an 11.13.0 one carries `Groups: [2]`, which also proves the refusal is right rather than over-cautious (writing the property below the floor would be inventing a key). mendixlabs/mxcli#272", "refs": ["mendixlabs/mxcli#272"]} {"area": "mdl/linter/rules", "date": "2026-09-23", "symptom": "CONV010 flagged an ACT_ nanoflow that delegated to a sub-flow \u2014 the very thing the rule demands. A real project patched its own copy of the rule and asked for the fix upstream. An ACT_ nanoflow could satisfy CONV010 in NO way: delegate and be flagged, or inline the logic and be flagged.", "cause": "ALLOWED_ACTIONS held `MicroflowCallAction` but not `NanoflowCallAction`. `microflows()` yields nanoflows too \u2014 the catalog's `microflows` table carries a MicroflowType column \u2014 so CONV010 lints ACT_ nanoflows, and a nanoflow delegates with a nanoflow call.", "file": "`.claude/lint-rules/conv010_act_microflow_content.star` (NanoflowCallAction added to ALLOWED_ACTIONS; cmd/mxcli/lint-rules/ is gitignored and regenerated by `make sync-lint-rules`), test `mdl/catalog/lint_rule_vocabulary_test.go` (added to the `permitted` list)", "insight": "Third time this one allowlist has been short, and the rule's own comments record the previous two: the wrong vocabulary entirely (storage names vs SDK names, matching nothing, 11 false positives of 13 findings) and a missing ExclusiveMerge that a permitted ExclusiveSplit necessarily creates (122 hits on one project). The recurring shape is an UNSATISFIABLE rule, and its cost is asymmetric: a rule that cannot be satisfied does not read as a broken rule, it reads as broken CODE, so users refactor around it or patch the rule locally and the defect never comes back upstream \u2014 which is exactly what happened here until someone wrote 'report upstream' in their findings. A vocabulary pin test (TestCONV010AllowsWhatTheCatalogCallsUIActions) already existed to stop this class and did not, because its `permitted` list is hand-maintained and was itself incomplete: pinning a rule to a hand-written list of what SHOULD be allowed only moves the completeness problem. Worth considering: enumerate the delegation actions from the type system rather than listing them.", "refs": ["ako/mxcli#644"]} +{"area": "mdl/linter/rules", "date": "2026-09-25", "symptom": "`mxcli lint` reported CONV013 \"Java action call in 'M.MF' uses '' error handling instead of Custom\" on calls that DO have `on error { ... }` / `on error without rollback { ... }` -- describe shows the handler, mx check is clean -- and never reported CONV014 for `on error continue` on an action. Every Java/REST/web service call was flagged whatever its handling, so the only way to clear the warning was to ignore it", "cause": "Mendix stores error handling on the action (Microflows$*Action.ErrorHandlingType) and both readers put it there, leaving the activity-level BaseActivity.ErrorHandlingType empty. CONV013 and CONV014 read the activity field. DESCRIBE was right because getActionErrorHandlingType reads the action (the reflection lookup from #1078); the linter never used it", "file": "`sdk/microflows/error_handling.go` (new: ActionActivity.ErrorHandling, ActionErrorHandlingType); `mdl/linter/rules/conv_error_handling.go`; `mdl/executor/cmd_microflows_show_helpers.go` (getActionErrorHandlingType now calls it); example `mdl-examples/bug-tests/lint-action-error-handling.mdl`", "insight": "The rule's unit tests built the activity by hand with the handling on the ACTIVITY, the one place the reader never puts it -- so they passed while the rule was wrong on every real model. A rule test should build its input the way the reader does, or run on a model read back from disk. The accessor now lives in sdk/microflows so the describer and the linter cannot drift apart again; one consumer being right (DESCRIBE) is what hid the other being wrong. Measured on a blank 11.12.1 app with four Java calls: before, CONV013 on all four with '' and no CONV014; after, CONV013 only on the unhandled (Rollback) and the continue call, CONV014 on the continue call", "rules": ["CONV013", "CONV014"], "refs": ["#1202", "#1078"]} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0c45536f..a63c7fad51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **`@caption` on a `while` loop was dropped without a word** (mendixlabs/mxcli#1187) — `check` passed, `exec` wrote the loop, and `describe` showed it without the caption, while the same caption on a `loop` was reported as **MDL042**. Both build a `Microflows$LoopedActivity`, which has no Caption property; only the for-each case was checked. MDL042 now covers a `while` too, pointing to `@annotation`, which round-trips through `describe`. +- **Lint CONV013 flagged every Java action, REST and web service call, and CONV014 never flagged an action's `on error continue`** (mendixlabs/mxcli#1202) — CONV013 reported `uses '' error handling instead of Custom` on calls that do have `on error { … }` or `on error without rollback { … }`, while `describe` showed the handler and `mx check` was clean. Mendix stores error handling on the action, and that is where the reader puts it; both rules read the activity-level field, which the reader leaves empty. They now read it where `describe` does, through one accessor in `sdk/microflows` shared by the describer and the linter. Measured on a blank 11.12.1 app with four Java calls: before, CONV013 on all four and no CONV014; after, CONV013 on the unhandled (`Rollback`) and `continue` calls only, and CONV014 on the `continue` call. + ## [0.24.0] - 2026-09-24 Headline: **An element's storage GUID is the database's identity, and mxcli now treats it as one.** A production report of 28 attributes emptied across 607 rows by a single edit (mendixlabs/mxcli#1119) traced to five write paths that re-minted GUIDs — one of them moving 282 in a single module. They are fixed, and a new guard at the write choke point refuses any write that moves one: a class of data loss that leaves the model valid, `mx check` clean and `DESCRIBE` byte-identical, and surfaces only when the package meets a database that already holds data. Alongside it, `MOVE ENTITY` and `RENAME` stop leaving a project unbuildable, and four more scripts that passed every gate and failed the build are refused. diff --git a/mdl-examples/bug-tests/lint-action-error-handling.mdl b/mdl-examples/bug-tests/lint-action-error-handling.mdl new file mode 100644 index 0000000000..03b4ebb7ca --- /dev/null +++ b/mdl-examples/bug-tests/lint-action-error-handling.mdl @@ -0,0 +1,64 @@ +-- ============================================================================ +-- Lint: CONV013 / CONV014 read the error handling of an action activity +-- ============================================================================ +-- +-- Symptom (before fix): +-- The reader stores an action activity's error handling on the ACTION +-- (Microflows$*Action.ErrorHandlingType), where the model keeps it. CONV013 +-- and CONV014 read the activity-level field instead, which the reader +-- leaves empty. So: +-- - CONV013 flags every Java action, REST and web service call, including +-- ones with `on error { ... }`, as "uses '' error handling"; +-- - CONV014 never flags an action with `on error continue`. +-- +-- After fix: +-- Both rules read the action's handling, the same way DESCRIBE does +-- (getActionErrorHandlingType): MF_Handled and MF_HandledNoRollback +-- raise no CONV013, MF_Unhandled raises one, MF_Continue raises CONV014. +-- +-- Usage: +-- mxcli exec mdl-examples/bug-tests/lint-action-error-handling.mdl -p app.mpr +-- mxcli lint -p app.mpr (look for LintEh.* under CONV013 / CONV014) +-- ============================================================================ + +create module LintEh; + +create java action LintEh.JA_Echo ( + Text: string not null +) returns string +as $$ +return Text; +$$; +/ + +-- Custom handling with rollback: no CONV013. +create microflow LintEh.MF_Handled ($Text: string) +begin + call java action LintEh.JA_Echo (Text = $Text) on error { + log error node 'LintEh' 'echo failed'; + }; +end; +/ + +-- Custom handling without rollback: no CONV013. +create microflow LintEh.MF_HandledNoRollback ($Text: string) +begin + call java action LintEh.JA_Echo (Text = $Text) on error without rollback { + log error node 'LintEh' 'echo failed'; + }; +end; +/ + +-- No handling (the default, Rollback): CONV013. +create microflow LintEh.MF_Unhandled ($Text: string) +begin + call java action LintEh.JA_Echo (Text = $Text); +end; +/ + +-- Continue swallows the error: CONV014. +create microflow LintEh.MF_Continue ($Text: string) +begin + call java action LintEh.JA_Echo (Text = $Text) on error continue; +end; +/ diff --git a/mdl/executor/cmd_microflows_show_helpers.go b/mdl/executor/cmd_microflows_show_helpers.go index 38c18d4f08..0c4ab82737 100644 --- a/mdl/executor/cmd_microflows_show_helpers.go +++ b/mdl/executor/cmd_microflows_show_helpers.go @@ -6,7 +6,6 @@ package executor import ( "context" "fmt" - "reflect" "sort" "strconv" "strings" @@ -2284,41 +2283,8 @@ func getActionErrorHandlingType(activity *microflows.ActionActivity) microflows. return "" } - if errType := actionErrorHandlingField(activity.Action); errType != "" { - return errType - } - // Fall back to activity level for action types without ErrorHandlingType field. - return activity.ErrorHandlingType -} - -// actionErrorHandlingField reads ErrorHandlingType off any action that declares it. -// -// Reflection rather than a case per action type. The hand-maintained switch this -// replaces had drifted to 17 of the 38 action types that carry the field, and -// every one of the 21 it missed — CreateObjectAction and ChangeObjectAction -// among them — dropped that activity's whole error branch from DESCRIBE. The -// list had already been patched twice for individual instances (#863, #1020's -// neighbour) and silently regrew, which is what makes an enumeration the wrong -// shape here: a new action type must not have to be remembered. -// -// Actions embed model.BaseElement, which has no such field, so a promoted field -// cannot be picked up by accident. -func actionErrorHandlingField(action microflows.MicroflowAction) microflows.ErrorHandlingType { - v := reflect.ValueOf(action) - for v.Kind() == reflect.Ptr { - if v.IsNil() { - return "" - } - v = v.Elem() - } - if v.Kind() != reflect.Struct { - return "" - } - f := v.FieldByName("ErrorHandlingType") - if !f.IsValid() || f.Kind() != reflect.String { - return "" - } - return microflows.ErrorHandlingType(f.String()) + // On the action where Mendix stores it; the activity field for action types without one. + return activity.ErrorHandling() } // collectErrorHandlerStatements traverses the error handler flow and collects statements. diff --git a/mdl/executor/microflow_error_handler_authoring_test.go b/mdl/executor/microflow_error_handler_authoring_test.go index 2a9442a387..6ae4285abe 100644 --- a/mdl/executor/microflow_error_handler_authoring_test.go +++ b/mdl/executor/microflow_error_handler_authoring_test.go @@ -45,7 +45,7 @@ func actionErrorHandlingTypes(fb *flowBuilder) []microflows.ErrorHandlingType { var out []microflows.ErrorHandlingType for _, o := range fb.objects { if a, ok := o.(*microflows.ActionActivity); ok { - out = append(out, actionErrorHandlingField(a.Action)) + out = append(out, microflows.ActionErrorHandlingType(a.Action)) } } return out diff --git a/mdl/executor/microflow_error_handler_roundtrip_test.go b/mdl/executor/microflow_error_handler_roundtrip_test.go index f4b9dec01b..c8aad3c3c9 100644 --- a/mdl/executor/microflow_error_handler_roundtrip_test.go +++ b/mdl/executor/microflow_error_handler_roundtrip_test.go @@ -155,33 +155,6 @@ func TestDescribe_RollbackIsStillNotRendered(t *testing.T) { } } -// actionErrorHandlingField is the reflection lookup that replaced the switch. -// Pinning it directly is what makes the fix durable: a NEW action type carrying -// ErrorHandlingType is handled the moment it exists, with nothing to remember. -func TestActionErrorHandlingField(t *testing.T) { - for _, tc := range []struct { - name string - action microflows.MicroflowAction - want microflows.ErrorHandlingType - }{ - {"reads the field", µflows.CreateVariableAction{ - ErrorHandlingType: microflows.ErrorHandlingTypeCustom}, microflows.ErrorHandlingTypeCustom}, - {"empty when unset", µflows.CreateVariableAction{}, ""}, - {"action without the field", µflows.ListOperationAction{}, ""}, - {"nil action", nil, ""}, - } { - if got := actionErrorHandlingField(tc.action); got != tc.want { - t.Errorf("%s: got %q, want %q", tc.name, got, tc.want) - } - } - - // A typed-nil pointer must not panic — activity.Action can hold one. - var typedNil *microflows.CreateVariableAction - if got := actionErrorHandlingField(typedNil); got != "" { - t.Errorf("typed nil: got %q, want empty", got) - } -} - // RestOperationCallAction stores the field but Mendix refuses a custom handler on // it (CE6035), so it is the one action deliberately not reported. A reflection // lookup would otherwise pick it up — this is the case that stops the generic fix diff --git a/mdl/linter/rules/conv_error_handling.go b/mdl/linter/rules/conv_error_handling.go index b7a522c3de..d7e720297a 100644 --- a/mdl/linter/rules/conv_error_handling.go +++ b/mdl/linter/rules/conv_error_handling.go @@ -71,14 +71,15 @@ func findUnhandledCalls(objects []microflows.MicroflowObject, mf linter.Microflo continue } - // Check if error handling is not custom - if act.ErrorHandlingType != microflows.ErrorHandlingTypeCustom && - act.ErrorHandlingType != microflows.ErrorHandlingTypeCustomWithoutRollback { + // Check if error handling is not custom. It is stored on the action, not the activity. + errType := act.ErrorHandling() + if errType != microflows.ErrorHandlingTypeCustom && + errType != microflows.ErrorHandlingTypeCustomWithoutRollback { *violations = append(*violations, linter.Violation{ RuleID: r.ID(), Severity: r.DefaultSeverity(), Message: fmt.Sprintf("%s in '%s.%s' uses '%s' error handling instead of Custom.", - actionName, mf.ModuleName, mf.Name, act.ErrorHandlingType), + actionName, mf.ModuleName, mf.Name, errType), Location: linter.Location{ Module: mf.ModuleName, DocumentType: mf.DocumentNoun(), @@ -144,7 +145,7 @@ func findContinueErrorHandling(objects []microflows.MicroflowObject, mf linter.M for _, obj := range objects { switch act := obj.(type) { case *microflows.ActionActivity: - if act.ErrorHandlingType == microflows.ErrorHandlingTypeContinue { + if act.ErrorHandling() == microflows.ErrorHandlingTypeContinue { caption := act.Caption if caption == "" { caption = "(unnamed activity)" diff --git a/mdl/linter/rules/conv_error_handling_test.go b/mdl/linter/rules/conv_error_handling_test.go index 45b57cf24c..2a2a01b3e6 100644 --- a/mdl/linter/rules/conv_error_handling_test.go +++ b/mdl/linter/rules/conv_error_handling_test.go @@ -3,6 +3,7 @@ package rules import ( + "strings" "testing" "github.com/mendixlabs/mxcli/mdl/linter" @@ -229,3 +230,42 @@ func TestNoContinueErrorHandlingRule_Metadata(t *testing.T) { t.Errorf("ID = %q, want CONV014", r.ID()) } } + +// The readers store an action activity's error handling on the ACTION and leave +// the activity field empty. Before the rules read it there, a handled Java call +// was reported as "uses ” error handling" and `on error continue` on an action +// was never reported at all. +func TestFindUnhandledCalls_HandlingOnTheAction(t *testing.T) { + for _, tc := range []struct { + name string + eh microflows.ErrorHandlingType + count int + }{ + {"custom", microflows.ErrorHandlingTypeCustom, 0}, + {"custom without rollback", microflows.ErrorHandlingTypeCustomWithoutRollback, 0}, + {"rollback", microflows.ErrorHandlingTypeRollback, 1}, + } { + objects := []microflows.MicroflowObject{ + µflows.ActionActivity{Action: µflows.JavaActionCallAction{ErrorHandlingType: tc.eh}}, + } + var violations []linter.Violation + findUnhandledCalls(objects, testMicroflow(), NewErrorHandlingOnCallsRule(), &violations) + if len(violations) != tc.count { + t.Fatalf("%s: expected %d violation(s), got %d", tc.name, tc.count, len(violations)) + } + if tc.count == 1 && !strings.Contains(violations[0].Message, "'Rollback'") { + t.Errorf("%s: message should name the stored handling, got %q", tc.name, violations[0].Message) + } + } +} + +func TestFindContinueErrorHandling_ContinueOnTheAction(t *testing.T) { + objects := []microflows.MicroflowObject{ + µflows.ActionActivity{Action: µflows.MicroflowCallAction{ErrorHandlingType: microflows.ErrorHandlingTypeContinue}}, + } + var violations []linter.Violation + findContinueErrorHandling(objects, testMicroflow(), NewNoContinueErrorHandlingRule(), &violations) + if len(violations) != 1 || violations[0].RuleID != "CONV014" { + t.Fatalf("expected one CONV014 for `on error continue` stored on the action, got %v", violations) + } +} diff --git a/sdk/microflows/error_handling.go b/sdk/microflows/error_handling.go new file mode 100644 index 0000000000..3c48e8b2ac --- /dev/null +++ b/sdk/microflows/error_handling.go @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 + +package microflows + +import "reflect" + +// ErrorHandling returns how the activity handles an error in its action. +// +// Mendix stores the handling on the action (Microflows$*Action.ErrorHandlingType), +// and that is where the readers put it; the activity-level field is only a +// fallback for an action type that has no such field. Reading the activity +// field alone sees "" for every action the reader built, which made CONV013 +// flag handled Java/REST/web service calls and CONV014 miss `on error continue`. +func (a *ActionActivity) ErrorHandling() ErrorHandlingType { + if a == nil { + return "" + } + if a.Action != nil { + if errType := ActionErrorHandlingType(a.Action); errType != "" { + return errType + } + } + return a.ErrorHandlingType +} + +// ActionErrorHandlingType reads ErrorHandlingType off any action that declares it. +// +// Reflection rather than a case per action type: a hand-maintained switch had +// drifted to 17 of the 38 action types that carry the field (mendixlabs/mxcli#1078), +// so a new action type must not have to be remembered. Actions embed +// model.BaseElement, which has no such field, so a promoted field cannot be +// picked up by accident. +func ActionErrorHandlingType(action MicroflowAction) ErrorHandlingType { + v := reflect.ValueOf(action) + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return "" + } + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return "" + } + f := v.FieldByName("ErrorHandlingType") + if !f.IsValid() || f.Kind() != reflect.String { + return "" + } + return ErrorHandlingType(f.String()) +} diff --git a/sdk/microflows/error_handling_test.go b/sdk/microflows/error_handling_test.go new file mode 100644 index 0000000000..a70fa9f3b4 --- /dev/null +++ b/sdk/microflows/error_handling_test.go @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 + +package microflows + +import "testing" + +// ActionErrorHandlingType is the reflection lookup that replaced the per-type switch +// (mendixlabs/mxcli#1078). Pinning it directly is what makes it durable: a NEW action +// type carrying ErrorHandlingType is handled the moment it exists. +func TestActionErrorHandlingType(t *testing.T) { + for _, tc := range []struct { + name string + action MicroflowAction + want ErrorHandlingType + }{ + {"reads the field", &CreateVariableAction{ErrorHandlingType: ErrorHandlingTypeCustom}, ErrorHandlingTypeCustom}, + {"empty when unset", &CreateVariableAction{}, ""}, + {"action without the field", &ListOperationAction{}, ""}, + {"nil action", nil, ""}, + } { + if got := ActionErrorHandlingType(tc.action); got != tc.want { + t.Errorf("%s: got %q, want %q", tc.name, got, tc.want) + } + } + + // A typed-nil pointer must not panic — activity.Action can hold one. + var typedNil *CreateVariableAction + if got := ActionErrorHandlingType(typedNil); got != "" { + t.Errorf("typed nil: got %q, want empty", got) + } +} + +// The readers put the handling on the action and leave the activity field empty, so the +// action must win; the activity field only answers for an action type that has none. +func TestActionActivityErrorHandling(t *testing.T) { + for _, tc := range []struct { + name string + activity *ActionActivity + want ErrorHandlingType + }{ + {"on the action, as the reader stores it", + &ActionActivity{Action: &JavaActionCallAction{ErrorHandlingType: ErrorHandlingTypeCustomWithoutRollback}}, + ErrorHandlingTypeCustomWithoutRollback}, + {"the action wins over the activity", + &ActionActivity{BaseActivity: BaseActivity{ErrorHandlingType: ErrorHandlingTypeAbort}, + Action: &RestCallAction{ErrorHandlingType: ErrorHandlingTypeContinue}}, + ErrorHandlingTypeContinue}, + {"activity field for an action without one", + &ActionActivity{BaseActivity: BaseActivity{ErrorHandlingType: ErrorHandlingTypeCustom}, + Action: &ListOperationAction{}}, + ErrorHandlingTypeCustom}, + {"no action", &ActionActivity{BaseActivity: BaseActivity{ErrorHandlingType: ErrorHandlingTypeRollback}}, + ErrorHandlingTypeRollback}, + {"nil activity", nil, ""}, + } { + if got := tc.activity.ErrorHandling(); got != tc.want { + t.Errorf("%s: got %q, want %q", tc.name, got, tc.want) + } + } +}