Sync ako/mxcli: page/OData/test-runner/microflow fixes (#795, #782, #802-#804, #791, #790) - #810
Merged
Conversation
…flowSettings (mendixlabs#795) DESCRIBE PAGE emitted `datagrid g1 {` with no DataSource for a DataGrid2 bound to a microflow, so re-applying the describe output recreated the grid with no data source at all. A `database from` source described correctly, which made the loss look selective. A Forms$MicroflowSource stores the microflow name in the nested Forms$MicroflowSettings ("MicroflowSettings" -> "Microflow") — what the write path emits and what Studio Pro stores. extractDataGrid2DataSource looked up a top-level "Microflow" key, got "", and returned no datasource. The describe formatter's `case "microflow"` branch was correct all along, so nothing was ever wrong with the stored model or the write path: purely a read bug. Four readers had their own copy of this lookup and disagreed — two read the nested settings, one read the top-level key, one did both. They now share microflowSourceRef / nanoflowSourceRef, which read the nested settings and fall back to the legacy top-level key so older files still round-trip. Two gaps the regression tests found while covering this: - a Gallery whose source used the legacy flat shape was unreadable too: the correct reader it delegates to handles only the nested form, and the flat fallback in its own switch is not reached for a CustomWidget gallery - Forms$NanoflowSource was missing entirely from the DataGrid2 and Gallery switches, so a Studio-Pro-authored nanoflow datasource described as nothing Left alone deliberately: CustomWidgets$CustomWidgetNanoflowSource, a different metamodel type whose Nanoflow key really is top-level, and the Forms$MicroflowAction reads, which are actions rather than datasources. Also corrects the datasource table in the create-page skill: it documented `datasource: microflow Module.GetData()`, and the trailing `()` does not parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
A construct the codec engine cannot represent — a nanoflow datasource on a DataGrid2 is the reachable case — was logged and discarded while `mxcli exec` still printed `Created page`. The grid landed with no data source at all. The converter does return an error; it just had nowhere to go. Child content is serialized through widgetobj.ChildSerializer, whose methods return BSON with no error channel — the interface change flagged by the TODO in mdl/backend/widgetobj/builder.go. Until that lands, the failure is recorded in the modelsdk backend and drained by the page/snippet write entry points, so the statement fails with the actionable message instead of the write succeeding with data missing. ADR-0004 already required this: where the codec path cannot reproduce a construct the backend refuses the op rather than dropping it. Authoring a pluggable nanoflow datasource still needs MXCLI_ENGINE=legacy, which writes it correctly — verified, since the error message says so. Also adds the missing `nanoflow` case to the DataGrid2 and Gallery describe formatters. Reading such a source works after mendixlabs#795, but the formatter had only database/microflow/parameter branches, so a legacy-written or Studio-Pro-authored nanoflow grid still described with no DataSource. Any new write entry point that builds pluggable widgets must drain too; the symptom table records that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…urns
A data container bound to a microflow or nanoflow reported the *flow* as its
context entity:
datagrid g1 (DataSource: microflow Module.GetOrders) {
-- Context: $currentObject (Module.GetOrders) <- the flow, not the entity
The datasource reference was used verbatim. That is correct for a database
source, where the reference is the entity, and wrong for a flow source, where
it is the flow's own qualified name. Besides the misleading comment, the
context is passed down to the column and child-widget readers, which resolve
attributes against it.
The flow's return type is resolved instead, taking the entity from an Object
or List return. When the flow cannot be resolved, or returns a scalar, the
reference is kept — the previous behaviour, so nothing gets worse.
Resolution goes through ListMicroflows/ListNanoflows plus the container
hierarchy rather than GetRawUnitByName, which the flow builder's fast path
uses: that method is unimplemented on the modelsdk engine, which is the
default, so the list path is the one that actually resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
) `alter entity X set allow_create_change_locally = true` reported success and left the flag off. So did every other remote setting, and `describe external entity` rejected the entity outright with "is not an external entity (source: )". The write path was never the problem: externalEntitySourceToGen switches on e.Source and handles all three Rest$OData* flavours. The *read* filled none of them — entityFromGen recognised only DomainModels$OqlViewEntitySource, so an external entity came back with an empty Source and every remote field zeroed. Setting a flag then wrote it onto a model that no longer knew the entity was external, and the update rebuilt the entity without its source at all. The legacy engine parses all three flavours (sdk/mpr/parser_domainmodel.go), so this was a modelsdk-engine gap — and modelsdk is the default. Under `--engine legacy` the same commands worked, which is a useful bisect signal. Now mirrored for: - Rest$ODataRemoteEntitySource — service, entity set, remote name, the capability flags, CreateChangeLocally and the remote key - Rest$ODataEntityTypeSource — service, type name, IsOpen and the remote key - Rest$ODataPrimitiveCollectionEntitySource — service Updatable is deliberately left zero: the storage type has no such field (updatability is per attribute via Rest$ODataMappedValue), there is no gen accessor, and the writer does not emit one, so read and write stay symmetric. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…y re-import `create or modify external entities` reset "Allow creating and changing objects locally" to false on every run, so the setting survived only until the next import — the other half of mendixlabs#782. applyExternalEntityFields stamps every field on both the create and the update path. That is right for the capability flags (Countable / Creatable / Deletable / SkipSupported / TopSupported), which are derived from the service's capability annotations and should be refreshed from the metadata. It is wrong for CreateChangeLocally, which no OData contract describes: it is a local modelling choice, so the top-level branch now leaves it alone. A newly imported entity arrives zero-valued, which is Mendix's default. The entity-type branch still clears it, since that storage type has no such field — a re-import that reclassifies an entity as a derived type would otherwise leave a stale value in the model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…endixlabs#791) A loop containing a split whose branch does `continue` wrote successfully and could not be opened in Studio Pro: System.Collections.Generic.KeyNotFoundException: The given key '806fca46-5c4b-46f8-a890-4d24dd29c24f' was not present in the dictionary. microflowObjectToGen had no case for Microflows$BreakEvent or Microflows$ContinueEvent, and its default branch returns nil. The event object was therefore dropped at serialization while the SequenceFlow pointing at it was written, leaving a DestinationPointer to a GUID that exists nowhere in the document — the key Studio Pro cannot resolve. Confirmed by dumping the written microflow and checking every *Pointer against the object IDs: one dangling pointer, and no ContinueEvent anywhere. The legacy engine serializes both, so this only affected the default engine. The identical bug had already been found and fixed for Microflows$ErrorEvent, whose case still carries the comment explaining it. Break and continue were missed. MDL051 goes with it. That rule rejected a conditional `break` at check time and pointed users at a guard-variable workaround, explicitly "until the serialization is fixed" — it now is, and keeping the rule would reject valid MDL. Note it only ever covered `break`: `continue` had no guard, which is how this reached a user as a corrupt project. Its negative fixture becomes a positive one, and its test now asserts the pattern is accepted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…endixlabs#803/mendixlabs#804) Three defects in one teardown, which together left a tested project permanently mutated. quotes before trailing punctuation, and DESCRIBE SETTINGS separates properties with commas, so AfterStartupMicroflow = 'MyFirstModule.ASU_Startup', parsed to `MyFirstModule.ASU_Startup',` and the restore statement became ALTER SETTINGS MODEL AfterStartupMicroflow = 'MyFirstModule.ASU_Startup',' which does not parse. The failure printed as a warning, so the run looked clean while the project was left pointing at MxTest.TestRunner — the microflow cleanup then deleted. Trailing `,;` now comes off before the quotes, the value is re-emitted through a helper that doubles embedded quotes (Mendix's escape, never backslashes), and cleanup returns its errors instead of printing them: a failed restore now fails the run and names what was left changed. so an empty module accumulated on every run. It now drops the module — but only when the run created it. CREATE MODULE is idempotent, so a project that already had its own MxTest module would otherwise have had it deleted; that case still removes just the generated microflow. Cleanup also re-checks that the module is present, so a run that failed before injection landed does not report a spurious cleanup failure. hardcoded PRODUCTION, silently changing projects that legitimately run at another level, and breaking any project whose published REST/OData services use custom authentication ("App security is off, but custom authentication is enabled for this service"). The after-startup microflow runs in an administrative context and is not subject to the Security Level, so this bought nothing. It is gone; the setting is the project's business. What cleanup must restore is now captured in a projectState before the first mutation, and the setup/cleanup statements are built by pure functions, so the restore is testable without a project or Docker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…ies (mendixlabs#790) A loop box was drawn far wider than its contents — 880px around three activities spanning 440px, leaving ~290px of empty area inside the frame. measureStatements sums each element's full width and adds HorizontalSpacing between them. But HorizontalSpacing is a centre-to-centre pitch: the builder centres each activity on posX and advances by exactly that, so counting it on top of each width over-measures a run of n simple activities by (n-1)*ActivityWidth — 240px for three. The loop width is derived from that measure, so the box inherited the error. Loop sizing now uses measureStatementsSpan, which returns the true extent (n-1)*HorizontalSpacing + ActivityWidth for a run of simple activities. The example above goes from 880 to 640 (the body occupies 90..530 inside it), and nested loops shrink at both levels. A compound element (IF/split, nested loop) advances posX by merge geometry this function cannot reproduce without duplicating the builder. An earlier attempt to approximate it under-sized the box and pushed the trailing activity 30px outside the frame — worse than a box that is merely too wide — so those runs fall back to the conservative measure and are unchanged. Verified by checking that every child of a LoopedActivity lies within its box, across loop-with-if, nested-loop, single-statement and multi-statement bodies. Not addressed: the other half of mendixlabs#790, "position annotations sometimes change". Explicit @position annotations round-trip exactly, and three successive describe/exec cycles produce byte-identical output, so there is nothing reproducible to fix here — the reporter also noted it was inconsistent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
fix(pages): read a pluggable widget's flow datasource, and stop dropping children silently
…-entity-782 # Conflicts: # .claude/skills/fix-issue.md
…eanup-802-804 # Conflicts: # .claude/skills/fix-issue.md
…-791-790 # Conflicts: # .claude/skills/fix-issue.md
…eanup-802-804 # Conflicts: # .claude/skills/fix-issue.md
…-791-790 # Conflicts: # .claude/skills/fix-issue.md
This was referenced Jul 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Syncs
ako/mxcliinto upstream: eight commits fixing seven reported issues, all reproduced first and verified against a real project.Closes
Pages / describe (
#795+ two follow-ups)A
Forms$MicroflowSourcestores the microflow name in the nestedForms$MicroflowSettings; the DataGrid2 reader looked up a top-levelMicroflowkey, got"", and returned no datasource. The describe formatter was correct all along — purely a read bug. Four readers each had their own copy of that lookup and disagreed; they now share one helper. Regression tests caught two more gaps: a Gallery using the legacy flat shape, andForms$NanoflowSourcemissing entirely from the DataGrid2/Gallery switches.A construct the codec engine cannot represent was logged and dropped while
execstill printedCreated page. The converter did return an error;widgetobj.ChildSerializerhas no error channel (the existingTODO(shared-types)). Failures are now recorded and drained at the page/snippet write entry points, so the statement fails instead — ADR-0004's refuse-don't-drop.A data container bound to a flow reported the flow as its context entity rather than the entity it returns. Not just cosmetic:
EntityContextfeeds the column and child-widget readers.OData external entities (
#782)Two independent bugs, and the first is much bigger than the flag.
entityFromGenrecognised onlyDomainModels$OqlViewEntitySource, so any OData external entity read back with an emptySourceand every remote field zeroed —describe external entityrejected it outright, and a read-modify-write rebuilt the entity without its source at all. The write path was always fine; it switches one.Source, which the read never populated. Works under--engine legacy, whose parser handles all three flavours — a modelsdk-engine gap, and modelsdk is the default.Separately,
create or modify external entitiesresetCreateChangeLocallyon every run. That is right for the capability flags, which the service's annotations describe, and wrong for a local modelling choice no OData contract mentions.Test runner (
#802/#803/#804)Three reports, one teardown routine. The after-startup value was mis-parsed (
DESCRIBE SETTINGSseparates properties with commas; quotes were trimmed before punctuation), so the restore statement was unparseable, printed as a warning, and the project was left pointing at the microflow cleanup then deleted. Cleanup now returns its errors and fails the run. The generatedMxTestmodule is dropped — but only when the run created it, sinceCREATE MODULEis idempotent and a pre-existing module is the user's. And the Security Level is left alone entirely: the after-startup microflow runs in an administrative context and is not subject to it, so forcing it OFF bought nothing while breaking custom-auth REST/OData services.Microflow loops (
#791,#790)microflowObjectToGenhad no case forMicroflows$BreakEvent/ContinueEventand its default returns nil, so the event was dropped while theSequenceFlowpointing at it was written — aDestinationPointerto a GUID that exists nowhere, exactly theKeyNotFoundExceptionStudio Pro reports. Verified by dumping the microflow and checking every pointer resolves: 1 dangling before, 0 after. The same bug had already been fixed once forErrorEvent; break and continue were missed. MDL051 — a check-time stopgap explicitly added "until the serialization is fixed" — is removed with it; note it only ever coveredbreak, which is why thecontinueform reached a user as a corrupt project.Loop boxes were sized from a measure that adds
HorizontalSpacingon top of each element's full width, but that constant is a centre-to-centre pitch — over-counting a run of n activities by(n-1)*ActivityWidth. An example box goes 880 → 640 around 440px of content. Compound elements (IF/split, nested loop) keep the conservative measure: approximating their advance under-sized the box and pushed activities outside the frame, which is worse.Already fixed, no change needed
#780 (call microflow in a loop without an error handler) is fixed on current main:
ehTypereturnsRollbackfor microflows unless overridden, and the BSON is structurally identical with and without the clause, with zero dangling pointers. Worth closing with that note.Not addressed
@positionround-trips exactly and three describe/exec cycles are byte-identical. The reporter noted it was inconsistent.--engine legacy.modelsdk/gen/pageshas noNanoflowSettingstype and there is no Studio Pro sample to verify a hand-written shape against. It now fails loudly instead of silently dropping the binding.0or999999still parses as an integer).Testing
make build,make test,make lintandmake check-mdlpass. Every fix has a regression test verified failing against the pre-fix code, plus a repro fixture undermdl-examples/bug-tests/. Symptom-table rows added for each.Two of these issues (
#801,#802) had reporters who said they had their own fixes ready — worth checking before merging so the work isn't duplicated