diff --git a/CHANGELOG.md b/CHANGELOG.md index 2334398a..c5844179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- process(inbox): **five field notes from an existing-app legacy-widget rebuild** — exec.sh keeps an MDL write when mxbuild crashes on it and the next exec runs on "Baseline NOT measured"; a signal stop of `run --test-endpoint` leaves AfterStartupMicroflow on `MxTest.RegisterEndpoint` (second field case for #244); Unit-row delete refused as ORPHAN, rename-then-drop works; a durable-rebuild kit with a "restate from DESCRIBE" STOP row (DESCRIBE omits module-role and layout-row visibility: 97 role locks in 16 documents, none shown); learned entries for File Uploader image mode (CE0463), native-mapped `radiobuttons`/`image`, tab-page `CaptionParams`/`Visible:` silently dropped, BUG-39 on widget XPath, and two-hop data-source paths. Three further notes (restore-mpr usage, build-plan-status flag order, another project's mxcli test blocking exec.sh) were dropped because master already fixes them. Mendix 11.14.0 Beta, mxcli v0.25.0, toolkit c1efcb3 — a field project (anonymised) - fix(conformance-check): read ledger rows whose pointer cell is bare (no backticks) and treat status `planned` as not-built; a ledger whose acceptance text is also bare prose is still not measured (acceptance must hold a backticked SHOW/DESCRIBE), but it now exits 2 with a FAULT that says rows were found and the text is prose, instead of "shape changed or glob matched nothing" (TK-006, partial: that ledger shape is diagnosed, not fixed) — field report TK-006 - fix(app-restart): **a secret with spaces in quotes is masked whole, and `Pwd` and `Credential` join the secret words.** Follow-up to #403: `--constant "Mod.ApiKey=two words"` and `--db-password 'two words'` printed everything after the first word (the quoted-flag case leaked before #403 too). `mask()` now masks each argument before quoting it for display, so the whole quoted value goes; the executed command is untouched, and a real-launch test proves the started app receives the raw argv; an env assignment at the start of a `--cmd` string keeps the whole command visible; the per-argument seds only run for arguments holding a secret word. The substring match still over-masks names like `Monkey` (Key) and `Turkey`; that is display only, kept, and pinned by a test. `tests/wave2/test-app-restart.sh` gains 22 cases, 8 of them failing on master. — review of #403 - fix(install-manifest): **a clean toolkit checkout passes its own `project-bin/` manifest self-check, so adopters no longer see the "named by NEITHER list" warning at the top of every sync.** `coverage-check-all.sh` (found by `review-module.sh` through its `_tool` lookup in the toolkit clone) and `ts-sync.sh` (UNPROVEN and SUPERSEDED per its header) join `MXTK_PROJECT_BIN_NOINSTALL` with reasons. New `tests/wave2/test-manifest-clean-checkout.sh` fails, rather than warns, when a `project-bin/` file is in neither list. — joshuamoesa (#381) diff --git a/contrib/inbox/2026-10-13-durable-rebuild-kit-legacy-widgets.md b/contrib/inbox/2026-10-13-durable-rebuild-kit-legacy-widgets.md new file mode 100644 index 00000000..94dd1cf7 --- /dev/null +++ b/contrib/inbox/2026-10-13-durable-rebuild-kit-legacy-widgets.md @@ -0,0 +1,83 @@ +# Durable rebuild of legacy-widget pages: a four-part kit, and "restate from DESCRIBE" as a STOP item + +**From:** a field project (anonymised) — an existing Mendix app (old widgets throughout) changed with mxcli + this toolkit +**Date:** 2026-10-13 +**Kind:** process +**Field evidence:** 79 non-writable widgets on 40 live documents (DESCRIBE `-- NOT re-executable` count) +and 97 role-locked widgets in 16 documents (BSON scan; DESCRIBE showed none). 34 pages and snippets +were restated whole; the read-back afterwards found 0 non-writable widgets and 0 role locks on them. +Two drafted scripts would have silently dropped role or row visibility had they run. Mendix 11.14.0 +Beta (Classic client, Production security), mxcli v0.25.0, toolkit c1efcb3. +**Proposed target:** `skills/existing-app-change.md` (a "legacy-widget pages" section), a STOP row in +`skills/learned-mdl-preflight.md`, possibly a `project-bin/` scan once field-proven as a file + +**Currency check:** 2026-10-16 against master d1cbd41: partly stale. STOP row 21 in `skills/learned-mdl-preflight.md` now covers restating a document another script also edits. Still open: the legacy-widget section in `skills/existing-app-change.md` and the scan for role locks that DESCRIBE does not show. +--- + +## What is already covered + +- `bug-logs/mxcli-bugs.md` BUG-115 addendum: "Grep a generated script for `NOT re-executable` before + executing it" (for the File Manager widget). +- `bug-logs/mxcli-bugs.md` (the `hasRole()` entry): no MDL syntax writes `ConditionalVisibilitySettings.ModuleRoles`. +- `skills/learned-mdl-cannot-express.md` (master bda5bed, #277): "A widget visible to one role and not + another — nothing", with the execute-grant recipe for action buttons. + +## What is not covered (findings) + +1. **DESCRIBE does not print module-role visibility.** One portal page had 19 role-locked widgets; DESCRIBE + printed none of them (nor a hidden "show page" action behind 9 image widgets). A page restated from that + DESCRIBE builds clean and shows every widget to every role. +2. **A drafted restatement said "the snippet has no visibility rules".** It restated a side-menu snippet + from DESCRIBE (navigation-list items have no names, so one item cannot be dropped alone). A BSON audit + found 13 role-locked items. Running the draft would have exposed all of them, with a clean build. +3. **DESCRIBE does not print layout-grid row visibility.** Two form pages had nine detail sections shown + by type through row visibility; a snippet had 56 row-level "show only when it has a value" rules. None + appear in DESCRIBE. The drafters re-wrote them as container `Visible:` (which DESCRIBE does print and + mxcli does write); an audit confirmed every rule was carried over. +4. **Tab-page `Visible:` and `CaptionParams` are accepted and not written** (see the learned-entries note). + Found only by reading the unit back after exec. + +So "copy the page from DESCRIBE, edit, `create or replace`" is silently lossy on any app built in Studio Pro. + +## Proposed STOP row (learned-mdl-preflight.md) + +| # | Operation | Why STOP | Do instead | +|---|---|---|---| +| new | Restate an existing page, snippet or layout from `describe` output (`create or replace`, or `alter … replace` of a container) | DESCRIBE omits module-role visibility, layout-grid row/column visibility and legacy-widget internals; check, exec and mxbuild all pass while the rules vanish | Run the visibility scan (below) on the unit first. Zero role locks and zero row rules: go. Otherwise rewrite each rule as container `Visible:` or flow access, or leave the page to `alter page` / Studio Pro | + +Probe that falsifies it: in Studio Pro, set one widget visible to one module role and one layout row +visible on an attribute; `describe page`; grep the output for the role name and the attribute. Today +(v0.25.0): 0 hits for both. + +## The kit (each part was done by hand in the field; none is a toolkit file yet) + +1. **BSON visibility scan, per document.** Decode each page/snippet/layout unit under `mprcontents/` + (the toolkit's `project-bin/mxunit_bson.py` codec is enough) and count (a) any + `ConditionalVisibilitySettings` whose `ModuleRoles` list is non-empty, (b) any + `ConditionalVisibilitySettings` on a `Forms$LayoutGridRow` or `Forms$LayoutGridColumn`. Report per + document: widget name, kind, roles or attribute. The field scans were inline one-offs, not a saved + script; a shipped version needs its own fixture. +2. **Non-writable count, per document.** `describe` every page, snippet and layout in the app's own + modules (Marketplace modules excluded) and count `-- NOT re-executable` lines, grouped by the BSON + type in the comment. Field result: `Forms$DropDown` 29, `Forms$ReferenceSelector` 27, + `Forms$DataGrid` 16, sign-in widgets 4, `Forms$TemplateGrid` 1, `Forms$ImageUploader` 1, + `Forms$SidebarToggleButton` 1. +3. **Rebuild brief per document.** Inventory first (DESCRIBE plus the scan), then each widget marked + KEEP / CUT / MOVED / NEW against the wireframe, then the restatement. Replacements used: + drop-down → `combobox` on the enumeration; reference selector → `combobox` on the association (a + microflow data source when the filter needs the context object); old data grid → Data grid 2; + template grid → `listview`; image uploader → File Uploader in files mode plus a `dynamicimage`; + role-only visibility → a per-viewer flag from a data-source microflow, or flow access (Mendix hides a + button whose flow the user cannot run). The restating script becomes the document's single owner; + older `alter page` blocks on it are marked superseded (compare #269). +4. **Post-exec read-back.** After each exec: DESCRIBE again (0 `NOT re-executable`), rerun the scan + (0 role locks unless intended), and grep the unit for every `Visible:` / `CaptionParams` the script + wrote. Item 4 above was caught only this way. + +Cost, for scale: six drafting agents in parallel; one gated exec per document (about 5–10 minutes each +with mxbuild and lint), serial because there is one writer to the `.mpr`. + +Upstream asks (mxcli), for the maintainer to file or link: print module-role and row/column visibility in +DESCRIBE (or at least a `-- role-based visibility not shown (N roles)` comment), accept it on write, give +navigation-list items stable names, and a legacy-widget migration (`DataGrid` → Data grid 2, +`ReferenceSelector`/`DropDown` → combobox) behind a dry run. diff --git a/contrib/inbox/2026-10-13-exec-keeps-write-when-mxbuild-cannot-run.md b/contrib/inbox/2026-10-13-exec-keeps-write-when-mxbuild-cannot-run.md new file mode 100644 index 00000000..3ebeb7f3 --- /dev/null +++ b/contrib/inbox/2026-10-13-exec-keeps-write-when-mxbuild-cannot-run.md @@ -0,0 +1,75 @@ +# exec.sh keeps an MDL write when mxbuild crashes after it, and the next exec runs on an unloadable model + +**From:** a field project (anonymised) — an existing Mendix app being changed with mxcli + this toolkit +**Date:** 2026-10-13 +**Kind:** bug +**Field evidence:** the project's BUILD-LOG shows two "gate could not run" rows 46 s apart: the +script that made the model unloadable, then the next script, which exec.sh accepted and ran on top +of it. Mendix 11.14.0 Beta (Classic client), mxcli v0.25.0, toolkit commit c1efcb3 (`project-bin/exec.sh` +is byte-identical on master 0c62f6f, blob a03adec). +**Proposed target:** `project-bin/exec.sh` (gate branch "mxbuild failed to run" and the pre-flight +"Baseline NOT measured" branch); one line in `skills/iterative-build-loop.md` + +--- + +## What happened (finding, reproduced once in the field) + +A page script set a data view's data source to a two-hop association path in its full form +(`$Parent/MyModule.A_B/MyModule.B/MyModule.B_C/MyModule.C`). `mxcli check` passed. After the +exec, the mxbuild loader itself crashed instead of reporting CE errors: + +``` +An error occurred when trying to set the 'DestinationEntity' property of a Entity ref step in a Page. +``` + +exec.sh printed: + +``` + ✗ mxbuild failed to run (exit 1) — gate could not verify the model. + → Snapshot preserved. Open in SP to verify manually before proceeding. +``` + +and kept the write. It did exit 1 and cleared the verification stamp (good), but the agent driving +the build ran the next script. exec.sh's pre-flight printed `⚠ Baseline NOT measured: mxbuild exited 1 +without checking the model.`, carried on, applied the next script, and hit the same crash. + +BUILD-LOG rows, verbatim except the script names: + +``` +| 2026-10-13T12:26:18 | `25g-PageA-rebuild.mdl` | fail | ❌ gate failed | 5 error(s): CE1613 — script rolled back · approval: auto (PROJECT.md) | +| 2026-10-13T12:28:46 | `25g-PageA-rebuild.mdl` | fail | ❌ gate could not run | mxbuild exit 1 · approval: auto (PROJECT.md) | +| 2026-10-13T12:29:32 | `25h-SnippetB-rebuild.mdl` | fail | ❌ gate could not run | mxbuild exit 1 · approval: auto (PROJECT.md) | +``` + +Recovery: `./bin/restore-mpr.sh ` then +`./bin/verify-model.sh` (0 errors). The 25h row had to be re-run afterwards. + +## Why it matters + +- The header comment of exec.sh says an unverified gate is rolled back **for `--patch` only**. That is + true and deliberate, but for MDL the "kept" case was designed for "mxbuild could not start" + (wrong version, no JDK). Here mxbuild started, ran on the baseline cleanly a minute earlier, and + crashed only on the written model. That is a property of the write, not of the machine. +- The next exec has no way to tell the two apart: "Baseline NOT measured" is a warning, not a refusal. + So in an unattended chain the damage compounds: every following script is written onto a model + nothing can load, and each one gets its own snapshot of the broken state. + +## Proposed fix (hypothesis, not built) + +1. **Restore when mxbuild ran clean on the baseline but cannot run after the write.** exec.sh already + has both facts: the pre-flight measured a baseline (`BASE_KNOWN=1`) and the post-exec run exits + non-zero with no errors file. In that case, call `restore_snapshot` exactly as the `--patch` path + does, and log `↩ rolled back: mxbuild crashed on the written model`. Keep today's behaviour when the + baseline itself was not measured (that is the real "machine cannot build" case). +2. **Refuse a write when the baseline cannot be measured** and the last BUILD-LOG row for this model + is `gate could not run`, with one STOP line naming `bin/restore-mpr.sh` and the snapshot to restore. + `ALLOW_UNVERIFIED=1` stays the explicit override. +3. Reword the kept-write message: "Open in SP to verify" is not something an agent can do. Something + like `STOP: the model may not load. Restore with ./bin/restore-mpr.sh ` is actionable. + +Probe that would falsify the claim (for the retest fixture): a fake mxbuild that exits 0 on the +baseline and exits 1 with no errors file after the exec; today exec.sh exits 1 and leaves the +written model in place; after the fix the model on disk equals the snapshot. + +Related: `--patch` already restores on an unverified gate (header lines 25–29); PR #304 touches the +same file (per-project lock) but not this branch. diff --git a/contrib/inbox/2026-10-13-learned-widgets-and-data-sources.md b/contrib/inbox/2026-10-13-learned-widgets-and-data-sources.md new file mode 100644 index 00000000..e5d7d710 --- /dev/null +++ b/contrib/inbox/2026-10-13-learned-widgets-and-data-sources.md @@ -0,0 +1,135 @@ +# Learned entries from a legacy-widget rebuild: File Uploader image mode, native-mapped keywords, tab pages, XPath comparands, multi-hop data sources + +**From:** a field project (anonymised) +**Date:** 2026-10-13 +**Kind:** learning +**Field evidence:** each entry below was hit while restating 34 pages of an existing app, through +`bin/exec.sh` (mxbuild delta gate) on mxcli v0.25.0, Mendix 11.14.0 Beta, Classic (Dojo) client, +toolkit c1efcb3. Error texts are verbatim; module/entity names are genericised. +**Proposed target:** per entry below (`skills/learned-file-upload-widget.md`, +`skills/learned-mdl-cannot-express.md`, `skills/learned-page-patterns.md`, `bug-logs/mxcli-bugs.md`) + +--- + +## 1. File Uploader: image mode fails the build; files mode against a `System.Image` subtype works + +Target: `skills/learned-file-upload-widget.md` §7 ("Image mode … was not tested"). +Status: files mode is ALREADY covered there (PR #139); this adds image mode and the image-preview pattern. + +```sql +fileuploader upPictures ( + uploadMode: 'images', + associatedImages: association $currentObject/MyModule.Picture_Asset, + readOnlyMode: true, + enableCustomButtons: false +) +``` + +`mxcli check` passed. mxbuild: `CE0463 The definition of this widget has changed.` on +`File uploader upPictures`; the gate rolled back (rollback verified by DESCRIBE). The same widget with +`uploadMode: 'files'` builds clean (finding). + +What built clean instead (finding, mxbuild 0 errors): uploads in **files** mode against an entity that +extends `System.Image` (which extends `FileDocument`), with a separate `dynamicimage` preview beside it; +read-only picture lists as a `listview` whose template holds a `dynamicimage`. + +Also stale: `skills/learned-mdl-cannot-express.md` row 1 still says "A file upload control — nothing … +⛔ not buildable", which contradicts `learned-file-upload-widget.md`. Suggest pointing that row at the +File Uploader skill. + +Related (finding): the installed FileDropper widget's definition lists `dataFileEntity` and +`dataContextAssociation` as known properties with no mapping; `check` accepts them, exec does not write +them, and mxbuild then fails. Upstream ask: refuse unmapped definition properties with MDL-WIDGET07. + +## 2. `radiobuttons` and `image` can resolve to NATIVE mobile widgets on a web page + +Target: `skills/learned-page-patterns.md` (or the widgets section of `learned-mdl-cannot-express.md`). +Status: NEW (no toolkit mention). + +In this project `DESCRIBE WIDGET TYPE radiobuttons` resolves to `com.mendix.widget.native.radiobuttons`, +and `check` warns MDL-WIDGET10 that the label is dropped. The project's `image.def.json` maps `image` to +`com.mendix.widget.native.image.Image`; a probe wrote `image` with no properties. DESCRIBE prints existing +**web** radio groups with the same `radiobuttons` keyword, so re-running DESCRIBE output writes a native +widget into a web page (finding for the resolution and the warning; the radiobuttons case was not taken +through mxbuild). + +Rule used: Boolean → `checkbox`, enumeration → `combobox`, images → `dynamicimage`. Before relying on a +keyword in an existing app, run `DESCRIBE WIDGET TYPE ` and check the id is a `web` widget. + +## 3. Tab pages: `CaptionParams` and `Visible:` pass every gate and are not written + +Target: `bug-logs/mxcli-bugs.md` as `## BUG-DRAFT-tabpage-captionparams-visible-dropped:`, plus a row in +`learned-mdl-cannot-express.md` next to the existing "count badge on a tab" row. +Status: NEW. (The existing row covers `Badge:`, which `check` refuses; these two are accepted.) + +```sql +tabpage tpDue (Caption: 'Due today or earlier · {1}', CaptionParams: ({1} = DueCount)) { … } +tabpage tpOther (Caption: 'Unassigned · {1}', Visible: $currentObject/ShowOtherTab) { … } +``` + +`check`, exec and mxbuild all passed, with no MDL-WIDGET07. The stored caption is literal text (the tab +reads `· {1}`), and the tab page has no visibility setting: `ShowOtherTab` occurs 0 times in the unit. +Found by a BSON read-back after exec (finding). + +Rule: no dynamic captions and no visibility on tab pages; put the count in a line inside the tab, and +put anything conditional in a container with `Visible:` (container visibility is written correctly). + +## 4. DESCRIBE omits module-role visibility and layout-grid row visibility + +Target and detail: see the inbox note "Durable rebuild of legacy-widget pages" (same date). Listed +here so triage sees it among the learned entries. Status: PARTLY (role visibility is known to be +unwritable; that DESCRIBE also hides it is not recorded). + +## 5. Page XPath with a `$currentObject/` comparand → CE0161 (BUG-39 on widgets) + +Target: `bug-logs/mxcli-bugs.md` BUG-39 (add a confirmation), `learned-page-patterns.md`. +Status: PARTLY. BUG-39 records it for microflow retrieves only. + +```sql +combobox cbLevel2 ( + Attribute: Helper_Level2, + DataSource: database from "MyModule"."Level2" + where [MyModule.Level2_Level1 = $currentObject/MyModule.Helper_Level1] +) +``` + +`check --references` passed; mxbuild: 3 × `CE0161 Error(s) in XPath constraint`, one per cascading combo +box; rolled back (finding). Studio Pro accepts the same constraint. + +Workaround that built: each combo box takes its options from a microflow data source that retrieves the +parent into a variable and filters on it. + +## 6. Two-hop association data source: neither path form is writable → use a microflow data source + +Target: `learned-page-patterns.md` (next to the BUG-04 multi-hop ContentParams section), and +`bug-logs/mxcli-bugs.md` as `## BUG-DRAFT-two-hop-datasource-path:`. +Status: NEW. BUG-20 (cross-module traversal writing a null `DestinationEntityId`) was fixed in v0.17.0; +this is a same-module, two-hop path on v0.25.0. + +Short form, exactly as DESCRIBE prints it: + +```sql +DataSource: $Parent/MyModule.A_B/MyModule.B_C +``` + +mxcli stored the last segment as an entity: +`CE1613 The selected entity 'MyModule.B_C' no longer exists.` (5 errors); rolled back. + +Full form: + +```sql +DataSource: $Parent/MyModule.A_B/MyModule.B/MyModule.B_C/MyModule.C +``` + +`check` passed; the mxbuild loader crashed: +`An error occurred when trying to set the 'DestinationEntity' property of a Entity ref step in a Page.` +(This crash is what exposed the exec.sh gap in the separate inbox note.) + +So DESCRIBE output containing such a path is not re-executable. Rule: no multi-hop association path as a +data-view source; use a microflow data source that retrieves over each association in turn (built clean). + +## Not included here + +Other cases from the same run (Data grid 2 custom-content column losing its Attribute; XPath through a +reverse association needing read access mxbuild does not check; names `check --references` does not +validate) are upstream mxcli material and are left for a separate note. diff --git a/contrib/inbox/2026-10-13-patch-unit-row-delete-orphan.md b/contrib/inbox/2026-10-13-patch-unit-row-delete-orphan.md new file mode 100644 index 00000000..9355c51b --- /dev/null +++ b/contrib/inbox/2026-10-13-patch-unit-row-delete-orphan.md @@ -0,0 +1,43 @@ +# exec.sh --patch: deleting a Unit row is refused by `mxcli fix hashes --repair` (ORPHAN file); rename-then-drop is the route that works + +**From:** a field project (anonymised) +**Date:** 2026-10-12 +**Kind:** learning +**Field evidence:** two pages with the same qualified name (a live one and an excluded, archived copy in +another folder). mxcli v0.25.0, Mendix 11.14.0 Beta, MPR v2, toolkit c1efcb3. The failed patch was +rolled back by exec.sh; the second route built at 0 errors. +**Proposed target:** `skills/learned-mdl-preflight.md` (a workaround row) or `skills/learned-mdl-cannot-express.md`; +an upstream mxcli ask + +--- + +## Situation + +`MyModule.Detail` existed twice: the live page and an archived copy (excluded from the project) with +the same name in `Archive/Pages`. mxcli addresses documents by qualified name only, and different +commands resolved the name to different copies: `DESCRIBE PAGE` found the live one, `SHOW ACCESS ON PAGE` +the archived one, so a `grant view` by name landed on the archived copy. There is no `drop page … in +folder '…'` and no drop by unit id. + +## Route 1, refused (finding) + +A gated patch through `./bin/exec.sh --patch` deleted the archived page's row from the `.mpr` `Unit` +table and left its `.mxunit` file on disk (file removal being left to the owner). The patch's +`mxcli fix hashes --repair` step then reported the file as **ORPHAN** (present on disk, not indexed) +and failed; exec.sh restored the snapshot. Afterwards both pages were present and 3161 units verified. + +## Route 2, worked (finding) + +1. A gated patch renamed only the archived unit's top-level `Name` to `Detail_Archived`, after a + byte-identical decode/encode round-trip check of that unit. mxbuild stayed at 0 errors. +2. Then plain MDL, through exec.sh: `drop page if exists "MyModule"."Detail_Archived";` + +## Rule proposed + +To remove one of two same-named documents: rename it by patch, then drop it by MDL. Never delete a +`Unit` row by hand; `fix hashes --repair` treats the leftover file as an orphan and the gate rolls back. + +Ask for upstream mxcli: warn when a qualified name resolves to more than one document, make every +command resolve it the same way, and add `drop page … in folder '…'` or a drop by unit id. + +Related, not the same: #294 (a shared patch helper) and #295 (adopt `mxcli fix hashes` in `--patch`). diff --git a/contrib/inbox/2026-10-13-test-endpoint-after-startup-left-after-signal.md b/contrib/inbox/2026-10-13-test-endpoint-after-startup-left-after-signal.md new file mode 100644 index 00000000..91d26f81 --- /dev/null +++ b/contrib/inbox/2026-10-13-test-endpoint-after-startup-left-after-signal.md @@ -0,0 +1,56 @@ +# `mxcli run --local --test-endpoint` stopped by a signal leaves AfterStartupMicroflow on the injected flow (second field case for #244, and the toolkit-side half) + +**From:** a field project (anonymised) +**Date:** 2026-10-13 +**Kind:** bug +**Field evidence:** an agent stopped a running `mxcli run --local --test-endpoint` (a background-task +stop, so a signal, not a clean exit) to restart the app. Later a clean-up removed the injected +`MxTest` module but not the setting; exec.sh's pre-flight then reported the model "ALREADY has 1 error" +(CE1613), and the next app start recursed with a StackOverflowError in `MxTest.RegisterEndpoint`. +mxcli v0.25.0, Mendix 11.14.0 Beta, toolkit c1efcb3. +**Proposed target:** upstream mxcli for the root cause (already logged here as #244, closed 2026-10-14 after the toolkit refusal landed in the integration PR #329; the upstream root cause remains); in the +toolkit, `skills/testing-shape.md` (the `--test-endpoint` paragraph) and an exec.sh pre-flight hint + +**Currency check:** 2026-10-16 against master d1cbd41: partly stale. exec.sh's pre-flight now refuses on a leftover injected setting and prints the `ALTER SETTINGS` repair (`_common.sh`), so the exec.sh hint half is done. Still open: the upstream root cause behind #244 (closed 2026-10-14 once the toolkit refusal landed), and `skills/testing-shape.md` does not say that a stop by signal skips the restore. +--- + +## What is already covered + +- #244 (closed 2026-10-14; the toolkit refusal landed in #329, the upstream root cause remains) describes the same family on mxcli v0.25.0 / Mendix 11.15.0: the wrapper chains the + "current" after-startup flow, which is already `MxTest.RegisterEndpoint`, so it calls itself. It + asks upstream to restore on interrupt and to refuse a self-wrap. +- `skills/testing-shape.md` says `--test-endpoint` writes into the working model while the app runs and + that "a clean stop prints 'test endpoint removed; project restored'". It does not say that a stop by + signal skips that restore. + +## What this case adds + +1. **Trigger is a signal stop of `run --test-endpoint` itself**, not an interrupted `mxcli test` run. + Agents routinely stop background tasks this way to restart an app. +2. **A partial clean-up makes it worse.** Removing the `MxTest` module afterwards (by hand or by a later + cleanup) leaves `AfterStartupMicroflow` pointing at a flow that no longer exists: CE1613 at build, + which exec.sh's delta gate then carries as a "pre-existing" error on every later script. Re-creating + it by starting with `--test-endpoint` again gives the self-recursion from #244. +3. **The repair that worked**, run through `bin/exec.sh` (an `ALTER SETTINGS`, said in chat): + + ```sql + mdl 1; + alter settings runtime (AfterStartupMicroflow: 'MyModule.ASU_Startup'); + ``` + + mxbuild went back to 0 errors; the app then started normally. + +## Proposed toolkit changes (hypothesis, not built) + +- `testing-shape.md`: add "Stop `run --test-endpoint` with Ctrl+C in its own terminal or let it exit; + a kill or task stop skips the restore. Afterwards `git status` on the model must be empty; if the + Settings unit is still modified, restore it from git (or with the `alter settings runtime` line + above) before any exec." Run build-time apps without `--test-endpoint` (the skill already says so). +- `exec.sh` pre-flight: when the baseline holds CE1613 on the project Settings, or `describe settings` + shows `AfterStartupMicroflow` starting with `MxTest.`, print one line naming the cause and the repair, + instead of only "Model ALREADY has 1 error(s)". +- Upstream (#244): restore the original after-startup on SIGTERM/SIGINT too, and refuse to wrap an + after-startup that is already `MxTest.RegisterEndpoint`. + +Probe: start `mxcli run --local --test-endpoint` on a scratch copy, stop it with `kill -TERM `, +then `describe settings`. Today (v0.25.0): after-startup is still `MxTest.RegisterEndpoint`.