diff --git a/.claude/skills/fix-issue.md b/.claude/skills/fix-issue.md index 6ad9739ce..f268240eb 100644 --- a/.claude/skills/fix-issue.md +++ b/.claude/skills/fix-issue.md @@ -16,6 +16,8 @@ to the symptom table below, so the next similar issue costs fewer reads. | Symptom | Root cause layer | First file to open | Fix pattern | |---------|-----------------|-------------------|-------------| +| `create or modify external entities` silently resets a per-entity setting the user had changed (e.g. allow-create-change-locally) | `applyExternalEntityFields` stamps every field on both the create and the update path, so anything not derivable from the OData contract was overwritten with a default | `mdl/executor/cmd_contract.go` (`applyExternalEntityFields`) | Separate contract-derived fields (Countable/Creatable/Deletable/Skip/Top — refresh from metadata) from local modelling choices (CreateChangeLocally — leave alone; a new entity arrives zero-valued, which is Mendix's default). Issue #782 | +| An **external (OData) entity** loses its remote settings on any read-modify-write — `describe external entity` says `is not an external entity (source: )`, and `alter entity … set allow_create_change_locally = true` reports success but the flag stays off. Works under `--engine legacy` | `entityFromGen` recognised only `DomainModels$OqlViewEntitySource`, so the three `Rest$OData*` sources read back as no source at all: `Source` empty, every remote field zeroed. The write path was fine — it switches on `e.Source`, which the read never populated | `mdl/backend/modelsdk/domainmodel.go` (`entityFromGen`'s source switch, `odataKeyFromGen`) | Mirror the legacy parser (`sdk/mpr/parser_domainmodel.go`) for all three flavours: RemoteEntitySource (capabilities + CreateChangeLocally + key), EntityTypeSource (type name + IsOpen + key), PrimitiveCollectionEntitySource (service only). `Updatable` has no gen accessor and the writer does not emit it — leaving it zero is symmetric. **Check the read side first when a write-path field "does not stick"**: a switch on a field the read never fills looks like a write bug. Repro `mdl-examples/bug-tests/782-external-entity-create-change-locally.mdl`. Issue #782 | | A pluggable widget's datasource (or child widget, or client action) is **silently dropped at write time** — `exec` prints `Created page` but the widget lands with the piece missing, and only a `log` line mentions `not yet supported — rerun with MXCLI_ENGINE=legacy` | The converter *does* return an error, but it travels through `widgetobj.ChildSerializer`, whose methods return BSON with no error channel (the `TODO(shared-types)` in `mdl/backend/widgetobj/builder.go`), so the caller logged it and returned nil | `mdl/backend/modelsdk/widget_pluggable_write.go` (`recordChildSerializeErr`, `takeChildSerializeErr`) + the drains in `page_write.go` / `snippet_write.go` | Record the failure in a package-level accumulator and drain it at every page/snippet write entry point, so the statement fails instead of the write succeeding with data missing (ADR-0004: refuse, don't drop). The real fix is the deferred `ChildSerializer` interface change; until then, any **new** write entry point that builds pluggable widgets must drain too. Repro `mdl-examples/bug-tests/795b-flow-datasource-context-entity.mdl` | | `describe page` reports the wrong context entity under a data container bound to a microflow/nanoflow — `-- Context: $currentObject (Module.GetOrders)` names the *flow* instead of the entity it returns | `widget.EntityContext = widget.DataSource.Reference` is correct for a database source (reference *is* the entity) and wrong for a flow source (reference is the flow's qualified name) | `mdl/executor/cmd_pages_describe_flowcontext.go` (`dataSourceEntityContext`, `flowReturnEntity`) + the five assignment sites in `cmd_pages_describe_parse.go` | Resolve the flow's return type via `ListMicroflows`/`ListNanoflows` + `getHierarchy().GetQualifiedName`, taking the entity from an Object/List return type; fall back to the reference when the flow is unresolvable or returns a scalar, so the result is never worse than before. Note `GetRawUnitByName` is unimplemented on the modelsdk engine — the list+hierarchy path is the one that works | | `describe page` omits a **pluggable** widget's `DataSource` when it is bound to a microflow (`datagrid g1 {` with no DataSource), while a `database from` source describes fine — re-applying the output recreates the grid unbound | A `Forms$MicroflowSource` stores the name in the nested `Forms$MicroflowSettings` (`MicroflowSettings` → `Microflow`), which is what the write path and Studio Pro emit; the reader looked up a top-level `Microflow` key, got `""`, and returned no datasource. The describe *formatter* was correct all along — read bug only | `mdl/executor/cmd_pages_describe_pluggable.go` (`microflowSourceRef`, `nanoflowSourceRef`, `extractDataGrid2DataSource`, `extractGalleryDataSource`, `parseCustomWidgetDataSource`) | Read the nested settings with a top-level fallback, via one shared helper — there were four divergent copies of this lookup and two were wrong. `Forms$NanoflowSource` was missing entirely from the DataGrid2/Gallery switches; add it alongside. Do **not** touch `CustomWidgets$CustomWidgetNanoflowSource` (a different metamodel type whose `Nanoflow` really is top-level) or the `Forms$MicroflowAction` reads (actions, not datasources). Repro `mdl-examples/bug-tests/795-datagrid-microflow-datasource-describe.mdl`. Issue #795 | diff --git a/mdl-examples/bug-tests/782-external-entity-create-change-locally.mdl b/mdl-examples/bug-tests/782-external-entity-create-change-locally.mdl new file mode 100644 index 000000000..1a89e43b0 --- /dev/null +++ b/mdl-examples/bug-tests/782-external-entity-create-change-locally.mdl @@ -0,0 +1,59 @@ +-- Bug #782: External Entities — allow_create_change_locally doesn't work +-- +-- Symptom: after importing entities from a consumed OData service, enabling +-- "Allow creating and changing objects locally" left the flag at false. +-- +-- Two independent causes, both fixed: +-- +-- (1) READ. entityFromGen (mdl/backend/modelsdk/domainmodel.go) recognised only +-- DomainModels$OqlViewEntitySource, so an OData external entity read back +-- with an empty Source and every remote field zeroed. `describe external +-- entity` rejected it outright ("is not an external entity (source: )"), and +-- `alter entity … set allow_create_change_locally = true` wrote the flag onto +-- a model that no longer knew the entity was external, so it was lost. The +-- legacy engine parsed all three OData source flavours, so this was a +-- modelsdk-engine gap — and modelsdk is the default engine. +-- +-- (2) RE-IMPORT. applyExternalEntityFields (mdl/executor/cmd_contract.go) stamped +-- CreateChangeLocally = false on every import, including updates of existing +-- entities, so `create or modify external entities` reset whatever the user +-- had set. Unlike Countable/Creatable/Deletable it cannot be derived from the +-- service contract — it is a local modelling choice — so it is now left alone +-- on the top-level branch. +-- +-- Manual verification (needs a project; the metadata file ships with the repo): +-- +-- cp mdl-examples/odata-local-metadata/sample-metadata.xml /path/to/app/ +-- mxcli exec 782-external-entity-create-change-locally.mdl -p app.mpr +-- +-- Expect the final DESCRIBE to report `AllowCreateChangeLocally: Yes`. Before the +-- fix the first DESCRIBE errored with "is not an external entity", and after the +-- re-import the flag was back to No. +-- +-- Adjust MetadataUrl below to wherever sample-metadata.xml sits. + +create module Issue782; +create module role Issue782.User; + +create constant Issue782.SvcUrl + type string + default 'https://services.odata.org/V4/Northwind/Northwind.svc/'; + +create odata client Issue782.Sample ( + MetadataUrl: './mdl-examples/odata-local-metadata/sample-metadata.xml', + ServiceUrl: '@Issue782.SvcUrl' +); + +create external entities from Issue782.Sample into Issue782; + +-- (1) The entity must read back as external at all. +describe external entity Issue782.Products; + +-- The reported flow: turn the flag on, and it must stick. +alter entity Issue782.Products set allow_create_change_locally = true; +describe external entity Issue782.Products; + +-- (2) A re-import refreshes the contract-derived capabilities but must not reset +-- the local-changes flag. +create or modify external entities from Issue782.Sample into Issue782; +describe external entity Issue782.Products; diff --git a/mdl/backend/modelsdk/domainmodel.go b/mdl/backend/modelsdk/domainmodel.go index 1635f8449..a8d61fa80 100644 --- a/mdl/backend/modelsdk/domainmodel.go +++ b/mdl/backend/modelsdk/domainmodel.go @@ -7,6 +7,7 @@ import ( "github.com/mendixlabs/mxcli/modelsdk/element" genDm "github.com/mendixlabs/mxcli/modelsdk/gen/domainmodels" + genRest "github.com/mendixlabs/mxcli/modelsdk/gen/rest" genTexts "github.com/mendixlabs/mxcli/modelsdk/gen/texts" "github.com/mendixlabs/mxcli/modelsdk/mprread" @@ -160,14 +161,52 @@ func entityFromGen(e *genDm.Entity) *domainmodel.Entity { out.Location = parseLocation(e.Location()) - // View entities carry an OqlViewEntitySource referencing their source - // document by qualified name; surface it so read-modify-write paths (e.g. - // MOVE ENTITY, which must reparent the source doc) can see it. - if src, ok := e.Source().(*genDm.OqlViewEntitySource); ok { + // An entity's Source says where its data comes from. Surface each flavour so + // read-modify-write paths (MOVE ENTITY reparenting a source doc, ALTER ENTITY + // flipping a remote capability) can see it — without this an external entity + // reads back looking local, DESCRIBE EXTERNAL ENTITY rejects it, and an update + // rebuilds it with no source at all (mendixlabs/mxcli#782). Mirrors the legacy + // parser in sdk/mpr/parser_domainmodel.go. + switch src := e.Source().(type) { + case *genDm.OqlViewEntitySource: out.Source = "DomainModels$OqlViewEntitySource" out.SourceObjectID = model.ID(src.ID()) out.SourceDocumentRef = src.SourceDocumentQualifiedName() out.OqlQuery = src.Oql() + + case *genRest.ODataRemoteEntitySource: + // Top-level external entity: has its own entity set, so it carries the + // CRUD/paging capabilities and the local-changes flag. + out.Source = "Rest$ODataRemoteEntitySource" + out.SourceObjectID = model.ID(src.ID()) + out.RemoteServiceName = src.SourceDocumentQualifiedName() + out.RemoteEntitySet = src.EntitySet() + out.RemoteEntityName = src.RemoteName() + out.Countable = src.Countable() + out.Creatable = src.Creatable() + out.Deletable = src.Deletable() + out.SkipSupported = src.SkipSupported() + out.TopSupported = src.TopSupported() + out.CreateChangeLocally = src.CreateChangeLocally() + out.RemoteKeyParts = odataKeyFromGen(src.Key()) + // Updatable has no gen accessor because the storage type has no such + // field — updatability is per attribute (Rest$ODataMappedValue). The + // write path does not emit it either, so leaving it zero is symmetric. + + case *genRest.ODataEntityTypeSource: + // Derived / abstract / contained target type: no entity set, no capabilities. + out.Source = "Rest$ODataEntityTypeSource" + out.SourceObjectID = model.ID(src.ID()) + out.RemoteServiceName = src.SourceDocumentQualifiedName() + out.RemoteEntityName = src.EntityTypeName() + out.IsOpen = src.IsOpen() + out.RemoteKeyParts = odataKeyFromGen(src.Key()) + + case *genRest.ODataPrimitiveCollectionEntitySource: + // NPE generated for a Collection(Edm.*) property; carries only the service. + out.Source = "Rest$ODataPrimitiveCollectionEntitySource" + out.SourceObjectID = model.ID(src.ID()) + out.RemoteServiceName = src.SourceDocumentQualifiedName() } for _, el := range e.AttributesItems() { @@ -341,6 +380,32 @@ func indexFromGen(idx *genDm.Index) *domainmodel.Index { // attributeTypeFromGen is the reverse of attributeTypeToGen: a gen attribute-type // element back to a domainmodel.AttributeType (with Length / enumeration ref). +// odataKeyFromGen converts a Rest$ODataKey to the semantic remote-key parts. The +// inverse of odataKeyToGen in domainmodel_write.go. +func odataKeyFromGen(key element.Element) []*domainmodel.RemoteKeyPart { + k, ok := key.(*genRest.ODataKey) + if !ok || k == nil { + return nil + } + var parts []*domainmodel.RemoteKeyPart + for _, el := range k.PartsItems() { + p, ok := el.(*genRest.ODataKeyPart) + if !ok { + continue + } + kp := &domainmodel.RemoteKeyPart{ + Name: p.EntityKeyPartName(), + RemoteName: p.Name(), + RemoteType: p.RemoteType(), + } + if t := p.Type(); t != nil { + kp.Type = attributeTypeFromGen(t) + } + parts = append(parts, kp) + } + return parts +} + func attributeTypeFromGen(t element.Element) domainmodel.AttributeType { switch at := t.(type) { case *genDm.StringAttributeType: diff --git a/mdl/backend/modelsdk/external_entity_read_test.go b/mdl/backend/modelsdk/external_entity_read_test.go new file mode 100644 index 000000000..556773014 --- /dev/null +++ b/mdl/backend/modelsdk/external_entity_read_test.go @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: Apache-2.0 + +// mendixlabs/mxcli#782: an external entity's `allow_create_change_locally` flag +// would not stick. The write path handled it; the *read* did not. entityFromGen +// recognised only DomainModels$OqlViewEntitySource, so an OData external entity +// came back with an empty Source and every remote field zeroed. Setting the flag +// then wrote it onto a model that no longer knew it was external, and the value +// was lost — as were describe, and any other read-modify-write on such an entity. +// +// The legacy engine (sdk/mpr/parser_domainmodel.go) parsed all three OData source +// flavours, so this was a modelsdk-engine gap, not a missing feature. +package modelsdkbackend + +import ( + "testing" + + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/domainmodel" +) + +// externalEntityFixture creates an OData external entity through the write path, +// then reads the domain model back with a fresh connection. Round-tripping through +// disk is the point: the read is what #782 broke. +func externalEntityFixture(t *testing.T, mutate func(*domainmodel.Entity)) (proj string, moduleID model.ID) { + t.Helper() + proj = copyFixture(t) + + b := New() + if err := b.Connect(proj); err != nil { + t.Fatalf("connect: %v", err) + } + mod, err := b.GetModuleByName("MyFirstModule") + if err != nil || mod == nil { + t.Fatalf("GetModuleByName: %v, %v", mod, err) + } + dm, err := b.GetDomainModel(mod.ID) + if err != nil || dm == nil { + t.Fatalf("GetDomainModel: %v, %v", dm, err) + } + + ent := &domainmodel.Entity{ + Name: "Products", + Persistable: true, + Source: "Rest$ODataRemoteEntitySource", + RemoteServiceName: "MyFirstModule.Sample", + RemoteEntitySet: "Products", + RemoteEntityName: "Product", + Countable: true, + Creatable: true, + Deletable: true, + SkipSupported: true, + TopSupported: true, + CreateChangeLocally: true, + RemoteKeyParts: []*domainmodel.RemoteKeyPart{{ + Name: "ProductID", + RemoteName: "ProductID", + RemoteType: "Edm.Int32", + Type: &domainmodel.IntegerAttributeType{}, + }}, + Attributes: []*domainmodel.Attribute{ + {Name: "ProductName", Type: &domainmodel.StringAttributeType{}}, + }, + } + if mutate != nil { + mutate(ent) + } + if err := b.CreateEntity(dm.ID, ent); err != nil { + t.Fatalf("CreateEntity: %v", err) + } + if err := b.Disconnect(); err != nil { + t.Fatalf("disconnect: %v", err) + } + return proj, mod.ID +} + +func readEntity(t *testing.T, proj string, moduleID model.ID, name string) *domainmodel.Entity { + t.Helper() + b := New() + if err := b.Connect(proj); err != nil { + t.Fatalf("reconnect: %v", err) + } + t.Cleanup(func() { _ = b.Disconnect() }) + dm, err := b.GetDomainModel(moduleID) + if err != nil || dm == nil { + t.Fatalf("GetDomainModel: %v, %v", dm, err) + } + for _, e := range dm.Entities { + if e.Name == name { + return e + } + } + t.Fatalf("entity %s not found after write", name) + return nil +} + +// TestExternalEntity_RemoteSourceRoundTrip is the regression test: every field of +// a Rest$ODataRemoteEntitySource must survive a write→read cycle. +func TestExternalEntity_RemoteSourceRoundTrip(t *testing.T) { + proj, modID := externalEntityFixture(t, nil) + got := readEntity(t, proj, modID, "Products") + + if got.Source != "Rest$ODataRemoteEntitySource" { + t.Fatalf("Source = %q, want Rest$ODataRemoteEntitySource — the entity does not read back as external", got.Source) + } + if !got.CreateChangeLocally { + t.Error("CreateChangeLocally = false, want true (#782)") + } + if got.RemoteServiceName != "MyFirstModule.Sample" { + t.Errorf("RemoteServiceName = %q", got.RemoteServiceName) + } + if got.RemoteEntitySet != "Products" { + t.Errorf("RemoteEntitySet = %q", got.RemoteEntitySet) + } + if got.RemoteEntityName != "Product" { + t.Errorf("RemoteEntityName = %q", got.RemoteEntityName) + } + if !got.Countable || !got.Creatable || !got.Deletable || !got.SkipSupported || !got.TopSupported { + t.Errorf("capability flags lost: countable=%v creatable=%v deletable=%v skip=%v top=%v", + got.Countable, got.Creatable, got.Deletable, got.SkipSupported, got.TopSupported) + } + if got.SourceObjectID == "" { + t.Error("SourceObjectID empty — a read-modify-write cannot preserve the source element") + } + if len(got.RemoteKeyParts) != 1 { + t.Fatalf("RemoteKeyParts = %+v, want 1 part", got.RemoteKeyParts) + } + kp := got.RemoteKeyParts[0] + if kp.Name != "ProductID" || kp.RemoteName != "ProductID" || kp.RemoteType != "Edm.Int32" { + t.Errorf("key part = %+v", kp) + } + if _, ok := kp.Type.(*domainmodel.IntegerAttributeType); !ok { + t.Errorf("key part type = %T, want *IntegerAttributeType", kp.Type) + } +} + +// TestExternalEntity_FlagSurvivesReadModifyWrite reproduces the reported flow: +// read the entity, flip the flag (what ALTER ENTITY … SET +// ALLOW_CREATE_CHANGE_LOCALLY does), write it back, read again. +func TestExternalEntity_FlagSurvivesReadModifyWrite(t *testing.T) { + proj, modID := externalEntityFixture(t, func(e *domainmodel.Entity) { + e.CreateChangeLocally = false // imported off, as CREATE EXTERNAL ENTITIES leaves it + }) + + b := New() + if err := b.Connect(proj); err != nil { + t.Fatalf("connect: %v", err) + } + dm, err := b.GetDomainModel(modID) + if err != nil { + t.Fatalf("GetDomainModel: %v", err) + } + var ent *domainmodel.Entity + for _, e := range dm.Entities { + if e.Name == "Products" { + ent = e + } + } + if ent == nil { + t.Fatal("Products not found") + } + ent.CreateChangeLocally = true + if err := b.UpdateEntity(dm.ID, ent); err != nil { + t.Fatalf("UpdateEntity: %v", err) + } + if err := b.Disconnect(); err != nil { + t.Fatalf("disconnect: %v", err) + } + + got := readEntity(t, proj, modID, "Products") + if !got.CreateChangeLocally { + t.Error("CreateChangeLocally = false after setting it to true (#782)") + } + // The rest of the source must not have been damaged by the update. + if got.Source != "Rest$ODataRemoteEntitySource" || got.RemoteEntitySet != "Products" { + t.Errorf("source damaged by the update: Source=%q EntitySet=%q", got.Source, got.RemoteEntitySet) + } + if len(got.RemoteKeyParts) != 1 { + t.Errorf("remote key lost by the update: %+v", got.RemoteKeyParts) + } +} + +// TestExternalEntity_EntityTypeSourceRoundTrip covers the second flavour: a +// derived/abstract/contained type, which has no entity set. +func TestExternalEntity_EntityTypeSourceRoundTrip(t *testing.T) { + proj, modID := externalEntityFixture(t, func(e *domainmodel.Entity) { + e.Name = "ProductDetail" + e.Source = "Rest$ODataEntityTypeSource" + e.Persistable = false + e.IsOpen = true + e.RemoteEntitySet = "" + }) + got := readEntity(t, proj, modID, "ProductDetail") + + if got.Source != "Rest$ODataEntityTypeSource" { + t.Fatalf("Source = %q, want Rest$ODataEntityTypeSource", got.Source) + } + if got.RemoteEntityName != "Product" { + t.Errorf("RemoteEntityName = %q, want Product", got.RemoteEntityName) + } + if !got.IsOpen { + t.Error("IsOpen = false, want true") + } + if got.RemoteServiceName != "MyFirstModule.Sample" { + t.Errorf("RemoteServiceName = %q", got.RemoteServiceName) + } + if len(got.RemoteKeyParts) != 1 { + t.Errorf("RemoteKeyParts = %+v, want 1 part", got.RemoteKeyParts) + } +} + +// TestExternalEntity_PrimitiveCollectionSourceRoundTrip covers the third flavour, +// the NPE generated for a Collection(Edm.*) property. +func TestExternalEntity_PrimitiveCollectionSourceRoundTrip(t *testing.T) { + proj, modID := externalEntityFixture(t, func(e *domainmodel.Entity) { + e.Name = "ProductTag" + e.Source = "Rest$ODataPrimitiveCollectionEntitySource" + e.Persistable = false + e.RemoteEntitySet = "" + e.RemoteKeyParts = nil + }) + got := readEntity(t, proj, modID, "ProductTag") + + if got.Source != "Rest$ODataPrimitiveCollectionEntitySource" { + t.Fatalf("Source = %q, want Rest$ODataPrimitiveCollectionEntitySource", got.Source) + } + if got.RemoteServiceName != "MyFirstModule.Sample" { + t.Errorf("RemoteServiceName = %q", got.RemoteServiceName) + } +} diff --git a/mdl/executor/cmd_contract.go b/mdl/executor/cmd_contract.go index 6628ff7ef..0d6357cd7 100644 --- a/mdl/executor/cmd_contract.go +++ b/mdl/executor/cmd_contract.go @@ -1171,7 +1171,11 @@ func applyExternalEntityFields( ent.Updatable = false ent.SkipSupported = true ent.TopSupported = true - ent.CreateChangeLocally = false + // CreateChangeLocally is deliberately NOT set. Unlike the capability flags + // above it cannot be derived from the service contract — it is a local + // modelling choice ("Allow creating and changing objects locally"), so + // stamping it here reset the user's setting on every re-import (#782). A + // newly-created entity arrives zero-valued, which is Mendix's default. return } @@ -1188,6 +1192,8 @@ func applyExternalEntityFields( ent.Updatable = false ent.SkipSupported = false ent.TopSupported = false + // An entity-type source has no CreateChangeLocally in storage (the writer does + // not emit one), so clear it when an entity is re-imported as a derived type. ent.CreateChangeLocally = false } diff --git a/mdl/executor/cmd_contract_test.go b/mdl/executor/cmd_contract_test.go index 64c4e4abd..696142eb9 100644 --- a/mdl/executor/cmd_contract_test.go +++ b/mdl/executor/cmd_contract_test.go @@ -159,3 +159,38 @@ func TestMendixAttrTypeToEdm(t *testing.T) { } } } + +// TestApplyExternalEntityFields_PreservesCreateChangeLocally is the re-import half +// of mendixlabs/mxcli#782: "Allow creating and changing objects locally" is a local +// modelling choice, not something the OData contract describes, so a re-import must +// not reset it. The capability flags, which the contract *does* describe, are still +// refreshed from the metadata. +func TestApplyExternalEntityFields_PreservesCreateChangeLocally(t *testing.T) { + et := &types.EdmEntityType{Name: "Product"} + es := &types.EdmEntitySet{Name: "Products"} + + // An entity the user had switched the flag on for, being re-imported. + existing := &domainmodel.Entity{CreateChangeLocally: true, Creatable: true} + applyExternalEntityFields(existing, et, true /*isTopLevel*/, "Svc.Sample", es, nil, nil) + if !existing.CreateChangeLocally { + t.Error("CreateChangeLocally reset by re-import, want it preserved (#782)") + } + if existing.Creatable { + t.Error("Creatable not refreshed from the contract (no InsertRestrictions ⇒ false)") + } + + // A newly-imported entity defaults to off, matching Mendix. + fresh := &domainmodel.Entity{} + applyExternalEntityFields(fresh, et, true, "Svc.Sample", es, nil, nil) + if fresh.CreateChangeLocally { + t.Error("a newly imported entity defaulted to CreateChangeLocally = true") + } + + // A derived/entity-type source has no such field in storage; clear it so a + // re-import that reclassifies an entity does not leave a stale value behind. + derived := &domainmodel.Entity{CreateChangeLocally: true} + applyExternalEntityFields(derived, et, false /*isTopLevel*/, "Svc.Sample", nil, nil, nil) + if derived.CreateChangeLocally { + t.Error("entity-type source kept CreateChangeLocally, want it cleared") + } +}