Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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":"cmd/mxcli","date":"2026-11-04","symptom":"`mxcli check --references` printed `✗ 1 reference error(s) found` and then `0 issues: 0 errors, 0 warnings, 0 info` — the reporter asked why a reference error does not count as an error (mendixlabs/mxcli#1726)","cause":"the closing summary sums the `printed` list, which only the `report` helper appends to; the reference tier prints its errors and warnings itself, so they never reached the total","file":"cmd/mxcli/cmd_check.go (reference tier appends to printed); test cmd/mxcli/check_reference_summary_test.go","insight":"When one summary is computed over a list that several tiers feed, a tier with its own print path drops out of the total silently. Only the run in the report had an earlier tier's violation, so the summary line printed at all; a script with nothing but a reference error printed no summary, which is why the test asserts `1 errors` rather than the absence of `0 errors`. Control: without the append, the test fails with no summary in the output.","refs":["ako/mxcli#1928","mendixlabs/mxcli#1726"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"cmd/mxcli","date":"2026-11-04","symptom":"The nightly goes red on the Mendix 11.14.0 leg only, in `TestTestAttach_KeepsTheHostingAppsPageBundles` (added the day before by ako/mxcli#1917): `mxcli test --attach` exits 1 with `this change needs a runtime restart (an entity or association changed), which --attach cannot do`, although the attach only adds a test microflow. 11.12.x, 11.13, 11.15 and 11.16 are green","cause":"On 11.14.0 every later build in an mxbuild --serve process fails, even with the model unchanged (`Cannot find module …/web/rollup.config.mjs`), and the failed /build response carries no restartRequired key at all. #1917 routes the attach's build through serveRecovery, whose legacy-hint path restarts onto a private serve and builds cold; a cold build reports restartRequired=true whatever changed, and runner_attach read that as a structural change. Before #1917 the same attach failed with the build failure itself, so attach never worked on 11.14.0; the test exposed it because its default 11.12.6 falls back to whatever mxbuild the nightly leg caches","file":"`cmd/mxcli/docker/serve_recovery.go` (rebuildFresh marks the cold answer `BuildResult.ColdRestart`; the incomplete-bundle path clears it when it takes the incremental answer), `cmd/mxcli/testrunner/runner_attach.go` (`attachRestartRefusal`), `cmd/mxcli/cmd_test_attach_bundle_integration_test.go` (11.14 branch)","insight":"A recovery that substitutes a fresh process's answer must say so: a cold build's restartRequired is \"unknown\", and only a caller that can restart may treat it as true. Settled by driving /build directly (a throwaway test with StartServe + Build, no watch loop): 11.14.0 build 2 with no change = Failure with no restartRequired; 11.15.0 = Success, false for no change and for a microflow, true for an entity, 0 pages (the 11.12.5 shape #1917 already recovers). That table is what puts the limit on 11.14.0 alone, so the test pins `>= 11.14 && < 11.15` and fails when an 11.14 mxbuild builds incrementally again. Reproduce in about 4 minutes: `mxcli setup mxbuild --version 11.14.0`, JAVA_HOME at a JDK 25, `MXCLI_RUNLIFE_VERSION=11.14.0 go test -tags integration -run TestTestAttach_KeepsTheHostingAppsPageBundles ./cmd/mxcli/`. Controls: the old test fails with the nightly's exact message; the new test passes on 11.14.0 and 11.15.0 and fails on 11.14.0 against a binary without the fix (it names the entity); stubbing the ColdRestart mark fails the two unit tests that pin it","refs":["mendixlabs/mxcli nightly run 41801646309","ako/mxcli nightly run 41801123003","ako/mxcli#1917","mendixlabs/mxcli#1723"],"test":"cmd/mxcli/cmd_test_attach_bundle_integration_test.go; cmd/mxcli/docker/serve_attached_test.go; cmd/mxcli/docker/serve_recovery_test.go; cmd/mxcli/testrunner/runner_attach_restart_test.go"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-11-04","symptom":"`check --references` passed drop/rename/move of a document that does not exist, and alter enumeration / alter microflow on one, while exec refused each with `<kind> not found` — 9 of the 11 statement forms that name an existing document (only alter entity and alter page were resolved)","cause":"validateAlterTarget was written for ALTER and extended one statement at a time; the drop/rename/move statements resolve against the same listings but were never routed to it","file":"mdl/executor/validate_alter_target.go (validateAlterTarget, targetListing, checkTarget, moveTargetKind), mdl/executor/validate.go (collectSingle: indirectDocs, renamedModules, opaqueModules, opaqueScript); test cmd/mxcli/check_statement_target_test.go","insight":"The risk in widening a not-found check is the false refusal, not the miss: a document can reach a name without a create statement naming it — an earlier rename or move, a rename module, an execute script, a generated SQL connector, a business event service, a bulk external-entities import. Record each in the pre-pass and stay silent for that name or module; the check must stay quieter than exec. Matching is exact-case because exec's lookups are. A PedApp test per form, each with an existing-target control, plus if-exists / created / renamed / moved controls. Control: without the new cases all 13 forms pass check.","refs":["ako/mxcli#1821","ako/mxcli#555"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-11-04","symptom":"`textbox (Attribute: PlannedOn)` in a data view, or `ContentParams: ({1} = PlannedOn)` / `CaptionParams`, on an entity without that attribute: check --references passed, exec wrote it, mxbuild CE1613 \"The selected attribute 'M.E.PlannedOn' no longer exists\" — measured 3 of 3 on a fresh 11.12.6 app, control page clean","cause":"the input-widget scope walk (checkInputBinding) looked the attribute up and returned silently when it was not found unless the name was an association; template parameters were never looked up","file":"mdl/executor/validate_widget_attribute_type.go (reportMissingAttribute, checkTemplateParams), mdl/executor/validate_widget_attribute_scope.go (walk); test cmd/mxcli/check_widget_attribute_exists_test.go","insight":"The context walk, the member index with inheritance and the association hops were already there for the type rule (MDL-WIDGET39) — the not-found case was a silent `return` in it. Before writing a new resolver, grep for the lookup that already returns found/known and see what it does on a miss. Stay quiet where the index is not sure: an unknown chain (known=false), an unresolved context, an altered entity, an expression or $var path, and the audit members (createdDate/changedDate/owner/changedBy), which bind by name but are not in the attribute list. Control: without the two calls the textbox and content-parameter subtests pass check.","refs":["ako/mxcli#555"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-11-04","symptom":"`alter page M.P { add parameters $X: … }` then a show page passing X, in one script: check --references and exec refused it with `page M.P has no parameter \"X\"`; the same statements as two scripts ran (mendixlabs/mxcli#1736)","cause":"the script pre-pass recorded page parameter names only from create page, so for an altered page the show page argument check fell through to the stored parameter list","file":"mdl/executor/validate.go (collectDefinitions AlterPageStmt case, pageParamEdits), mdl/executor/validate_show_page_params.go (recordPageParamEdits, applyPageParamEdits); test validate_show_page_params_alter_test.go","insight":"A script-context index that only listens to create statements reads every alter as a no-op — the same class as the #1355 script-set fix, one level down. Record the alter's edits rather than a computed list, because the pre-pass has no project to read the stored list from; apply them over whichever list wins at lookup time. End-to-end on PedApp 11.13.0: the base build refuses with the reported message, the fix passes check and exec writes both. Control: without the pre-pass case, the add/drop/create-then-alter subtests fail.","refs":["ako/mxcli#1940","mendixlabs/mxcli#1736"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-11-04","symptom":"with a live and an excluded microflow of one name, `check --references` refused `call microflow M.Test(param1 = …)` with `has no parameter \"param1\" (declared parameters: param2)` — the excluded twin's parameters (mendixlabs/mxcli#1726)","cause":"the validators index stored documents by name in maps (`result[name] = …` over ListMicroflows/ListPages), so the twin listed LAST won: buildFlowSignatures, buildMicroflowReturnTypes, buildPageParamNames, buildPageParamEntities, the workflow call-microflow parameter map and microflowsByQualifiedName","file":"mdl/executor/validate_datasource_args.go, helpers.go, validate_show_page_params.go, validate_workflow_task_signature.go, validate_workflow_refs.go, cmd_odata_actions.go; test validate_excluded_twin_signature_test.go","insight":"The #1469 sweep fixed first-match LOOPS (`for … { if name matches { act; return } }`), where the excluded twin wins by being listed FIRST; a name-keyed MAP is the mirror image — the twin wins by being listed LAST — so #1469's excluded-first fixtures could not see it. Grep `GetQualifiedName(.*ContainerID, .*Name)\\] = ` for value maps (boolean name sets are safe), iterate liveFirst and keep the first entry. Test both list orders; the excluded-last case is the reported one. Control: without the change, the excluded-last subtests fail with the reporter's exact message.","refs":["ako/mxcli#1928","mendixlabs/mxcli#1726","mendixlabs/mxcli#1469"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"other","date":"2026-11-04","symptom":"A unit test fails at random on one nightly leg, in setup, not in what it tests: `TestCreateMicroflow_NotesRefusedOnFullConstructor` (mdl/backend/mcp) with `copy fixture: open .mxcli/cache/unit-contents.db-journal: no such file or directory`. The same commit passes on every other leg and on re-run","cause":"A cross-package race on the checked-in fixtures, not a product defect. Since the unit-contents store (ako/mxcli#1804) any reader that opens a project writes `.mxcli/cache/unit-contents.db` next to it, and tests that open testdata/expr-checker, testdata/pedapp or modelsdk/mpr/testdata/v1-project in place write it into the fixture; its SQLite journal appears and disappears with each write. go test runs packages in parallel, so another package's whole-directory `os.CopyFS` (25 tests) or hand-written walk listed the journal and then failed to open it. Nothing under a fixture's .mxcli is tracked","file":"`internal/testutil/fixture.go` (`CopyFixture`: os.CopyFS over a view that hides every .mxcli), used by every whole-fixture copy in tests, and by cmd/mxcli's copyTree and copyFixtureTree","insight":"A cache that writes next to the project turns every checked-in fixture into shared mutable state between test packages; any whole-directory copy of one is then a race whose odds grow with the suite. Copy what the fixture commits, never its .mxcli, which also makes the copy what a fresh checkout holds whichever tests ran first (exec_atomic's hashTree already ignored .mxcli for the same reason). After a full `make test`, `find . -name .mxcli -path '*testdata*'` lists the fixtures being written in place. The race is reproduced deterministically by an fs.FS whose listing names a journal that Open reports missing: plain os.CopyFS fails with the nightly's error, CopyFixture copies; with its filter stubbed it fails the same way","refs":["ako/mxcli nightly run 41801123003 (test (Mendix 11.12.2), Unit tests)","ako/mxcli#1804"],"test":"internal/testutil/fixture_test.go"}
20 changes: 12 additions & 8 deletions .claude/skills/mendix/test-microflows/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ description: "Write and run MDL-based microflow tests with `mxcli test` — anno
| 185-227 | Test File Formats |
| 228-243 | string Operations |
| 244-267 | Running Tests |
| 268-461 | How It Works |
| 462-500 | Check a test file before you run it |
| 501-622 | Writing Good Tests |
| 623-637 | Test File Organization |
| 638-650 | Interpreting Failures |
| 651-672 | CI Integration |
| 673-679 | Related Skills |
| 680-682 | Warm test loop (`mxcli test --local [--watch]`, `--attach`, `run --local --test-endpoint`) |
| 268-465 | How It Works |
| 466-504 | Check a test file before you run it |
| 505-626 | Writing Good Tests |
| 627-641 | Test File Organization |
| 642-654 | Interpreting Failures |
| 655-676 | CI Integration |
| 677-683 | Related Skills |
| 684-686 | Warm test loop (`mxcli test --local [--watch]`, `--attach`, `run --local --test-endpoint`) |

<!-- /skill-index -->
# Test Microflows Skill
Expand Down Expand Up @@ -389,6 +389,10 @@ Three things to know before reaching for it:
- **A change needing a runtime restart is refused** — a new entity or
association. That runtime belongs to the other process. Restart it, or drop
`--attach`.
- **On Mendix 11.14.0 every attach is refused**, whatever the tests change: its
`mxbuild --serve` fails every build after its first, so the attach falls back
to a full build, and a full build asks for a runtime restart whatever changed.
Use `--local` there. (11.12.x, 11.13 and 11.15+ build incrementally.)

| | Boot | Database | Owns the runtime |
|---|---|---|---|
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **`test --attach` on Mendix 11.14.0 says why it is refused instead of blaming an entity** — that version's `mxbuild --serve` fails every build after its first, so since ako/mxcli#1917 the attach builds on a fresh serve process, whose first build asks for a runtime restart whatever changed. The refusal claimed "an entity or association changed" and suggested restarting the dev loop, which does not help; it now says the build could not be done incrementally and points to `--local`. The hosting app's pages survive either way. 11.15.0 builds incrementally and attaches as before.
- **`describe microflow` no longer leaves out a note linked to the start event or to another note** (ako/mxcli#1933, mendixlabs/mxcli#1731) — such a note counted as linked, so it was not printed as a free note, and the element it is linked to prints no `@annotation`, so it was not printed at all; re-executing the description deleted it. It is now described as a free note, after a `-- WARNING` line saying the link itself has no MDL form and a rewrite drops it.
- **Rebuilding a described decision no longer adds a merge** (ako/mxcli#1929, mendixlabs/mxcli#1727) — a `case`, `split type` or `if … elsif` one of whose branches returns is described with its own merge as a label (`end case; join shared1; merge shared1;`). Executing that as a fresh create built the decision's merge and a second one for the label, wired merge to merge, and every further describe → drop + create added another (`join junctionN; merge junctionN;`). The label's merge is now the decision's merge when nothing else reaches it, so the round trip keeps one merge.
- **`describe microflow` prints a blank `call external action` argument as `= nothing`** (ako/mxcli#1915) — Studio Pro 11.16 stores an optional OData action parameter left blank as an empty argument, and `describe` printed it as `additional = )`, which does not parse, so every `mdl 1;` `create or modify` of such a microflow was refused (MDL-V1-REBUILD). `call external action` now takes `Param = nothing`, the blank-argument spelling `call microflow` and `call nanoflow` already use; `Param = empty` (what 11.14 stored) stays a different value, and each round-trips as itself.
Expand Down
87 changes: 87 additions & 0 deletions cmd/mxcli/brain/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: Apache-2.0

// list.go - the committed store as data.
//
// `brief --json` returns each shard's rendered markdown, which is right for a
// prompt and wrong for a UI: rendering a tree from it means parsing a format
// mxcli owns, a second parser of the same thing that drifts the first time the
// format changes (ako/mxcli#1952). List hands out the parsed entries instead.
package brain

import (
"fmt"
"slices"
)

// ListFilter narrows a listing. The zero value lists everything.
type ListFilter struct {
// Slice keeps one slice's entries: its requirements and the questions
// filed against it.
Slice string
// Kind keeps decisions or requirements. An open question is a decision
// unless it was filed against a slice.
Kind Kind
// Open, when set, keeps only open questions (true) or only settled
// entries (false).
Open *bool
}

// ListedEntry is one committed entry with the shard it is in — the same shape
// `staged --json` gives a queued entry, so a consumer reads both sides alike.
type ListedEntry struct {
Entry
Shard string `json:"shard"`
}

// ListResult is a listing plus the blocks that could not be read. Those are
// reported rather than dropped: a UI that silently shows fewer entries than
// the file holds is the failure a structured read exists to prevent.
type ListResult struct {
Entries []ListedEntry `json:"entries"`
Malformed []string `json:"malformed"`
}

// List returns the committed entries matching f, in store order: project.md,
// then modules, then slices, and file order within each.
func (s *Store) List(f ListFilter) (ListResult, error) {
out := ListResult{Entries: []ListedEntry{}, Malformed: []string{}}
switch f.Kind {
case "", KindDecision, KindRequirement:
default:
return out, fmt.Errorf("kind %q: use %q or %q", f.Kind, KindDecision, KindRequirement)
}
if f.Slice != "" && f.Kind == KindDecision {
return out, fmt.Errorf("a decision has no slice, so --slice with --kind %s matches nothing; drop one of them", KindDecision)
}

shards, err := s.ListShards()
if err != nil {
return out, err
}
if f.Slice != "" {
want := PlanShard(f.Slice)
shards = slices.DeleteFunc(shards, func(sh string) bool { return sh != want })
}
for _, sh := range shards {
entries, malformed, err := s.LoadShard(sh)
if err != nil {
return out, err
}
for _, m := range malformed {
out.Malformed = append(out.Malformed, sh+": "+m)
}
for _, e := range entries {
// Explicit, so a consumer never has to know that an empty
// kind means a decision.
e.Kind = e.EntryKind()
if f.Kind != "" && e.Kind != f.Kind {
continue
}
if f.Open != nil && e.Open != *f.Open {
continue
}
out.Entries = append(out.Entries, ListedEntry{Entry: e, Shard: sh})
}
}
return out, nil
}
Loading
Loading