Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e460169
feat(test): --reuse-build skips injection and mxbuild when project, t…
MendixMau Nov 6, 2026
bb92c02
test(reusebuild): unit tests for the hash key, record round-trip and …
MendixMau Nov 9, 2026
4603eda
fix(test): exclude theme-cache from --reuse-build's project hash (clo…
MendixMau Nov 9, 2026
b7d4af2
fix(test --watch): recover builds on a fresh mxbuild --serve (key-for…
MendixMau Nov 6, 2026
4c9287c
test(watch-recovery): cover headless prebuilt-bundle skip and the mac…
MendixMau Nov 9, 2026
5737670
Merge mendixlabs/mxcli#1787: test --watch recovers builds on a fresh …
claude Nov 11, 2026
d23ff9e
Merge mendixlabs/mxcli#1783: test --reuse-build skips injection and m…
claude Nov 11, 2026
81eef3d
chore(test): review follow-ups for the --reuse-build and --watch reco…
claude Nov 11, 2026
65dba32
fix(pages): keep a check box's label position, radio orientation and …
ako Nov 11, 2026
ba47de5
Bump the Go toolchain to 1.27.4 for GO-2026-6859
claude Nov 11, 2026
89cdd9a
Merge pull request #2123 from ako/integrate/mendixmau-1783-1787
ako Nov 11, 2026
c411c4a
Merge remote-tracking branch 'origin/main' into fix/2106-page-round-t…
ako Nov 11, 2026
2f399d0
fix(pages): keep stored formatting and keys a page round trip cannot …
ako Nov 11, 2026
87dd0ab
Merge pull request #2124 from ako/fix/2106-page-round-trip-losses
ako Nov 11, 2026
14062e7
Merge remote-tracking branch 'origin/main' into fix/2106-page-round-t…
ako Nov 11, 2026
f3747ce
test(roundtrip): strike 8 WorkflowCommons snippets that now round-tri…
ako Nov 11, 2026
d623963
Merge pull request #2125 from ako/fix/2106-page-round-trip-carry
ako Nov 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-11-11", "refs": ["ako/mxcli#2106"], "ce": [], "symptom": "describe page -> exec of FactoryManagement pages reported Replaced page and changed three settings: a check box's LabelPosition AfterControl became Default (ConversationalUI.TraceFiltering_Edit), a radio button group's RenderHorizontal false became true (TcConnector.TeamcenterConfiguration_NewEdit), and a show page button's NumberOfPagesToClose2 '1' became '' (SnowflakeRESTSQL.ConnectionDetails_NewEdit_Step2). mx check stays green on both sides; the page behaves differently.", "file": "sdk/pages (CheckBox.LabelPosition, PageClientAction.NumberOfPagesToClose); mdl/executor cmd_pages_builder_v3_widgets.go (LabelPosition, RenderHorizontal), cmd_pages_describe_parse.go + cmd_pages_describe_output.go (read and print both), cmd_pages_describe_action_settings.go (NumberOfPagesToClose in with (…)), page_action_settings.go, widget_native_props.go (MDL-WIDGET52 reads), validate_widgets.go (known props); mdl/visitor/visitor_action_settings.go (showPage kind, numeric value); grammar actionSettingV3 takes NUMBER_LITERAL; mdl/backend/modelsdk/widget_write.go (SetLabelPosition, SetNumberOfPagesToClose2). Tests: cmd_pages_round_trip_settings_2106_test.go, visitor_show_page_close_2106_test.go, widget_write_round_trip_settings_2106_test.go", "cause": "None of the three had an MDL spelling. The writer wrote the default each time: NumberOfPagesToClose2 hard-coded to \"\", LabelPosition never set, and RenderHorizontal derived from a RenderDirection nothing ever filled, so every rewritten radio button group became horizontal.", "insight": "**A value-only BSON diff of the first exec, swept over a whole app, separates real losses from unexpressible formatting.** Of FactoryManagement's 14 'Replaced' pages, these three change behaviour; the other eleven differ in values MDL cannot state (TabIndex, trailing newlines in DynamicClasses, XPath whitespace, condition order, an inherited attribute's declaring entity, a null SourceVariable key, an unused placeholder parameter), which a canon carry handles separately. A model field the writer reads but no builder sets (RadioButtons.RenderDirection) is a silent default: grep each writer input for a builder that assigns it. Controls: reverting each of the ten edits fails its test; a new page using all three builds with mx check 0 errors on the Agent Builder Starter App; TraceFiltering_Edit and ConnectionDetails_NewEdit_Step2 now report Unchanged page."}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "modelsdk/canon", "date": "2026-11-11", "refs": ["ako/mxcli#2106", "ako/mxcli#2052"], "ce": [], "symptom": "describe page -> exec of 8 FactoryManagement pages reported Replaced page with nothing changed: a DynamicClasses expression lost its trailing newlines (3 pages), an XPath constraint was reformatted (2), an enumeration condition's values were reordered (1), an AssociationSource lost its null SourceVariable key (2), and an inherited createdDate binding was requalified from GenAICommons.Span to GenAICommons.ToolSpan (1).", "file": "modelsdk/canon/unexpressed.go (whitespaceInsensitive for Expression, DynamicClasses, XPathConstraint; sameConditionsReordered; anyIDUsedOutside), mdl/backend/modelsdk/widget_write.go (Forms$AssociationSource NullFields SourceVariable), mdl/executor/cmd_pages_builder_input.go entityAttributeOwners (system members count as the storing entity's own). Tests: modelsdk/canon/unexpressed_2106_test.go, widget_association_source_null_2106_test.go, cmd_pages_builder_system_member_2106_test.go", "cause": "#2052's carry compared only Expression and only by TrimSpace; DynamicClasses and XPathConstraint are expressions too, and Studio Pro keeps the author's interior line breaks. The writer emits enumeration conditions in enumeration order where Studio Pro keeps insertion order. Forms$AssociationSource had no NullFields registration (Studio Pro stores the key on 40 of 40). declaringEntityFor indexed declared attributes only, so a system member a specialization inherits resolved to the context entity.", "insight": "**Census the stored form across a whole app before choosing a carry or a writer fix.** 40 of 40 association sources carry SourceVariable, so it is a property-set gap fixed once in the writer; the condition order differs only by authoring history, so it is a carry. And check a 'formatting' difference before carrying it: the placeholder 'unused parameter' this sweep also showed is the template's only, USED, expression parameter that MDL cannot state - a real loss for new syntax, which the carry rule rightly would not have touched. Controls: each test fails without its change; a full sweep of FactoryManagement's 229 pages turns exactly these 8 Unchanged and regresses none."}
10 changes: 5 additions & 5 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:

- uses: actions/setup-go@v7
with:
go-version: '1.27.3'
go-version: '1.27.4'

- name: Cache ANTLR4 JAR
uses: actions/cache@v6
Expand Down Expand Up @@ -90,7 +90,7 @@ jobs:

- uses: actions/setup-go@v7
with:
go-version: '1.27.3'
go-version: '1.27.4'

# mxbuild 9 draws through System.Drawing; without libgdiplus every mx call
# dies with "The type initializer for 'Gdip' threw an exception."
Expand Down Expand Up @@ -141,7 +141,7 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.3'
go-version: '1.27.4'
- name: Test the tunnel seam
run: bash scripts/test-tunnel-seam.sh

Expand All @@ -162,7 +162,7 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.3'
go-version: '1.27.4'
- name: Cache ANTLR4 JAR
uses: actions/cache@v6
with:
Expand Down Expand Up @@ -193,7 +193,7 @@ jobs:

- uses: actions/setup-go@v7
with:
go-version: '1.27.3'
go-version: '1.27.4'

- uses: oven-sh/setup-bun@v2

Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/push-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
with:
# The toolchain matters: gofmt's output differs between Go releases
# (the mismatch fixed in #1759), so this must match the other jobs.
go-version: '1.27.3'
go-version: '1.27.4'
cache: false
- name: Check gofmt
run: make fmt-check
Expand Down Expand Up @@ -100,7 +100,7 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.3'
go-version: '1.27.4'
- name: Test the tunnel seam
shell: bash
run: bash scripts/test-tunnel-seam.sh
Expand All @@ -120,7 +120,7 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.3'
go-version: '1.27.4'
- name: Test the Windows process helpers
shell: bash
# Scoped with -run: the rest of the docker package's tests assert POSIX
Expand Down Expand Up @@ -165,7 +165,7 @@ jobs:
submodules: true
- uses: actions/setup-go@v7
with:
go-version: '1.27.3'
go-version: '1.27.4'
- name: Cache ANTLR4 JAR
uses: actions/cache@v6
with:
Expand Down Expand Up @@ -289,7 +289,7 @@ jobs:
submodules: ${{ matrix.suite == 'roundtrip' || matrix.suite == 'parity' }}
- uses: actions/setup-go@v7
with:
go-version: '1.27.3'
go-version: '1.27.4'
- name: Cache ANTLR4 JAR
uses: actions/cache@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version: '1.27.3'
go-version: '1.27.4'
- uses: oven-sh/setup-bun@v2
- name: Cache ANTLR4 JAR
uses: actions/cache@v6
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- **`mxcli test --local --reuse-build` skips the injection and the build when nothing changed** (mendixlabs/mxcli#1783, closes mendixlabs/mxcli#1782) — a local test run injects its test microflows, builds and boots; when the project, the test files and mxcli are unchanged, the deployment the previous run left is already this run's build, so `--reuse-build` boots it directly (about 12 s against 60 s, measured by the author). The decision is a content hash of the project folder (outputs, caches, `theme-cache/`, and any `.git`/`node_modules` left out), the generated test MDL, the mxcli binary and the mxbuild override, pinned to the identity of `deployment/model/model.mdp`, so a deployment rebuilt by anything else is never taken for the test build. Not under `--watch` or `--skip-build`.
- **A `genai-runtime` skill for the runtime side of Agents Kit 2** (ako/mxcli#2068) — registering a Mendix Cloud GenAI key or an OpenAI-compatible endpoint, seeding AgentCommons agents from JSON (matched by `Title` only, so a changed JSON never reaches a stored agent), resuming Conversational UI chats, loading PgVector and Mendix Cloud knowledge bases, and traces. It collects the gotchas that `check` and `mx check` cannot see: the encryption key both the OpenAI and PgVector setup need, the `vector` extension only the UI's Test button creates, the 60–120 s indexing delay, the chat that collapses outside its page, and `StoreTraces` defaulting to false. Its examples pass `check --references` and build with 0 errors against the Agent Builder Starter App's modules.
- **`mxcli test --attach` runs a test without building it, against an app started with `--allow-call`** (mendixlabs/mxcli#1788, mendixlabs/mxcli#1789) — `run --local --test-endpoint --allow-call` switches on an executor in the test endpoint. A `.test.mdl` test whose statements it supports (`declare`, `set`, `create`, `change`, `commit`, `delete`, `call microflow`, database `retrieve` with `where`/`first`; `@expect` with `count`, `@throws`, `@setup`, `@cleanup`) is compiled by mxcli into steps and run in the app with the Mendix runtime API, every expression evaluated by `Core.evaluateExpression`. Nothing is injected or built, so a test edit costs one HTTP round trip: on a small app the suite took 0.26 s against 3.2 s for inject-and-build, with identical verdicts. Retrieves over an association and the list aggregates `count`/`sum`/`average`/`minimum`/`maximum` over an attribute run there too. A test with any other statement is injected and built as before. Only that test is, while the rest of the run still goes straight to the app, and the output names the test and the MDL statement (`the statement if`, `a list operation (find)`). Because the runtime evaluator returns empty for an unknown name, an undeclared `$variable` and a non-existent enumeration value are refused (ERROR) before evaluating, and a model newer than the running build is refused with `mxcli run wait` as the fix under `--watch`. The route takes POST, needs the run's token and a loopback caller, and answers 403 without `--allow-call`, since it calls the microflows a test names in a system context. `.test.mdl` stays the only test language. Design discussed with the authors of #1789; measurements in `mdl-examples/spikes/evaluate-expression.mdl`.
- **`flows_for()` and `CATALOG.FLOWS`: a flow's sequence flows in lint rules, and each activity's place on the canvas** (ako/mxcli#1299, mendixlabs/mxcli#1308) — `activities_for()` listed the objects but not which leads to which, so a rule could not tell which log belongs to which custom error handler, which activity an annotation explains, or which branch of a split is `true`. Each sequence and annotation flow is now a row with its origin and destination activity, `is_error_handler`, the branch's `case_value` and the sides it connects; activities gain `x`, `y`, `width` and `height`. Full catalog; the schema moves to version 44, so a cached catalog is rebuilt once.
Expand All @@ -26,6 +27,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **`describe page` → `exec` no longer rewrites a page over formatting and stored keys MDL cannot state** (ako/mxcli#2106) — on FactoryManagement, 8 pages reported `Replaced page` with nothing changed: a dynamic-classes expression's trailing newlines, an XPath constraint's line breaks and spacing, the order of an enumeration condition's values, an association source's null `SourceVariable` key, and an inherited `createdDate` binding requalified to the specialization. The stored value is now kept where only its form differs, the writer stores the key Studio Pro stores, and a system member resolves to the entity that stores it. A full sweep of the app's 229 pages turns those 8 `Unchanged` and changes no other.
- **`describe page` → `exec` keeps a check box's label position, a radio button group's orientation and a show page action's close count** (ako/mxcli#2106) — none had an MDL spelling, so a round trip of a Studio Pro page reset a check box with its label after the control to the default, turned every vertical radio button group horizontal, and stopped a show page button from closing the page it came from, with `mx check` green. `checkbox` takes `LabelPosition: Default|BeforeControl|AfterControl`, `radiobuttons` takes `RenderHorizontal: false`, and `show page` takes `with (NumberOfPagesToClose: n)`; `describe` prints each when it differs from the default.
- **`mxcli test --local --watch` keeps applying changes on macOS and Linux mxbuild** (mendixlabs/mxcli#1787, refs mendixlabs/mxcli#1786) — the test runner's rebuild called mxbuild's serve process directly, without the recovery `run --local --watch` has, so the second build of a serve process failed on regenerated pages and every later change reported "could not be applied". The rebuild now goes through the same recovery, which also recognises mxbuild's resource-key form of that failure (`IO_PathNotFound_Path`, as macOS and Linux report it) and skips the web-bundle check, which only matters when a browser is served.
- **`exec` and `check` no longer rescan an MPR v1 project for every lookup** (ako/mxcli#2094, mendixlabs/mxcli#1795) — each statement cost 0.3–0.9 s on a large single-file project, whatever it changed: every typed listing re-read the metadata of the whole `Unit` table, and every lookup by name read every microflow's contents. The reader now keeps the scan, and each unit's name, until a write or another process's commit says the table may have changed. On a 3,400-unit project a `create microflow` went from 0.30 s to 0.11 s, a one-activity `create or modify` from 0.86 s to 0.35 s, and `check` of 150 such statements from 76 s to 15 s. MPR v2 projects already kept this index.
- **`mxcli test --local` beside a `run --local --test-endpoint` dev loop runs, and leaves the project buildable** (ako/mxcli#2119) — the test run read the dev loop's `MxTest.RegisterEndpoint` as the project's after-startup microflow and chained its own endpoint to it, to itself: the scratch runtime failed to boot with a `StackOverflowError` and no test ran. Its cleanup then dropped the dev loop's endpoint while the setting still named it, so the dev loop's next build failed with CE1613. A live host's restore record now says what the endpoint replaced, and the test run chains to that; its cleanup removes only its test microflows and leaves the dev loop's endpoint, Java source and setting as it found them. An endpoint whose run died is removed first, as `run` does, and one with no record of what it replaced is refused rather than chained to. When the test run rebuilt the `deployment/` folder both share, the dev loop builds its own back once the run ends (a reload or a restart), so a following `--attach` is not refused for a restart.
- **A test run no longer rebuilds or restarts the dev loop it shares a project with** (ako/mxcli#2063, field test on ako/mxcli#2104) — `mxcli test --local`, and `--attach` with a test that has to be built, write their test microflows into the project a `run --local --watch` dev loop is watching, so the dev loop rebuilt for the injection and its cleanup, and applied it by restarting the runtime, which drops every browser session, `--hub` preview included; on a small app the restart itself failed (`EOFException`). The test run now holds the dev loop's watcher (`.mxcli/watch-hold.json`) while it owns the project, `--attach` now puts the project file back byte for byte as `--local` already did, and on release the watcher compares the model with the one the hold began from: the same model is not built at all, and `run status` / `run wait` read it as covered. An edit made before or during the run is still built once it ends, and a hold whose run died is ignored.
Expand Down
2 changes: 2 additions & 0 deletions cmd/mxcli/cmd_test_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Examples:
junitOutput, _ := cmd.Flags().GetString("junit")
requireAssertions, _ := cmd.Flags().GetBool("require-assertions")
skipBuild, _ := cmd.Flags().GetBool("skip-build")
reuseBuild, _ := cmd.Flags().GetBool("reuse-build")
local, _ := cmd.Flags().GetBool("local")
legacyRunner, _ := cmd.Flags().GetBool("legacy-runner")
watch, _ := cmd.Flags().GetBool("watch")
Expand Down Expand Up @@ -214,6 +215,7 @@ Examples:
ProjectPath: projectPath,
TestFiles: resolveTestPaths(args, projectPath),
SkipBuild: skipBuild,
ReuseBuild: reuseBuild,
Local: local,
LegacyRunner: legacyRunner,
Watch: watch,
Expand Down
13 changes: 12 additions & 1 deletion cmd/mxcli/docker/localapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ type LocalApp struct {
RuntimeLogPath string

serve *ServeServer
// rec runs Rebuild's builds with the recovery `run --local --watch` has: a
// build whose outcome only a fresh mxbuild --serve fixes is repeated on one.
rec *serveRecovery
}

func (o *LocalAppOptions) applyDefaults() {
Expand Down Expand Up @@ -323,6 +326,7 @@ func StartLocalApp(opts LocalAppOptions) (*LocalApp, error) {
return nil, fmt.Errorf("starting mxbuild serve: %w", err)
}
app.serve = serve
app.rec = &serveRecovery{serve: serve, deployDir: opts.DeployDir, out: w, headless: true}

build, err := buildRetryingServeTimeout(serve, BuildRequest{Target: TargetDeploy, ProjectFilePath: opts.ProjectPath}, w)
if err != nil {
Expand Down Expand Up @@ -365,7 +369,14 @@ func (a *LocalApp) Rebuild(projectPath string) (ApplyAction, *BuildResult, error
if a.Runtime == nil {
return ActionReload, nil, fmt.Errorf("the runtime is not running")
}
build, err := a.serve.Build(BuildRequest{Target: TargetDeploy, ProjectFilePath: projectPath})
req := BuildRequest{Target: TargetDeploy, ProjectFilePath: projectPath}
var build *BuildResult
var err error
if a.rec != nil {
build, err = a.rec.Build(req)
} else {
build, err = a.serve.Build(req)
}
if err != nil {
return ActionReload, nil, err
}
Expand Down
6 changes: 5 additions & 1 deletion cmd/mxcli/docker/serve_recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ type serveRecovery struct {
// failed. A theme failure then is not trustworthy: the process may be
// reporting the earlier error rather than the source on disk.
themeFailed bool

// headless: nothing serves the client (the test runner), so a build that
// succeeds with an incomplete web/dist is a good build and is not repeated.
headless bool
}

func (r *serveRecovery) Build(req BuildRequest) (*BuildResult, error) {
Expand Down Expand Up @@ -105,7 +109,7 @@ func (r *serveRecovery) Build(req BuildRequest) (*BuildResult, error) {
}

if res.OK() {
if !prebuilt {
if !prebuilt || r.headless {
return res, nil
}
after, _ := readPrebuiltBundle(r.deployDir)
Expand Down
Loading
Loading