From 5119a09249c12fc11fd43fc5b48be0c4d8c8d744 Mon Sep 17 00:00:00 2001 From: engalar Date: Mon, 30 Mar 2026 08:16:17 +0800 Subject: [PATCH] fix: resolve CE1613 and Studio Pro crash from invalid BSON (#50) Three bugs fixed: 1. Association members in CREATE/CHANGE OBJECT were misidentified as attributes (strings.Contains(".") heuristic replaced with domain model lookup via resolveMemberChange). 2. Entity types in DECLARE were treated as enumerations (bare qualified names now checked against domain model via isEntity callback). 3. CrossAssociation serialization included ParentConnection and ChildConnection fields that only exist on Association, causing Studio Pro to crash with InvalidOperationException. --- .claude/skills/debug-bson.md | 63 ++++++++++++++++++ mdl/executor/bugfix_regression_test.go | 33 ++++++++++ sdk/mpr/writer_domainmodel.go | 31 ++++----- sdk/mpr/writer_domainmodel_test.go | 89 ++++++++++++++++++++++++++ 4 files changed, 201 insertions(+), 15 deletions(-) create mode 100644 sdk/mpr/writer_domainmodel_test.go diff --git a/.claude/skills/debug-bson.md b/.claude/skills/debug-bson.md index f0d5cd3d..37ecd320 100644 --- a/.claude/skills/debug-bson.md +++ b/.claude/skills/debug-bson.md @@ -5,6 +5,8 @@ This skill provides a systematic workflow for debugging BSON serialization error ## When to Use This Skill Use when encountering: +- **Studio Pro crash** `System.InvalidOperationException: Sequence contains no matching element` at `MprProperty..ctor` +- **CE1613** "The selected attribute/enumeration no longer exists" - **CE0463** "The definition of this widget has changed" - **CE0642** "Property X is required" - **CE0091** validation errors on widget properties @@ -135,6 +137,67 @@ Templates must include both `type` (PropertyTypes schema) AND `object` (default ## Common Error Patterns +### Studio Pro Crash: InvalidOperationException in MprProperty..ctor + +**Symptom**: Studio Pro crashes when opening a project with `System.InvalidOperationException: Sequence contains no matching element` at `Mendix.Modeler.Storage.Mpr.MprProperty..ctor`. + +**Root cause**: A BSON document contains a property (field name) that does not exist in the Mendix type definition for its `$Type`. Studio Pro's `MprProperty` constructor uses `First()` to look up each BSON field in the type cache, and crashes on unrecognized fields. + +**Diagnosis workflow**: + +1. **Collect all (type, property) pairs from the crash project** (requires `pip install pymongo`): +```python +import bson, os +from collections import defaultdict + +type_props = defaultdict(set) + +def walk_bson(obj, tp): + if isinstance(obj, dict): + t = obj.get("$Type", "") + if t: + for k in obj.keys(): + if k not in ("$Type", "$ID"): + tp[t].add(k) + for v in obj.values(): + walk_bson(v, tp) + elif isinstance(obj, list): + for item in obj: + walk_bson(item, tp) + +for root, dirs, files in os.walk("mprcontents"): + for f in files: + if f.endswith(".mxunit"): + with open(os.path.join(root, f), "rb") as fh: + walk_bson(bson.decode(fh.read()), type_props) +``` + +2. **Compare against a known-good baseline project** (e.g., GenAIDemo): +```python +# Collect baseline_props the same way, then: +for t, props in crash_props.items(): + if t in baseline_props: + extra = props - baseline_props[t] + if extra: + print(f"{t}: EXTRA props = {sorted(extra)}") +``` + +3. **Extra properties = the crash cause**. The fix is to remove those fields from the writer function. + +**Example**: `DomainModels$CrossAssociation` had `ParentConnection` and `ChildConnection` copied from `DomainModels$Association`, but these fields don't exist on `CrossAssociation`. Removing them fixed the crash. + +**Key principle**: When copying serialization code between similar types (e.g., Association → CrossAssociation), always verify which fields belong to each type by checking a baseline project's BSON. + +### CE1613: Selected Attribute/Enumeration No Longer Exists + +**Symptom**: `mx check` reports `[CE1613] "The selected attribute 'Module.Entity.AssocName' no longer exists."` or `"The selected enumeration 'Module.Entity' no longer exists."` + +**Root cause**: Two variants: + +1. **Association stored as Attribute**: In `ChangeActionItem` BSON, an association name was written to the `Attribute` field instead of the `Association` field. Check the executor code that builds `MemberChange` — it must query the domain model to distinguish associations from attributes. + +2. **Entity treated as Enumeration**: In `CreateVariableAction` BSON, an entity qualified name was used as `DataTypes$EnumerationType` instead of `DataTypes$ObjectType`. Check `buildDataType()` in the visitor — bare qualified names default to `TypeEnumeration` and need catalog-based disambiguation. + ### CE0463: Widget Definition Changed **Root cause**: Object property values inconsistent with mode-dependent visibility rules. diff --git a/mdl/executor/bugfix_regression_test.go b/mdl/executor/bugfix_regression_test.go index 4fe691d4..fd13a268 100644 --- a/mdl/executor/bugfix_regression_test.go +++ b/mdl/executor/bugfix_regression_test.go @@ -390,3 +390,36 @@ func TestDeriveColumnName_CaptionSpecialChars(t *testing.T) { t.Errorf("deriveColumnName(special chars) = %q, want %q", got, "Order__ID__main") } } + +// ============================================================================= +// Issue #50: Association misidentified as Attribute (fallback without reader) +// ============================================================================= + +// TestResolveMemberChange_FallbackWithoutReader verifies that resolveMemberChange +// falls back to dot-contains heuristic when no reader is available. +// Regression: https://github.com/mendixlabs/mxcli/issues/50 +func TestResolveMemberChange_FallbackWithoutReader(t *testing.T) { + fb := &flowBuilder{ + // reader is nil — simulates no project context + } + + // Without reader: a name without dot should default to attribute + mc := µflows.MemberChange{} + fb.resolveMemberChange(mc, "Label", "Demo.Child") + if mc.AttributeQualifiedName != "Demo.Child.Label" { + t.Errorf("expected attribute 'Demo.Child.Label', got %q", mc.AttributeQualifiedName) + } + if mc.AssociationQualifiedName != "" { + t.Errorf("expected empty association, got %q", mc.AssociationQualifiedName) + } + + // With a dot in the name: should be treated as fully-qualified association (fallback) + mc2 := µflows.MemberChange{} + fb.resolveMemberChange(mc2, "Demo.Child_Parent", "Demo.Child") + if mc2.AssociationQualifiedName != "Demo.Child_Parent" { + t.Errorf("expected association 'Demo.Child_Parent', got %q", mc2.AssociationQualifiedName) + } + if mc2.AttributeQualifiedName != "" { + t.Errorf("expected empty attribute, got %q", mc2.AttributeQualifiedName) + } +} diff --git a/sdk/mpr/writer_domainmodel.go b/sdk/mpr/writer_domainmodel.go index 672082c5..109bcce0 100644 --- a/sdk/mpr/writer_domainmodel.go +++ b/sdk/mpr/writer_domainmodel.go @@ -921,22 +921,23 @@ func serializeCrossAssociation(ca *domainmodel.CrossModuleAssociation) bson.M { if storageFormat == "" { storageFormat = "Column" } + // CrossAssociation does NOT have ParentConnection/ChildConnection properties + // (unlike Association). Writing them causes Studio Pro to crash with + // InvalidOperationException in MprProperty..ctor. return bson.M{ - "$ID": idToBsonBinary(string(ca.ID)), - "$Type": "DomainModels$CrossAssociation", - "Name": ca.Name, - "Documentation": ca.Documentation, - "ExportLevel": "Hidden", - "GUID": idToBsonBinary(string(ca.ID)), - "ParentPointer": idToBsonBinary(string(ca.ParentID)), - "Child": ca.ChildRef, - "Type": string(ca.Type), - "Owner": string(ca.Owner), - "ParentConnection": "0;50", - "ChildConnection": "100;50", - "StorageFormat": storageFormat, - "Source": nil, - "DeleteBehavior": serializeDeleteBehavior(ca.ParentDeleteBehavior, ca.ChildDeleteBehavior), + "$ID": idToBsonBinary(string(ca.ID)), + "$Type": "DomainModels$CrossAssociation", + "Name": ca.Name, + "Documentation": ca.Documentation, + "ExportLevel": "Hidden", + "GUID": idToBsonBinary(string(ca.ID)), + "ParentPointer": idToBsonBinary(string(ca.ParentID)), + "Child": ca.ChildRef, + "Type": string(ca.Type), + "Owner": string(ca.Owner), + "StorageFormat": storageFormat, + "Source": nil, + "DeleteBehavior": serializeDeleteBehavior(ca.ParentDeleteBehavior, ca.ChildDeleteBehavior), } } diff --git a/sdk/mpr/writer_domainmodel_test.go b/sdk/mpr/writer_domainmodel_test.go new file mode 100644 index 00000000..4d9d4174 --- /dev/null +++ b/sdk/mpr/writer_domainmodel_test.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mpr + +import ( + "testing" + + "github.com/mendixlabs/mxcli/sdk/domainmodel" +) + +// ============================================================================= +// Issue #50: CrossAssociation must NOT include ParentConnection/ChildConnection +// ============================================================================= + +// TestSerializeCrossAssociation_NoConnectionFields verifies that +// serializeCrossAssociation does NOT emit ParentConnection or ChildConnection. +// These properties only exist on DomainModels$Association, not on +// DomainModels$CrossAssociation. Writing them causes Studio Pro to crash with +// System.InvalidOperationException: Sequence contains no matching element. +func TestSerializeCrossAssociation_NoConnectionFields(t *testing.T) { + ca := &domainmodel.CrossModuleAssociation{ + Name: "Child_Parent", + ParentID: "parent-entity-id", + ChildRef: "OtherModule.Parent", + Type: domainmodel.AssociationTypeReference, + Owner: domainmodel.AssociationOwnerDefault, + } + ca.ID = "test-cross-assoc-id" + + result := serializeCrossAssociation(ca) + + // Must NOT contain these fields + for key := range result { + if key == "ParentConnection" { + t.Error("serializeCrossAssociation must NOT include ParentConnection (only valid for Association)") + } + if key == "ChildConnection" { + t.Error("serializeCrossAssociation must NOT include ChildConnection (only valid for Association)") + } + } + + // Must contain all expected fields (exhaustive structural contract) + expectedKeys := []string{"$ID", "$Type", "Name", "Child", "ParentPointer", "Type", "Owner", + "Documentation", "ExportLevel", "GUID", "StorageFormat", "Source", "DeleteBehavior"} + for _, key := range expectedKeys { + if _, ok := result[key]; !ok { + t.Errorf("serializeCrossAssociation missing expected field %q", key) + } + } + + // $Type must be CrossAssociation + if got := result["$Type"]; got != "DomainModels$CrossAssociation" { + t.Errorf("$Type = %q, want %q", got, "DomainModels$CrossAssociation") + } +} + +// TestSerializeAssociation_HasConnectionFields verifies that the regular +// serializeAssociation DOES include ParentConnection and ChildConnection +// (to ensure we didn't accidentally remove them from the wrong function). +func TestSerializeAssociation_HasConnectionFields(t *testing.T) { + a := &domainmodel.Association{ + Name: "Child_Parent", + ParentID: "parent-entity-id", + ChildID: "child-entity-id", + Type: domainmodel.AssociationTypeReference, + Owner: domainmodel.AssociationOwnerDefault, + } + a.ID = "test-assoc-id" + + result := serializeAssociation(a) + + hasParentConn := false + hasChildConn := false + for key := range result { + if key == "ParentConnection" { + hasParentConn = true + } + if key == "ChildConnection" { + hasChildConn = true + } + } + + if !hasParentConn { + t.Error("serializeAssociation must include ParentConnection") + } + if !hasChildConn { + t.Error("serializeAssociation must include ChildConnection") + } +}