From a58f9e493dfe4a11c2a5f49baf8ae28f508ae857 Mon Sep 17 00:00:00 2001 From: MendixMau Date: Mon, 12 Oct 2026 19:35:20 +0800 Subject: [PATCH 1/2] =?UTF-8?q?new(skill):=20rest-between-mendix-apps=20?= =?UTF-8?q?=E2=80=94=20contract,=20consumer=20honesty,=20cross-app=20test?= =?UTF-8?q?=20ladder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REST between Mendix apps you build yourself: the decisions to write down before the first script (pull vs push, versioned path, key + version rule, cursor paging, correlation id, error shape incl. the runtime's 560, auth in the operation microflow per BUG-152, ports, one owner per fact), 17 publisher and consumer rules, a six-rung test ladder (publisher contract tests, consumer logic tests, fixture server per upstream, mock-vs-live contract diff, curl status matrix, cross-app consistency, cross-app journeys) with a per-change/slice/release cadence, and a 26-entry failure register drawn from three demo apps (22 backed by a FINDINGS line, 4 by a proof doc or UX scan). Routed as ondemand build/integration for architect, mdl, test, review at stages 3-6; rest-integration-first-time-right.md links to it. Co-Authored-By: Claude Opus 5.5 --- README.md | 1 + ROUTING.md | 1 + agents/architect-agent.md | 1 + agents/mdl-agent.md | 1 + agents/review-agent.md | 1 + agents/test-agent.md | 1 + bin/gate-check.sh | 8 +- bin/lib/skill-routing.tsv | 1 + skills/rest-between-mendix-apps.md | 242 ++++++++++++++++++++ skills/rest-integration-first-time-right.md | 2 + 10 files changed, 255 insertions(+), 4 deletions(-) create mode 100644 skills/rest-between-mendix-apps.md diff --git a/README.md b/README.md index 465a11f5..bdc4262f 100644 --- a/README.md +++ b/README.md @@ -636,6 +636,7 @@ Every mxcli project has a `.ai-context/skills/` directory (bundled by `mxcli ini | Task | Skill to load | |---|---| | Building a REST integration (consumed or published) for the first time on a project — the checks that avoid a rebuild after the first live call | `skills/rest-integration-first-time-right.md` | +| Two or more Mendix apps you build that read each other over REST — before the contract, before the first consumer, and before calling an API tested: pull vs push, key+version rule, auth in the operation microflow (BUG-152), honest last-success/stale/"as of", one owner per fact, and the six-rung cross-app test ladder with its per-change/slice/release cadence | `skills/rest-between-mendix-apps.md` | | Adding a constant, installing a marketplace module, deploying to a new environment, or needing a Mendix PAT — where an encryption key, API credential, endpoint or access token gets its value, why a free node has only one channel, and why a token missing from the files you grepped is not missing | `skills/learned-constants-and-secrets.md` | **Verify — does it work** diff --git a/ROUTING.md b/ROUTING.md index d2c508e7..2d767ce4 100644 --- a/ROUTING.md +++ b/ROUTING.md @@ -189,6 +189,7 @@ picks the row up. That is the whole procedure — there is no second list to rem | Always relevant for | Load this | Agent(s) | Stage(s) | Tier | |---|---|---|---|---| | Building a REST integration (consumed or published) for the first time on a project — the checks that avoid a rebuild after the first live call | `skills/rest-integration-first-time-right.md` | mdl | 4,5 | ondemand | +| Two or more Mendix apps you build that read each other over REST — before the contract, before the first consumer, and before calling an API tested: pull vs push, key+version rule, auth in the operation microflow (BUG-152), honest last-success/stale/"as of", one owner per fact, and the six-rung cross-app test ladder with its per-change/slice/release cadence | `skills/rest-between-mendix-apps.md` | architect,mdl,test,review | 3,4,5,6 | ondemand | | Adding a constant, installing a marketplace module, deploying to a new environment, or needing a Mendix PAT — where an encryption key, API credential, endpoint or access token gets its value, why a free node has only one channel, and why a token missing from the files you grepped is not missing | `skills/learned-constants-and-secrets.md` | mdl,architect,gate | 4,5,6 | ondemand | #### Build · Security diff --git a/agents/architect-agent.md b/agents/architect-agent.md index 6dfcfbeb..09521aa6 100644 --- a/agents/architect-agent.md +++ b/agents/architect-agent.md @@ -75,6 +75,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/agent-permission-friction.md` | Any refused, denied or blocked command | | `project-bin/build-plan-status.sh` | After marking a module done, or any time "how much is built vs proven" is asked | | `skills/workflow-structure-rules.md` | Designing or reviewing a Workflow's SHAPE before or after the MDL | +| `skills/rest-between-mendix-apps.md` | Two or more Mendix apps you build that read each other over REST | | `skills/mendix-epics-api.md` | Working with the Mendix Epics board programmatically | | `skills/upstream-feedback.md` | About to open an issue, PR or discussion against mxcli or the toolkit | | `skills/doctor-triage.md` | doctor.sh reports FAIL or WARN, or someone asks whether a red setup line blocks them | diff --git a/agents/mdl-agent.md b/agents/mdl-agent.md index 1bca74dd..ce3ea64e 100644 --- a/agents/mdl-agent.md +++ b/agents/mdl-agent.md @@ -91,6 +91,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/learned-workflow-patterns.md` | Writing or debugging a Mendix native Workflow (CREATE WORKFLOW/USER TASK/OUTCOMES) | | `skills/workflow-structure-rules.md` | Designing or reviewing a Workflow's SHAPE before or after the MDL | | `skills/rest-integration-first-time-right.md` | Building a REST integration (consumed or published) for the first time on a project | +| `skills/rest-between-mendix-apps.md` | Two or more Mendix apps you build that read each other over REST | | `skills/bug-submission-checklist.md` | Preparing an mxcli/Studio Pro bug for submission | | `skills/upstream-feedback.md` | About to open an issue, PR or discussion against mxcli or the toolkit | | `skills/empty-widget-triage.md` | A page/grid/combobox renders empty (blank cells, zero rows, zero options) during UI review or an e2e run | diff --git a/agents/review-agent.md b/agents/review-agent.md index bf373cbf..9c0fc7ce 100644 --- a/agents/review-agent.md +++ b/agents/review-agent.md @@ -90,6 +90,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/journey-examples.md` | Writing an actual .journey.json | | `skills/wiring-sweep.md` | Every module before it is called done | | `skills/workflow-structure-rules.md` | Designing or reviewing a Workflow's SHAPE before or after the MDL | +| `skills/rest-between-mendix-apps.md` | Two or more Mendix apps you build that read each other over REST | | `skills/bug-submission-checklist.md` | Preparing an mxcli/Studio Pro bug for submission | | `skills/upstream-feedback.md` | About to open an issue, PR or discussion against mxcli or the toolkit | | `skills/empty-widget-triage.md` | A page/grid/combobox renders empty (blank cells, zero rows, zero options) during UI review or an e2e run | diff --git a/agents/test-agent.md b/agents/test-agent.md index 980d5e8a..695af176 100644 --- a/agents/test-agent.md +++ b/agents/test-agent.md @@ -65,6 +65,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/improvement-register.md` | Any review pass that runs more than once | | `skills/journey-examples.md` | Writing an actual .journey.json | | `skills/wiring-sweep.md` | Every module before it is called done | +| `skills/rest-between-mendix-apps.md` | Two or more Mendix apps you build that read each other over REST | | `skills/upstream-feedback.md` | About to open an issue, PR or discussion against mxcli or the toolkit | | `skills/empty-widget-triage.md` | A page/grid/combobox renders empty (blank cells, zero rows, zero options) during UI review or an e2e run | | `skills/doctor-triage.md` | doctor.sh reports FAIL or WARN, or someone asks whether a red setup line blocks them | diff --git a/bin/gate-check.sh b/bin/gate-check.sh index 23e6e797..e6d1af52 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -1769,10 +1769,10 @@ stage_protocol_paths() { 0) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-scope.md skills/source-triage.md skills/image-transcription.md skills/small-project-tier.md skills/existing-app-change.md skills/app-analysis.md skills/layering-review.md skills/module-dependency-review.md skills/microflow-loop-antipatterns.md skills/assess-migration.md skills/migration-pipeline.md skills/migrate-general.md skills/migrate-outsystems.md skills/source-os11.md skills/os-xml-schema.md skills/source-node-express-react.md skills/document-discovery.md skills/extractor-quality-loop.md skills/qa-loop-goal-pattern.md skills/mendix-epics-api.md skills/corpus-extraction-integrity.md skills/gate-check-file-locations.md" ;; 1) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-extraction.md skills/image-transcription.md skills/small-project-tier.md skills/migration-pipeline.md skills/source-os11.md skills/os-xml-schema.md skills/source-node-express-react.md skills/document-discovery.md skills/extractor-quality-loop.md skills/kb-generation.md skills/corpus-extraction-integrity.md" ;; 2) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-brd.md skills/checkpoints/checkpoint-architecture.md skills/image-transcription.md skills/small-project-tier.md skills/kb-generation.md skills/brd-generation.md skills/brd-validation.md" ;; - 3) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-design.md skills/small-project-tier.md skills/mendix-best-practices-index.md skills/layering-review.md skills/architecture-blueprint.md skills/modularize-domain.md skills/design-artifacts.md skills/brd-to-build-plan.md skills/workflow-structure-rules.md skills/learned-mdl-cannot-express.md" ;; - 4) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-build.md skills/agent-roles.md skills/small-project-tier.md skills/module-brief.md skills/module-folder-convention.md skills/brd-to-build-plan.md skills/coverage-ledger.md skills/workflow-structure-rules.md skills/rest-integration-first-time-right.md skills/learned-constants-and-secrets.md" ;; - 5|build-ready) echo "skills/interview-protocol.md skills/grill-mode.md skills/agent-roles.md skills/module-brief.md skills/learned-mdl-preflight.md skills/module-folder-convention.md skills/learned-microflow-patterns.md skills/microflow-preflight.md skills/mendix-best-practices-index.md skills/ui-preflight-pages.md skills/design-spacing.md skills/learned-stylegallery.md skills/ui-loop.md skills/module-review.md skills/testing-shape.md skills/microflow-loop-antipatterns.md skills/iterative-build-loop.md skills/mdl-cookbook-microflows.md skills/build/mdl/oneshot-mdl-method.md skills/learned-page-patterns.md skills/oneshot-page-structure-patterns.md skills/mendix-agents.md skills/mendix-agent-ui.md skills/mendix-agent-setup.md skills/fixture-seeding.md skills/journey-proof.md skills/unhappy-path-testing.md skills/monkey-test.md skills/e2e-locators-that-lie.md skills/report-schema.md skills/harness-architecture.md skills/process-coherence-pass.md skills/lint-that-actually-runs.md skills/improvement-register.md skills/journey-examples.md skills/wiring-sweep.md skills/learned-workflow-patterns.md skills/workflow-structure-rules.md skills/rest-integration-first-time-right.md skills/bug-submission-checklist.md skills/empty-widget-triage.md skills/learned-sidebar-collapse-icons.md skills/learned-popup-navigation.md skills/learned-datagrid-customcontent-binding.md skills/learned-popup-feedback-pattern.md skills/learned-mdl-cannot-express.md skills/learned-css-that-never-applied.md skills/learned-detection-gaps.md skills/learned-dg2-patterns.md skills/learned-file-upload-widget.md skills/security-is-not-a-later-script.md skills/learned-local-db-confusion.md skills/full-harness-audit.md skills/test-result-audit.md skills/finding-disposition.md skills/preview-over-hub-tunnel.md skills/walking-skeleton.md skills/platform-link.md skills/teamserver-alignment.md skills/learned-constants-and-secrets.md" ;; - 6) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-cutover.md skills/mendix-best-practices-index.md skills/module-review.md skills/testing-shape.md skills/existing-app-assurance.md skills/app-analysis.md skills/module-dependency-review.md skills/microflow-loop-antipatterns.md skills/qa-loop-goal-pattern.md skills/mendix-agent-setup.md skills/e2e-harness-base.md skills/learned-db-assertions.md skills/fixture-seeding.md skills/journey-proof.md skills/unhappy-path-testing.md skills/monkey-test.md skills/e2e-locators-that-lie.md skills/learned-skill-ux-audit.md skills/learned-skill-scope-delta.md skills/report-schema.md skills/harness-architecture.md skills/process-coherence-pass.md skills/e2e-evidence-report.md skills/record-demo-video.md skills/share-demo-package.md skills/lint-that-actually-runs.md skills/improvement-register.md skills/journey-examples.md skills/wiring-sweep.md skills/workflow-structure-rules.md skills/bug-submission-checklist.md skills/empty-widget-triage.md skills/anonymize-client-app-for-demo.md skills/learned-css-that-never-applied.md skills/learned-detection-gaps.md skills/learned-local-db-confusion.md skills/full-harness-audit.md skills/test-result-audit.md skills/finding-disposition.md skills/handoff-to-studio-pro.md skills/preview-over-hub-tunnel.md skills/platform-link.md skills/teamserver-alignment.md skills/learned-constants-and-secrets.md" ;; + 3) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-design.md skills/small-project-tier.md skills/mendix-best-practices-index.md skills/layering-review.md skills/architecture-blueprint.md skills/modularize-domain.md skills/design-artifacts.md skills/brd-to-build-plan.md skills/workflow-structure-rules.md skills/rest-between-mendix-apps.md skills/learned-mdl-cannot-express.md" ;; + 4) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-build.md skills/agent-roles.md skills/small-project-tier.md skills/module-brief.md skills/module-folder-convention.md skills/brd-to-build-plan.md skills/coverage-ledger.md skills/workflow-structure-rules.md skills/rest-integration-first-time-right.md skills/rest-between-mendix-apps.md skills/learned-constants-and-secrets.md" ;; + 5|build-ready) echo "skills/interview-protocol.md skills/grill-mode.md skills/agent-roles.md skills/module-brief.md skills/learned-mdl-preflight.md skills/module-folder-convention.md skills/learned-microflow-patterns.md skills/microflow-preflight.md skills/mendix-best-practices-index.md skills/ui-preflight-pages.md skills/design-spacing.md skills/learned-stylegallery.md skills/ui-loop.md skills/module-review.md skills/testing-shape.md skills/microflow-loop-antipatterns.md skills/iterative-build-loop.md skills/mdl-cookbook-microflows.md skills/build/mdl/oneshot-mdl-method.md skills/learned-page-patterns.md skills/oneshot-page-structure-patterns.md skills/mendix-agents.md skills/mendix-agent-ui.md skills/mendix-agent-setup.md skills/fixture-seeding.md skills/journey-proof.md skills/unhappy-path-testing.md skills/monkey-test.md skills/e2e-locators-that-lie.md skills/report-schema.md skills/harness-architecture.md skills/process-coherence-pass.md skills/lint-that-actually-runs.md skills/improvement-register.md skills/journey-examples.md skills/wiring-sweep.md skills/learned-workflow-patterns.md skills/workflow-structure-rules.md skills/rest-integration-first-time-right.md skills/rest-between-mendix-apps.md skills/bug-submission-checklist.md skills/empty-widget-triage.md skills/learned-sidebar-collapse-icons.md skills/learned-popup-navigation.md skills/learned-datagrid-customcontent-binding.md skills/learned-popup-feedback-pattern.md skills/learned-mdl-cannot-express.md skills/learned-css-that-never-applied.md skills/learned-detection-gaps.md skills/learned-dg2-patterns.md skills/learned-file-upload-widget.md skills/security-is-not-a-later-script.md skills/learned-local-db-confusion.md skills/full-harness-audit.md skills/test-result-audit.md skills/finding-disposition.md skills/preview-over-hub-tunnel.md skills/walking-skeleton.md skills/platform-link.md skills/teamserver-alignment.md skills/learned-constants-and-secrets.md" ;; + 6) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-cutover.md skills/mendix-best-practices-index.md skills/module-review.md skills/testing-shape.md skills/existing-app-assurance.md skills/app-analysis.md skills/module-dependency-review.md skills/microflow-loop-antipatterns.md skills/qa-loop-goal-pattern.md skills/mendix-agent-setup.md skills/e2e-harness-base.md skills/learned-db-assertions.md skills/fixture-seeding.md skills/journey-proof.md skills/unhappy-path-testing.md skills/monkey-test.md skills/e2e-locators-that-lie.md skills/learned-skill-ux-audit.md skills/learned-skill-scope-delta.md skills/report-schema.md skills/harness-architecture.md skills/process-coherence-pass.md skills/e2e-evidence-report.md skills/record-demo-video.md skills/share-demo-package.md skills/lint-that-actually-runs.md skills/improvement-register.md skills/journey-examples.md skills/wiring-sweep.md skills/workflow-structure-rules.md skills/rest-between-mendix-apps.md skills/bug-submission-checklist.md skills/empty-widget-triage.md skills/anonymize-client-app-for-demo.md skills/learned-css-that-never-applied.md skills/learned-detection-gaps.md skills/learned-local-db-confusion.md skills/full-harness-audit.md skills/test-result-audit.md skills/finding-disposition.md skills/handoff-to-studio-pro.md skills/preview-over-hub-tunnel.md skills/platform-link.md skills/teamserver-alignment.md skills/learned-constants-and-secrets.md" ;; 7) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-cutover.md skills/close-the-loop.md skills/share-demo-package.md skills/handoff-to-studio-pro.md skills/platform-link.md skills/teamserver-alignment.md skills/deploy-to-sandbox.md" ;; *) echo "" ;; # diff --git a/bin/lib/skill-routing.tsv b/bin/lib/skill-routing.tsv index c0fd385e..ea6a9fcd 100644 --- a/bin/lib/skill-routing.tsv +++ b/bin/lib/skill-routing.tsv @@ -187,6 +187,7 @@ wiring-sweep skills/wiring-sweep.md Every module before it is called done — do learned-workflow-patterns skills/learned-workflow-patterns.md Writing or debugging a Mendix native Workflow (CREATE WORKFLOW/USER TASK/OUTCOMES) — syntax, the 11 workflow microflow statements, DECISION vs CALL MICROFLOW, and the two corruption classes (binary-version $Type, and create-before-reference) mdl 5 ondemand build/workflow workflow-structure-rules skills/workflow-structure-rules.md Designing or reviewing a Workflow's SHAPE before or after the MDL — where a path may end, boundary event vs event sub-process, parallel-split limits, outcome minimums, targeting from the sentence, multi-user decision methods, which edits break running instances; and any CE6689/CE1844/CE1845/MW0012 after a clean mxcli check architect,mdl,review 3,4,5,6 ondemand build/workflow rest-integration-first-time-right skills/rest-integration-first-time-right.md Building a REST integration (consumed or published) for the first time on a project — the checks that avoid a rebuild after the first live call mdl 4,5 ondemand build/integration +rest-between-mendix-apps skills/rest-between-mendix-apps.md Two or more Mendix apps you build that read each other over REST — before the contract, before the first consumer, and before calling an API tested: pull vs push, key+version rule, auth in the operation microflow (BUG-152), honest last-success/stale/"as of", one owner per fact, and the six-rung cross-app test ladder with its per-change/slice/release cadence architect,mdl,test,review 3,4,5,6 ondemand build/integration mendix-epics-api skills/mendix-epics-api.md Working with the Mendix Epics board programmatically — creating/reading stories and epics, updating workflow state, or integrating BRDs with the portal ba,architect P,0 ondemand reference bug-submission-checklist skills/bug-submission-checklist.md Preparing an mxcli/Studio Pro bug for submission — scope pinning, read-back-vs-write-path verification, gate-sensitivity negative controls, severity scoping, before it's called filable mdl,gate,review 5,6 ondemand diagnose upstream-feedback skills/upstream-feedback.md About to open an issue, PR or discussion against mxcli or the toolkit — before drafting, choosing which repo and vehicle it belongs to all - ondemand diagnose diff --git a/skills/rest-between-mendix-apps.md b/skills/rest-between-mendix-apps.md new file mode 100644 index 00000000..8debe210 --- /dev/null +++ b/skills/rest-between-mendix-apps.md @@ -0,0 +1,242 @@ +# REST between Mendix apps: contract, consumer honesty, and a test ladder that stays cheap + +**Applies to:** two or more Mendix apps that you build yourself and that read each other's data over +REST ("microservices between Mendix apps"). One app publishes what it owns; another polls it. + +**Purpose:** the decisions to make before the first script, the consumer habits that keep a page +honest about stale data, and how to test across apps without paying for a full multi-app run on +every change. + +**Evidence:** three demo apps built with mxcli v0.24 to v0.26 on Mendix 11.15, October 2026: a +**change app** (publishes `rest/change-notices/v1` and `rest/service-bulletins/v1`), a **supply app** +(polls the change app, publishes `rest/change-readiness/v1` and two supply feeds) and a **service app** +(publishes four fleet feeds and polls both others). Nine published APIs, five consumer directions. +Every rule below that is not marked **(recommendation)** comes from a failure or proof recorded in +one of the three apps' FINDINGS files or proof documents. §5 lists them. + +**Not this file:** + +| For | Read | +|---|---| +| JSON structure, import mapping, the `all` range flag, a REST-fed DataGrid | `rest-integration-first-time-right.md` | +| Which channel a password or base URL comes from | `learned-constants-and-secrets.md` | +| What "tested" means for one module, and the false-green register | `testing-shape.md` | +| MDL syntax | `./mxcli syntax rest` → `rest.published`, `rest.consumed`, `rest.call`; `./mxcli syntax integration` | +| A stand-in for a third-party API (Prism, proxy) | the mxcli skill `mock-rest-apis` | + +--- + +## 1. Before the first script: write the contract down + +Write the contract **in the publisher's repo** as an OpenAPI file per API, plus one short markdown +file with the decisions. The consumer's team reviews it; nothing is built until both agree. +In the evidence, every API had an OpenAPI file, and the decisions table (pull only, key, version +rule, auth, versioning) was settled before either side wrote MDL. + +Decide these, and write each down: + +| Decision | What the evidence did | Why | +|---|---|---| +| **Pull or push** | Pull only, in every direction. Each app publishes what it owns; the other polls. | No outbox, no retry queue, no inbound write endpoint. An app that is down is simply not polled, and nothing is lost. Push is a valid choice, but it needs all three. | +| **Versioned path** | `rest//v1`. Additive changes only inside a version; a breaking change is `/v2` beside `/v1`. Every body carries `contractVersion`. | The consumer can test against a `version` mode of its fixture (§3) and refuse a body it does not understand. | +| **Record key and version** | One key per record (for example `CHG-0001/PART-B/SUP-A`) and an integer version, +1 on every content change. Receivers upsert on the key and ignore a version not higher than the one stored. | Replays, overlapping polls and a restore from backup all land on the same state. | +| **Paging** | Opaque cursor: `nextCursor` from the previous page, `null` on the last page. Cursor = base64 of `updatedAt|key`; order by `updatedAt`, then key. | Stable under inserts. A row with an empty `updatedAt` never matches a cursor, so the seed must set it on every row. | +| **Incremental read** | `since=`; the consumer sends its last fully processed `updatedAt` minus an overlap (2 minutes in the evidence). | The overlap is safe only because of the version rule above. | +| **Correlation** | The caller sends `X-Correlation-Id`; the publisher echoes it in the response header and in every error body, and both sides log it. | One id to grep in two runtime logs. | +| **Error shape** | `{ "code": "bad_limit", "message": "...", "correlationId": "..." }`. 401 bad or missing credentials (with `WWW-Authenticate`), 403 known account not allowed on this API, 404 unknown key, 422 bad parameter. | See §2 for the 560 the runtime sends before your microflow runs. | +| **Auth** | One service account per direction, named `-reader`, HTTP Basic; HTTPS outside dev. | See §2: in MDL the check lives in the operation microflow. | +| **Ports** | A fixed port per app, written in the plan (in the evidence 8085, 8086, 8087). Never 8081/8091: `mxcli test --local` binds those. | A wrong app answering on a shared port returns 200 with the wrong body. Smoke tests check the body, not only the status. | +| **Who owns each fact** | A table: fact → owning app. The consumer stores a *copy* with an "as of" time and never computes its own version of an owned fact. | §2 has the two cross-app contradictions this rule would have prevented. | +| **Transport dates** | Keep dates as String in the JSON structure and validate them in the microflow; give the snippet a non-date sample. | A date-shaped sample infers DateTime, and a String mapping onto it fails mxbuild with CE5015 (`check` does not see it). | + +Secrets: the reader passwords are **constants with no value in the model**, set per machine +(`.mxcli/constants.json`, never committed) or per environment. An empty value, or a placeholder +such as `__SET_ME__`, closes the account. `learned-constants-and-secrets.md` has the channel rules. + +--- + +## 2. Building it + +### Publisher side + +1. **Authenticate inside the operation microflow.** A published REST service has no auth clause in + MDL, and an `Authentication:` property is silently ignored (BUG-152). Read the `Authorization` + header, compare against the constant, return 401 with `WWW-Authenticate` for none or wrong, 403 + for a known account on an API it may not read. Bonus: an inbound call-log row can then name the + caller on every outcome, including 401. +2. **`''` is not `empty` for a String query parameter.** An unset `?limit=` arrives as `''`. Test + `$p != empty and trim($p) != ''`, or every unset parameter is rejected (7 of 29 tests passed + before this fix, 29 of 29 after). +3. **Know which errors you never see.** A malformed parameter such as `cursor=%%%` (invalid + percent-encoding) is rejected by the Mendix runtime with **560** before your microflow runs. A + well-formed bad cursor reaches your code and gets your **422** `bad_cursor`. Put both in the + contract, so the consumer and the mock know the 560 exists. +4. **Every change that alters the payload must bump both `version` and `updatedAt`.** The evidence + shipped a producer with the notice version hardcoded to 1 and state hardcoded to `Issued`, and a + `since` filter on the change's date only. A withdrawal changed neither, so the consumer's + incremental read skipped it and advanced its cursor past it. Rule: list every attribute the + payload projects, and make the `since` XPath cover every timestamp that can change any of them. +5. **One script owns the service and names every resource** — `create or modify published rest + service` replaces the resource list (BUG-152). +6. **Write `contractVersion` and `generatedAt` into every body**, and make the field signs and units + match the contract example (the evidence had a drift value whose sign the database stored the + other way round; the API now writes `0 - DriftPct`). + +### Consumer side + +7. **Keep one endpoint row per upstream API**: `LastSuccessAt`, `LastError`, `LastErrorAt`, + `CredentialsRejected`, cursor, and the source's `generatedAt` of the last good read. +8. **A non-2xx answer raises an error in the runtime.** Handle it **without rollback** and read the + latest HTTP response, or the status is lost: the first handler in the evidence logged a 401 as + 0 "Not reachable" and rolled the endpoint row back. A 401 sets `CredentialsRejected` and stops + polling until someone changes the password; a 5xx or timeout keeps polling. +9. **`LastError = ''` is not `LastError = empty`.** After a good read the code cleared the error to + `''`; the banner's XPath `LastError != empty` matched the empty string, so every healthy feed said + "unreachable". Guard on non-empty string, and test both values (§3, rung 1). +10. **Show "as of" from the source's time, and a stale marker.** The line reads + "Supplier readiness as of 2026-10-08 17:17, not reachable" and keeps the last good rows. A first + read that fails says "never read", not "0". A feed polled every 5 minutes whose last success is + days old is stale: say so on the page. **(recommendation)** Stale threshold = 3 × the poll + interval, computed on the page, not stored. +11. **A status card shows the last real call, not a probe.** The evidence had a card saying + "Reachable · ok" from a probe three days old while every live call that day returned 400. Write + `LastSuccessAt` / `LastError` from the production call path, and show both. +12. **Ordering guard.** Ignore a record whose version is not higher than the stored one. A test + pins it with three reads of the same two rows: a higher version stores both, a lower one stores + none, a mixed read stores only the row that is newer. An old message can never + overwrite newer state, and a repeat read changes nothing. +13. **Prune only after a complete good read.** The consumer upserted what the publisher served but + never removed what it stopped serving, so a supplier stayed on the card after the publisher + dropped it. Delete rows not re-stamped by the latest read, and only when the read had no error + and no next page. A failed or partial read keeps the last known rows. +14. **A cursor can skip; give a full read a button.** After the producer bug in step 4, the + recovery was: scheduled reads stay incremental with overlap, an admin "Read now" does a full, + idempotent read. Keep one. +15. **A check done once at publish time needs a retry.** An applicability check ran only when the + bulletin was published; the other app was down then, and the card said "not reachable" for three + days until a "Re-check" action was added. +16. **One owner per fact; never invent the owner's fact on failure.** Two contradictions reached the + UI: + - a consumer's fallback seeded a record as "Approved, approved now" when its read failed, so the + same change was Approved in one app and Open in the owner; + - two apps counted "field cases for this change" each in their own way (5 in one, 6 in the + other: one counted app-wide cases). + A consumer that has no answer shows "not visible here" or "never read", not a guess. A count of + an owned fact is read from the owner's API, never recomputed. +17. **Size the cache columns from the real feed, not the example.** Part numbers up to 40 + characters arrived from the live publisher; the consumer's columns were 30. The mock never sent + one that long. + +--- + +## 3. Testing across apps: the ladder + +Climb from cheap to dear. A rung earns its cost only once the rung below is clean. + +| Rung | What it proves | How (evidence) | Cost measured | +|---|---|---|---| +| **1. Publisher contract tests** | Filters, limits, paging, 404/422, auth rule, version rule — against the publisher's own microflows | `mxcli test tests/api-.test.mdl`: change-notices 29 cases, service bulletins 17 | One module ~76 s; one file 9 min 21 s wall for 848 ms of tests (the injection into the `.mpr` is the cost) | +| **1b. Consumer logic tests** | Version rule, prune, "never read" vs "as of", `''` vs empty, feed-status line | Seed endpoint rows and stored copies in the test; call the SUB directly. Feed status: 4 cases (healthy, one failing, none ever read, failure before first success) | Same as rung 1 | +| **2. Consumer vs fixture server** | The consumer's HTTP path: auth, paging, every failure mode, idempotent repeat | One fixture per upstream under `tools/mocks/` (Python or Node, loopback only, fixture password). Modes switched by `GET /mock-admin/mode?mode=normal\|down\|partial\|invalid\|version\|cursor-cycle`; `/mock-admin/stats` counts requests. 6 modes × 3 feeds = 18 cases. Point the consumer's base-URL constant at the fixture for the run only, and restore the constants file on exit | Seconds per case; no publisher running | +| **3. Contract diff, mock vs live publisher** | That the mock still tells the truth | Same requests against the mock and the live publisher; diff keys, shapes and status codes. First run: **9 of 10** — the runtime's 560 on `cursor=%%%` (mock: 200) and an empty `limit=` (mock: 422, publisher: not given) | Minutes; needs the publisher running | +| **4. Live smoke per app** | Auth and routing on the real port | `curl -u` per API: no credentials, wrong password, each reader account. Matrix of 16 of 16 as designed (401/401/200/403 per API), plus `limit=1` then `nextCursor` on every paged API (4 of 4) | Seconds; check the body as well as the status | +| **5. Cross-app consistency** **(recommendation)** | The same key gives the same status and counts in every app | For a handful of keys, read the owner's API and each consumer's stored copy (OQL or its page), compare status, version and counts, print a table with the denominator. Nothing in the evidence did this; a UX scan found the two contradictions in §2.16 by eye | Seconds once all apps run | +| **6. Cross-app journeys** | The few flows that truly span apps: publish → poll → act → poll back; upstream down then back | Browser journeys per app, in order: "upstream unreachable after a good read" 26/26; the full withdrawal story across three apps | Minutes to hours; run rarely | + +### Cadence + +| When | Run | +|---|---| +| Per change to an API or a consumer | Rung 1 for the touched app (only the touched test file), rung 1b if consumer logic changed, rung 2 for the touched consumer | +| Per slice | Rungs 1–2 in full for the touched apps, rung 3 for every API whose publisher or mock changed, rung 4 for every touched API | +| Per release, or after a contract change | All rungs; rung 5 on a fixed list of keys; rung 6 only for the cross-app journeys | + +### Keeping it cheap + +- **One `mxcli test --local` at a time on the machine, across all apps.** It binds the fixed ports + 8081 and 8091, whichever project runs it. Runners check those ports and refuse when they are + taken; let them, and queue the next app's run behind the current one. Apps that must run tests in + parallel use `mxcli test --attach` against their own running app on their own ports. +- **The model-write guard is machine-wide too.** `exec.sh` refused because another project's raw + `mxcli exec` was running. Serialise model writes across apps. +- **Do not combine `run --test-endpoint` with `test --local` on the same model** (StackOverflowError + at startup), and never commit while an app started with `--test-endpoint` runs: it writes an + `MxTest` module and an after-startup setting into the model. +- **`@cleanup rollback` is the default.** A seed that later tests use must say `@cleanup none`, and a + cleanup test must remove it; delete by exact key, not by prefix (a prefix cleanup reached demo + data that shared the prefix). +- **Reset every fixture before the suite and name its port in the suite header.** A stale mock + process on the old port served two old records and failed a count; a test that reads a live mock + goes red after any run that changed the mock. +- **An absent upstream is SKIPPED, not passed.** The runner that targets the real publisher reports + SKIPPED when nothing listens on its port, and the wrapper must read **every** verdict, a clean + exit and zero skips — one wrapper read only the first verdict. +- **Two apps on one host share the session cookie.** Give each app its own hostname + (`app-b.localhost`) in browser journeys, or one login signs the other out. +- **Journey order can be load-bearing** when journeys reset a shared mock or need a state another + journey leaves. Write the order in the suite, or make each journey set up its own state. + +--- + +## 4. Definition of done for one API + +| Check | Evidence to show | +|---|---| +| Contract file in the publisher's repo, reviewed by the consumer | the file path | +| Rung 1 and 1b | `N/N` from the test output | +| Rung 2: every fixture mode | `N/N`, modes listed | +| Rung 3: contract diff | the diff file, `x of y` equal, each difference explained | +| Rung 4: status matrix | the curl table | +| Page shows "as of", stale and "never read" states | a screenshot of each, as a real user role | +| Fact ownership row added | the ownership table | + +--- + +## 5. Failure register + +"Caught by" is the rung that found it. "Would have" is the cheapest rung that could have. +Source: **F** = a FINDINGS line in one of the three apps; **D** = another project document (a proof +file or the UX scan of 2026-10-12). + +| # | Failure | Caught by | Cheapest rung that would have | Src | +|---|---|---|---|---| +| 1 | Unset String query parameter arrives as `''`, rejected as invalid (7 of 29 → 29 of 29) | 1 | 1 | F | +| 2 | Per-test rollback removed a seed the later tests needed | 1 | 1 | F | +| 3 | Published REST has no auth clause in MDL; an auth property is ignored (BUG-152) | 4 | 4 | F | +| 4 | Malformed cursor gets runtime 560 before the microflow; the mock answered 200 | 3 | 3 | F | +| 5 | Mock rejected empty `limit=`; the publisher treats it as not given | 3 | 3 | F | +| 6 | Consumer never pruned rows the publisher stopped serving | the publisher team's confirmation request (cross-app, by hand) | 1b | F | +| 7 | Demo reset left stored copies hanging off recreated records | 6 (journey) | 1b | F | +| 8 | Non-2xx handler lost the status (401 logged as 0) and rolled back the endpoint row | 2 | 2 | D | +| 9 | Cache columns 30 characters, live feed sends 40 | 2, run against the live publisher instead of the mock | 3 | D | +| 10 | `LastError = ''` matched `!= empty`; healthy feeds shown as unreachable | live UI review | 1b | F | +| 11 | Producer hardcoded version and state; `since` missed the withdrawal timestamp; consumer cursor skipped the withdrawal | 6 (fresh cross-app story) | 1 | F | +| 12 | Stale mock process on the old port served old records; false test failure | 2 | 2 (reset fixtures first) | F | +| 13 | Two consumer tests pass on the mock and fail on the live publisher | 3 (by accident: run against both) | 3 | F | +| 14 | Consumer fallback seeded "Approved, now" on a failed read; owner holds the change Open | cross-app process trail and UX scan | 5 | F | +| 15 | Same change: 5 field cases in one app, 6 in another | recorded video, UX scan | 5 | F | +| 16 | Status card "Reachable · ok" from a 3-day-old probe while live calls returned 400 | UX scan | 1b (card fed by the call path) | D | +| 17 | "Last success" days old on APIs polled every 5 minutes; no last-error shown (cause not investigated) | UX scan | 1b + a stale marker | D | +| 18 | Applicability checked once at publish while the other app was down; never retried | live page review | 2 (`down` mode at first call) | F | +| 19 | Field sign in the contract example opposite to what the database stores | building rung 1 | 1 | F | +| 20 | Rows with an empty `updatedAt` never match the cursor | building rung 1 | 1 | F | +| 21 | Date-shaped JSON sample inferred DateTime; String mapping failed mxbuild (CE5015) | exec gate (mxbuild) | exec gate | F | +| 22 | Two apps on one host shared the session cookie; one login signed the other out | 6 | 6 | F | +| 23 | `test --local` while the same model runs with `--test-endpoint`: StackOverflowError at startup | 1 | — (process rule) | F | +| 24 | Model-write guard is machine-wide; an exec refused while another app wrote | any exec | — (serialise) | F | +| 25 | Consumed-test wrapper read only the first verdict | audit of the wrapper | — (read every verdict) | F | +| 26 | Journey order load-bearing: a mock reset in one journey broke the next (4 false failures) | 6 | — (order in the suite) | F | + +What the register says: rungs 1–4 caught 12 of 26; a journey (rung 6) caught 4; a human looking +(page review, UX scan, recorded video, another team's question) caught 7; the exec gate or an audit +of a runner caught 3. Of the 11 found by a journey or a human, 9 had a cheaper rung that would have +caught them, and two of those — the cross-app contradictions, 14 and 15 — needed the one rung +nobody ran: rung 5. + +## Related + +- `rest-integration-first-time-right.md` — the mapping and page mechanics for one consumer +- `learned-constants-and-secrets.md` — where reader passwords and base URLs live +- `testing-shape.md` — false-green register; `@cleanup` and test-runtime ports +- `tool-output-is-not-ground-truth.md` — a smoke test checks the body, not the status code +- `bug-logs/mxcli-bugs.md` BUG-152 — published REST auth, resource list, mapping drops diff --git a/skills/rest-integration-first-time-right.md b/skills/rest-integration-first-time-right.md index 8a27ae75..8985def6 100644 --- a/skills/rest-integration-first-time-right.md +++ b/skills/rest-integration-first-time-right.md @@ -308,6 +308,8 @@ association direction; all fine, page empty → stale deployment or a different ## Related +- `rest-between-mendix-apps.md` — when both ends are your own Mendix apps: the contract, consumer + honesty (stale, "as of", one owner per fact) and the cross-app test ladder - `learned-mdl-preflight.md` — write-mode choice and the STOP table - `learned-page-patterns.md` — DataGrid-2 filter placement for database-backed grids - `tool-output-is-not-ground-truth.md` — why the runtime test, not `DESCRIBE`, is the oracle From e0ecd0b3d49017a3c439ef9439da41c863b41975 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 07:52:58 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(rest-between-mendix-apps):=20review=20f?= =?UTF-8?q?ixes=20=E2=80=94=20BUG-157,=20--attach=20is=20a=20write,=20auth?= =?UTF-8?q?=20pinned=20to=20v0.24,=20generic=20domain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CHANGELOG line added. - StackOverflowError rule now cites BUG-157 (a signal-stopped test run leaves after-startup on the injected MxTest flow); dropped the advice to run `test --attach` in parallel, since --attach is a model write. - Auth claim pinned to mxcli v0.24 (BUG-152); notes Studio Pro can set the service's own auth and that microflow-only auth leaves the endpoint anonymous. - Related: testing-shape.md citation no longer claims @cleanup content. - Evidence claim narrowed; §1 rationales without a register row are marked (design choice); 401-stop/5xx-continue marked (recommendation); rung-1 cost wording clarified as two measured runs. - Routing row front-loads "App-to-app REST or microservices"; re-rendered. - §5 register moved to process/rest-between-mendix-apps-register-2026-10.md, a short summary stays in the skill (skill rule 6). - Domain wording generalised to order/stock/customer examples. Skill words 3593 -> 3090 (on-demand); baseline 75038 unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + README.md | 2 +- ROUTING.md | 2 +- agents/architect-agent.md | 2 +- agents/mdl-agent.md | 2 +- agents/review-agent.md | 2 +- agents/test-agent.md | 2 +- bin/lib/skill-routing.tsv | 2 +- ...st-between-mendix-apps-register-2026-10.md | 49 ++++++ skills/rest-between-mendix-apps.md | 139 ++++++++---------- 10 files changed, 115 insertions(+), 88 deletions(-) create mode 100644 process/rest-between-mendix-apps-register-2026-10.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 59bca1b3..89ff45b7 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 +- learn(rest-between-mendix-apps): **new on-demand skill for two or more Mendix apps you build that read each other over REST (app-to-app, microservices).** The contract decisions to write down before the first script (pull vs push, versioned path, key + version rule, cursor paging, `since` with overlap, error shape incl. the runtime's 560, auth in the operation microflow as of mxcli v0.24 per BUG-152, fixed ports, one owner per fact), 17 publisher and consumer rules for honest "as of"/stale/"never read" states, and a six-rung cross-app test ladder with a per-change/slice/release cadence; a signal-stopped test run is BUG-157. The 26-row failure register behind it is `process/rest-between-mendix-apps-register-2026-10.md`. Routed ondemand for architect, mdl, test and review at stages 3–6. Field evidence: three demo apps, mxcli v0.24–v0.26, Mendix 11.15, 2026-10-07 to 2026-10-12, domain names generalised — MendixMau - process(docs): **1.0 release notes, a versioning rule, and a README for adopters.** New `docs/releases/v1.0.md`: the first named milestone, covering 2026-09-30 to 2026-10-14 by theme (independent review before merge, baseline words 79,997 → 76,259, exec.sh lock and restart verdicts, lint batches 1–4 with SEC014 and the stock-revert guard, journey/e2e fixes, the Stage 3 interaction model, delivery rounds), with upgrade steps, known gaps and credits. `docs/RELEASE-PROCESS.md` §8 is the single source for versioning: a milestone (1.0, 1.1, 2.0) is a train release the maintainer also names, with curated notes, a GitHub Release and a CHANGELOG pointer; major = a consuming project breaks without action (needs a migration section and a sync path), minor = no action needed; deprecations are announced in one milestone and removed no earlier than the next major; the `vYYYY.MM.DD` train and `bin/cut-release.sh` are unchanged; plus how a consumer checks, upgrades and pins a version. The header above now defers to it. README rewritten for newcomers: what it is, how it works, ways to use it, quick start, Make it yours (company brain, CLAUDE.local.md, lint opt-outs, waivers and --adopt, interview mode, exec approval and permission profiles, agent stubs, other harnesses), testing, a call for input, releases. The earlier material sits under Reference with the generated routing blocks untouched. Corrected: the Windows `pgrep` hazard (replaced by the per-project exec lock in #304), the hand-edit steps in "How to add a new skill" (tables are generated), and the stale Stage 6 and `examples/` notes. — MendixMau - fix(interview-mode): **`attended` and `unattended` are now read as aliases for `steering` and `auto` — a project that opted into unattended as documented was silently run as steering.** CLAUDE.md, the runbook and intake Q9 taught `Interview mode: unattended`, but `bin/interview-mode.sh` knew only `steering | assist | auto` and fell back to steering. The aliases are accepted in every source (env, `.interview-mode`, `PROJECT.md`, `--set`) under the same parse rules, with one stderr note. The `PROJECT.md` value is read after stripping bold/backticks: a bare mode or alias (`**auto**`, `` `auto` ``, `auto.`) resolves, a note may follow `steering`, but `assist`/`auto` with any trailing text falls back to steering with a "must stand alone on the line" message — so an annotated or hedged line never resolves quieter than before (two hedge word lists were tried in review and both leaked); env, session file and `--set` stay exact. Output stays canonical so `interview-gate.sh` is unchanged, and any other value still falls back to steering. Docs now name the canonical values (vocabulary in `interview-protocol.md`); intake Q9's body offers the three modes with steering the default (heading kept: it is sync's locator), and `sync-project.sh` reads an earlier unanswered Q9 body as unanswered rather than "edited". `test-interview-mode.sh` +57 checks (19 → 76) — MendixMau (found in the 1.0 README review) - fix(dispatching, sync-project, obligation-check): **review follow-ups for the 2026-10-14 integration batch.** `dispatching.md` names app-restart exits 4 and 5 beside 3 and 1. `test-test-scratch.sh` drops `ls | grep -q` under pipefail for here-strings; sync step 4d no longer aborts on empty doctor output; `_ob_has_denominator` strips NUL bytes so bash stops warning. — integration 2026-10-14 reviews diff --git a/README.md b/README.md index 486ad697..27a35909 100644 --- a/README.md +++ b/README.md @@ -650,7 +650,7 @@ Every mxcli project has a `.ai-context/skills/` directory (bundled by `mxcli ini | Task | Skill to load | |---|---| | Building a REST integration (consumed or published) for the first time on a project — the checks that avoid a rebuild after the first live call | `skills/rest-integration-first-time-right.md` | -| Two or more Mendix apps you build that read each other over REST — before the contract, before the first consumer, and before calling an API tested: pull vs push, key+version rule, auth in the operation microflow (BUG-152), honest last-success/stale/"as of", one owner per fact, and the six-rung cross-app test ladder with its per-change/slice/release cadence | `skills/rest-between-mendix-apps.md` | +| App-to-app REST or microservices: two or more Mendix apps you build that read each other over REST — before the contract, before the first consumer, and before calling an API tested: pull vs push, key+version rule, auth in the operation microflow (BUG-152), honest last-success/stale/"as of", one owner per fact, and the six-rung cross-app test ladder with its per-change/slice/release cadence | `skills/rest-between-mendix-apps.md` | | Adding a constant, installing a marketplace module, deploying to a new environment, or needing a Mendix PAT — where an encryption key, API credential, endpoint or access token gets its value, why a free node has only one channel, and why a token missing from the files you grepped is not missing | `skills/learned-constants-and-secrets.md` | **Verify — does it work** diff --git a/ROUTING.md b/ROUTING.md index 4bd291d3..5848b573 100644 --- a/ROUTING.md +++ b/ROUTING.md @@ -193,7 +193,7 @@ picks the row up. That is the whole procedure — there is no second list to rem | Always relevant for | Load this | Agent(s) | Stage(s) | Tier | |---|---|---|---|---| | Building a REST integration (consumed or published) for the first time on a project — the checks that avoid a rebuild after the first live call | `skills/rest-integration-first-time-right.md` | mdl | 4,5 | ondemand | -| Two or more Mendix apps you build that read each other over REST — before the contract, before the first consumer, and before calling an API tested: pull vs push, key+version rule, auth in the operation microflow (BUG-152), honest last-success/stale/"as of", one owner per fact, and the six-rung cross-app test ladder with its per-change/slice/release cadence | `skills/rest-between-mendix-apps.md` | architect,mdl,test,review | 3,4,5,6 | ondemand | +| App-to-app REST or microservices: two or more Mendix apps you build that read each other over REST — before the contract, before the first consumer, and before calling an API tested: pull vs push, key+version rule, auth in the operation microflow (BUG-152), honest last-success/stale/"as of", one owner per fact, and the six-rung cross-app test ladder with its per-change/slice/release cadence | `skills/rest-between-mendix-apps.md` | architect,mdl,test,review | 3,4,5,6 | ondemand | | Adding a constant, installing a marketplace module, deploying to a new environment, or needing a Mendix PAT — where an encryption key, API credential, endpoint or access token gets its value, why a free node has only one channel, and why a token missing from the files you grepped is not missing | `skills/learned-constants-and-secrets.md` | mdl,architect,gate | 4,5,6 | ondemand | #### Build · Security diff --git a/agents/architect-agent.md b/agents/architect-agent.md index f79d787d..e8a32d92 100644 --- a/agents/architect-agent.md +++ b/agents/architect-agent.md @@ -77,7 +77,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/agent-permission-friction.md` | Any refused, denied or blocked command | | `project-bin/build-plan-status.sh` | After marking a module done, or any time "how much is built vs proven" is asked | | `skills/workflow-structure-rules.md` | Designing or reviewing a Workflow's SHAPE before or after the MDL | -| `skills/rest-between-mendix-apps.md` | Two or more Mendix apps you build that read each other over REST | +| `skills/rest-between-mendix-apps.md` | App-to-app REST or microservices: two or more Mendix apps you build that read each other over REST | | `skills/mendix-epics-api.md` | Working with the Mendix Epics board programmatically | | `skills/upstream-feedback.md` | About to open an issue, PR or discussion against mxcli or the toolkit | | `skills/doctor-triage.md` | doctor.sh reports FAIL or WARN, or someone asks whether a red setup line blocks them | diff --git a/agents/mdl-agent.md b/agents/mdl-agent.md index 103febfe..c51eb0f6 100644 --- a/agents/mdl-agent.md +++ b/agents/mdl-agent.md @@ -91,7 +91,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/learned-workflow-patterns.md` | Writing or debugging a Mendix native Workflow (CREATE WORKFLOW/USER TASK/OUTCOMES) | | `skills/workflow-structure-rules.md` | Designing or reviewing a Workflow's SHAPE before or after the MDL | | `skills/rest-integration-first-time-right.md` | Building a REST integration (consumed or published) for the first time on a project | -| `skills/rest-between-mendix-apps.md` | Two or more Mendix apps you build that read each other over REST | +| `skills/rest-between-mendix-apps.md` | App-to-app REST or microservices: two or more Mendix apps you build that read each other over REST | | `skills/bug-submission-checklist.md` | Preparing an mxcli/Studio Pro bug for submission | | `skills/upstream-feedback.md` | About to open an issue, PR or discussion against mxcli or the toolkit | | `skills/empty-widget-triage.md` | A page/grid/combobox renders empty (blank cells, zero rows, zero options) during UI review or an e2e run | diff --git a/agents/review-agent.md b/agents/review-agent.md index 6de22610..cdf56c9b 100644 --- a/agents/review-agent.md +++ b/agents/review-agent.md @@ -90,7 +90,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/journey-examples.md` | Writing an actual .journey.json | | `skills/wiring-sweep.md` | Every module before it is called done | | `skills/workflow-structure-rules.md` | Designing or reviewing a Workflow's SHAPE before or after the MDL | -| `skills/rest-between-mendix-apps.md` | Two or more Mendix apps you build that read each other over REST | +| `skills/rest-between-mendix-apps.md` | App-to-app REST or microservices: two or more Mendix apps you build that read each other over REST | | `skills/bug-submission-checklist.md` | Preparing an mxcli/Studio Pro bug for submission | | `skills/upstream-feedback.md` | About to open an issue, PR or discussion against mxcli or the toolkit | | `skills/empty-widget-triage.md` | A page/grid/combobox renders empty (blank cells, zero rows, zero options) during UI review or an e2e run | diff --git a/agents/test-agent.md b/agents/test-agent.md index 659329e2..cecf6270 100644 --- a/agents/test-agent.md +++ b/agents/test-agent.md @@ -65,7 +65,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/improvement-register.md` | Any review pass that runs more than once | | `skills/journey-examples.md` | Writing an actual .journey.json | | `skills/wiring-sweep.md` | Every module before it is called done | -| `skills/rest-between-mendix-apps.md` | Two or more Mendix apps you build that read each other over REST | +| `skills/rest-between-mendix-apps.md` | App-to-app REST or microservices: two or more Mendix apps you build that read each other over REST | | `skills/upstream-feedback.md` | About to open an issue, PR or discussion against mxcli or the toolkit | | `skills/empty-widget-triage.md` | A page/grid/combobox renders empty (blank cells, zero rows, zero options) during UI review or an e2e run | | `skills/doctor-triage.md` | doctor.sh reports FAIL or WARN, or someone asks whether a red setup line blocks them | diff --git a/bin/lib/skill-routing.tsv b/bin/lib/skill-routing.tsv index b33c42a6..bb9d4d09 100644 --- a/bin/lib/skill-routing.tsv +++ b/bin/lib/skill-routing.tsv @@ -191,7 +191,7 @@ wiring-sweep skills/wiring-sweep.md Every module before it is called done — do learned-workflow-patterns skills/learned-workflow-patterns.md Writing or debugging a Mendix native Workflow (CREATE WORKFLOW/USER TASK/OUTCOMES) — syntax, the 11 workflow microflow statements, DECISION vs CALL MICROFLOW, and the two corruption classes (binary-version $Type, and create-before-reference) mdl 5 ondemand build/workflow workflow-structure-rules skills/workflow-structure-rules.md Designing or reviewing a Workflow's SHAPE before or after the MDL — where a path may end, boundary event vs event sub-process, parallel-split limits, outcome minimums, targeting from the sentence, multi-user decision methods, which edits break running instances; and any CE6689/CE1844/CE1845/MW0012 after a clean mxcli check architect,mdl,review 3,4,5,6 ondemand build/workflow rest-integration-first-time-right skills/rest-integration-first-time-right.md Building a REST integration (consumed or published) for the first time on a project — the checks that avoid a rebuild after the first live call mdl 4,5 ondemand build/integration -rest-between-mendix-apps skills/rest-between-mendix-apps.md Two or more Mendix apps you build that read each other over REST — before the contract, before the first consumer, and before calling an API tested: pull vs push, key+version rule, auth in the operation microflow (BUG-152), honest last-success/stale/"as of", one owner per fact, and the six-rung cross-app test ladder with its per-change/slice/release cadence architect,mdl,test,review 3,4,5,6 ondemand build/integration +rest-between-mendix-apps skills/rest-between-mendix-apps.md App-to-app REST or microservices: two or more Mendix apps you build that read each other over REST — before the contract, before the first consumer, and before calling an API tested: pull vs push, key+version rule, auth in the operation microflow (BUG-152), honest last-success/stale/"as of", one owner per fact, and the six-rung cross-app test ladder with its per-change/slice/release cadence architect,mdl,test,review 3,4,5,6 ondemand build/integration mendix-epics-api skills/mendix-epics-api.md Working with the Mendix Epics board programmatically — creating/reading stories and epics, updating workflow state, or integrating BRDs with the portal ba,architect P,0 ondemand reference bug-submission-checklist skills/bug-submission-checklist.md Preparing an mxcli/Studio Pro bug for submission — scope pinning, read-back-vs-write-path verification, gate-sensitivity negative controls, severity scoping, before it's called filable mdl,gate,review 5,6 ondemand diagnose upstream-feedback skills/upstream-feedback.md About to open an issue, PR or discussion against mxcli or the toolkit — before drafting, choosing which repo and vehicle it belongs to all - ondemand diagnose diff --git a/process/rest-between-mendix-apps-register-2026-10.md b/process/rest-between-mendix-apps-register-2026-10.md new file mode 100644 index 00000000..e6f794e7 --- /dev/null +++ b/process/rest-between-mendix-apps-register-2026-10.md @@ -0,0 +1,49 @@ +# REST between Mendix apps: failure register, 2026-10 + +The evidence behind `skills/rest-between-mendix-apps.md`. Three demo apps built with mxcli +v0.24 to v0.26 on Mendix 11.15, 2026-10-07 to 2026-10-12: an **order app**, a **stock app** and a +**customer app**, with nine published APIs and five consumer directions. Domain names in this file +and in the skill are generic placeholders, not the apps' real domain. + +"Caught by" is the rung of the skill's test ladder (§3) that found the failure. "Would have" is the +cheapest rung that could have. Source: **F** = a FINDINGS line in one of the three apps; **D** = +another project document (a proof file or the UX scan of 2026-10-12). + +| # | Failure | Caught by | Cheapest rung that would have | Src | +|---|---|---|---|---| +| 1 | Unset String query parameter arrives as `''`, rejected as invalid (7 of 29 → 29 of 29) | 1 | 1 | F | +| 2 | Per-test rollback removed a seed the later tests needed | 1 | 1 | F | +| 3 | Published REST has no auth clause in MDL; an auth property is ignored (BUG-152) | 4 | 4 | F | +| 4 | Malformed cursor gets runtime 560 before the microflow; the mock answered 200 | 3 | 3 | F | +| 5 | Mock rejected empty `limit=`; the publisher treats it as not given | 3 | 3 | F | +| 6 | Consumer never pruned rows the publisher stopped serving | the publisher team's confirmation request (cross-app, by hand) | 1b | F | +| 7 | Demo reset left stored copies hanging off recreated records | 6 (journey) | 1b | F | +| 8 | Non-2xx handler lost the status (401 logged as 0) and rolled back the endpoint row | 2 | 2 | D | +| 9 | Cache columns 30 characters, live feed sends 40 | 2, run against the live publisher instead of the mock | 3 | D | +| 10 | `LastError = ''` matched `!= empty`; healthy feeds shown as unreachable | live UI review | 1b | F | +| 11 | Producer hardcoded version and state; `since` missed the cancellation timestamp; consumer cursor skipped the cancellation | 6 (fresh cross-app story) | 1 | F | +| 12 | Stale mock process on the old port served old records; false test failure | 2 | 2 (reset fixtures first) | F | +| 13 | Two consumer tests pass on the mock and fail on the live publisher | 3 (by accident: run against both) | 3 | F | +| 14 | Consumer fallback seeded "Approved, now" on a failed read; owner holds the order Open | cross-app process trail and UX scan | 5 | F | +| 15 | Same order: 5 support cases in one app, 6 in another | recorded video, UX scan | 5 | F | +| 16 | Status card "Reachable · ok" from a 3-day-old probe while live calls returned 400 | UX scan | 1b (card fed by the call path) | D | +| 17 | "Last success" days old on APIs polled every 5 minutes; no last-error shown (cause not investigated) | UX scan | 1b + a stale marker | D | +| 18 | Eligibility checked once at publish while the other app was down; never retried | live page review | 2 (`down` mode at first call) | F | +| 19 | Field sign in the contract example opposite to what the database stores | building rung 1 | 1 | F | +| 20 | Rows with an empty `updatedAt` never match the cursor | building rung 1 | 1 | F | +| 21 | Date-shaped JSON sample inferred DateTime; String mapping failed mxbuild (CE5015) | exec gate (mxbuild) | exec gate | F | +| 22 | Two apps on one host shared the session cookie; one login signed the other out | 6 | 6 | F | +| 23 | StackOverflowError at startup: a test run stopped by a signal left after-startup on the injected `MxTest` flow, and the next injection chained it to itself (BUG-157) | 1 | — (process rule) | F | +| 24 | Model-write guard is machine-wide; an exec refused while another app wrote | any exec | — (serialise) | F | +| 25 | Consumed-test wrapper read only the first verdict | audit of the wrapper | — (read every verdict) | F | +| 26 | Journey order load-bearing: a mock reset in one journey broke the next (4 false failures) | 6 | — (order in the suite) | F | + +**What the register says.** Rungs 1–4 caught 12 of 26; a journey (rung 6) caught 4; a human +looking (page review, UX scan, recorded video, another team's question) caught 7; the exec gate +or an audit of a runner caught 3. Of the 11 found by a journey or a human, 9 had a cheaper rung +that would have caught them, and two of those — the cross-app contradictions, 14 and 15 — needed +the one rung nobody ran: rung 5. + +Row 23 was first recorded as "`test --local` while the same model runs with `--test-endpoint`". +The 2026-10 review matched it to BUG-157, whose mechanism (a signal-stopped run skips the restore) +is the one stated above. diff --git a/skills/rest-between-mendix-apps.md b/skills/rest-between-mendix-apps.md index 8debe210..37eee988 100644 --- a/skills/rest-between-mendix-apps.md +++ b/skills/rest-between-mendix-apps.md @@ -7,12 +7,14 @@ REST ("microservices between Mendix apps"). One app publishes what it owns; anot honest about stale data, and how to test across apps without paying for a full multi-app run on every change. -**Evidence:** three demo apps built with mxcli v0.24 to v0.26 on Mendix 11.15, October 2026: a -**change app** (publishes `rest/change-notices/v1` and `rest/service-bulletins/v1`), a **supply app** -(polls the change app, publishes `rest/change-readiness/v1` and two supply feeds) and a **service app** -(publishes four fleet feeds and polls both others). Nine published APIs, five consumer directions. -Every rule below that is not marked **(recommendation)** comes from a failure or proof recorded in -one of the three apps' FINDINGS files or proof documents. §5 lists them. +**Evidence:** three demo apps built with mxcli v0.24 to v0.26 on Mendix 11.15, October 2026: an +**order app** (publishes `rest/orders/v1` and `rest/announcements/v1`), a **stock app** (polls the +order app, publishes `rest/order-readiness/v1` and two stock feeds) and a **customer app** (publishes +four customer feeds and polls both others). Nine published APIs, five consumer directions. Domain +names are generic placeholders. Every numbered rule in §2 and every result in §3 that is not marked +**(recommendation)** comes from a failure or proof recorded in the apps' FINDINGS files or proof +documents; the 26-row register is `process/rest-between-mendix-apps-register-2026-10.md`. In §1, +a "Why" marked **(design choice)** is reasoning the apps adopted, not a recorded failure. **Not this file:** @@ -37,14 +39,14 @@ Decide these, and write each down: | Decision | What the evidence did | Why | |---|---|---| -| **Pull or push** | Pull only, in every direction. Each app publishes what it owns; the other polls. | No outbox, no retry queue, no inbound write endpoint. An app that is down is simply not polled, and nothing is lost. Push is a valid choice, but it needs all three. | +| **Pull or push** | Pull only, in every direction. Each app publishes what it owns; the other polls. | No outbox, no retry queue, no inbound write endpoint. An app that is down is simply not polled, and nothing is lost. Push is a valid choice, but it needs all three: an outbox, a retry queue and an inbound write endpoint. **(design choice)** | | **Versioned path** | `rest//v1`. Additive changes only inside a version; a breaking change is `/v2` beside `/v1`. Every body carries `contractVersion`. | The consumer can test against a `version` mode of its fixture (§3) and refuse a body it does not understand. | -| **Record key and version** | One key per record (for example `CHG-0001/PART-B/SUP-A`) and an integer version, +1 on every content change. Receivers upsert on the key and ignore a version not higher than the one stored. | Replays, overlapping polls and a restore from backup all land on the same state. | -| **Paging** | Opaque cursor: `nextCursor` from the previous page, `null` on the last page. Cursor = base64 of `updatedAt|key`; order by `updatedAt`, then key. | Stable under inserts. A row with an empty `updatedAt` never matches a cursor, so the seed must set it on every row. | +| **Record key and version** | One key per record (for example `ORD-0001/ITEM-B/WH-A`) and an integer version, +1 on every content change. Receivers upsert on the key and ignore a version not higher than the one stored. | Replays, overlapping polls and a restore from backup all land on the same state. | +| **Paging** | Opaque cursor: `nextCursor` from the previous page, `null` on the last page. Cursor = base64 of `updatedAt|key`; order by `updatedAt`, then key. | Stable under inserts **(design choice)**. Recorded: a row with an empty `updatedAt` never matches a cursor, so the seed must set it on every row. | | **Incremental read** | `since=`; the consumer sends its last fully processed `updatedAt` minus an overlap (2 minutes in the evidence). | The overlap is safe only because of the version rule above. | -| **Correlation** | The caller sends `X-Correlation-Id`; the publisher echoes it in the response header and in every error body, and both sides log it. | One id to grep in two runtime logs. | -| **Error shape** | `{ "code": "bad_limit", "message": "...", "correlationId": "..." }`. 401 bad or missing credentials (with `WWW-Authenticate`), 403 known account not allowed on this API, 404 unknown key, 422 bad parameter. | See §2 for the 560 the runtime sends before your microflow runs. | -| **Auth** | One service account per direction, named `-reader`, HTTP Basic; HTTPS outside dev. | See §2: in MDL the check lives in the operation microflow. | +| **Correlation** | The caller sends `X-Correlation-Id`; the publisher echoes it in the response header and in every error body, and both sides log it. | One id to grep in two runtime logs. **(design choice)** | +| **Error shape** | `{ "code": "bad_limit", "message": "...", "correlationId": "..." }`. 401 bad or missing credentials (with `WWW-Authenticate`), 403 known account not allowed on this API, 404 unknown key, 422 bad parameter. | One shape for every consumer to parse **(design choice)**. Recorded: the runtime's 560 arrives before your microflow runs (§2, step 3). | +| **Auth** | One service account per direction, named `-reader`, HTTP Basic; HTTPS outside dev. | As of mxcli v0.24 (BUG-152), MDL cannot set the service's own authentication, so the check lives in the operation microflow (§2, step 1). | | **Ports** | A fixed port per app, written in the plan (in the evidence 8085, 8086, 8087). Never 8081/8091: `mxcli test --local` binds those. | A wrong app answering on a shared port returns 200 with the wrong body. Smoke tests check the body, not only the status. | | **Who owns each fact** | A table: fact → owning app. The consumer stores a *copy* with an "as of" time and never computes its own version of an owned fact. | §2 has the two cross-app contradictions this rule would have prevented. | | **Transport dates** | Keep dates as String in the JSON structure and validate them in the microflow; give the snippet a non-date sample. | A date-shaped sample infers DateTime, and a String mapping onto it fails mxbuild with CE5015 (`check` does not see it). | @@ -59,11 +61,15 @@ such as `__SET_ME__`, closes the account. `learned-constants-and-secrets.md` has ### Publisher side -1. **Authenticate inside the operation microflow.** A published REST service has no auth clause in - MDL, and an `Authentication:` property is silently ignored (BUG-152). Read the `Authorization` - header, compare against the constant, return 401 with `WWW-Authenticate` for none or wrong, 403 - for a known account on an API it may not read. Bonus: an inbound call-log row can then name the - caller on every outcome, including 401. +1. **Authenticate inside the operation microflow.** As of mxcli v0.24, `create published rest + service` has no auth clause, and an `Authentication:` property is silently ignored (BUG-152); + re-probe on a newer binary. Studio Pro can set the published service's own authentication. Done + only in the microflow, the endpoint itself is anonymous at runtime: every request reaches the + microflow with no user, so every operation microflow must run the check, and nothing else stands + in front of it. Read the `Authorization` header, compare against the constant, return 401 with + `WWW-Authenticate` for none or wrong, 403 for a known account on an API it may not read. Then + prove it at runtime (rung 4). Bonus: an inbound call-log row can then name the caller on every + outcome, including 401. 2. **`''` is not `empty` for a String query parameter.** An unset `?limit=` arrives as `''`. Test `$p != empty and trim($p) != ''`, or every unset parameter is rejected (7 of 29 tests passed before this fix, 29 of 29 after). @@ -72,15 +78,15 @@ such as `__SET_ME__`, closes the account. `learned-constants-and-secrets.md` has well-formed bad cursor reaches your code and gets your **422** `bad_cursor`. Put both in the contract, so the consumer and the mock know the 560 exists. 4. **Every change that alters the payload must bump both `version` and `updatedAt`.** The evidence - shipped a producer with the notice version hardcoded to 1 and state hardcoded to `Issued`, and a - `since` filter on the change's date only. A withdrawal changed neither, so the consumer's + shipped a producer with the order version hardcoded to 1 and state hardcoded to `Open`, and a + `since` filter on the order's date only. A cancellation changed neither, so the consumer's incremental read skipped it and advanced its cursor past it. Rule: list every attribute the payload projects, and make the `since` XPath cover every timestamp that can change any of them. 5. **One script owns the service and names every resource** — `create or modify published rest service` replaces the resource list (BUG-152). 6. **Write `contractVersion` and `generatedAt` into every body**, and make the field signs and units - match the contract example (the evidence had a drift value whose sign the database stored the - other way round; the API now writes `0 - DriftPct`). + match the contract example (the evidence had a variance value whose sign the database stored the + other way round; the API now writes `0 - VariancePct`). ### Consumer side @@ -88,13 +94,14 @@ such as `__SET_ME__`, closes the account. `learned-constants-and-secrets.md` has `CredentialsRejected`, cursor, and the source's `generatedAt` of the last good read. 8. **A non-2xx answer raises an error in the runtime.** Handle it **without rollback** and read the latest HTTP response, or the status is lost: the first handler in the evidence logged a 401 as - 0 "Not reachable" and rolled the endpoint row back. A 401 sets `CredentialsRejected` and stops - polling until someone changes the password; a 5xx or timeout keeps polling. + 0 "Not reachable" and rolled the endpoint row back. **(recommendation)** A 401 sets + `CredentialsRejected` and stops polling until someone changes the password; a 5xx or timeout + keeps polling. 9. **`LastError = ''` is not `LastError = empty`.** After a good read the code cleared the error to `''`; the banner's XPath `LastError != empty` matched the empty string, so every healthy feed said "unreachable". Guard on non-empty string, and test both values (§3, rung 1). 10. **Show "as of" from the source's time, and a stale marker.** The line reads - "Supplier readiness as of 2026-10-08 17:17, not reachable" and keeps the last good rows. A first + "Stock readiness as of 2026-10-08 17:17, not reachable" and keeps the last good rows. A first read that fails says "never read", not "0". A feed polled every 5 minutes whose last success is days old is stale: say so on the page. **(recommendation)** Stale threshold = 3 × the poll interval, computed on the page, not stored. @@ -106,24 +113,24 @@ such as `__SET_ME__`, closes the account. `learned-constants-and-secrets.md` has none, a mixed read stores only the row that is newer. An old message can never overwrite newer state, and a repeat read changes nothing. 13. **Prune only after a complete good read.** The consumer upserted what the publisher served but - never removed what it stopped serving, so a supplier stayed on the card after the publisher + never removed what it stopped serving, so a warehouse stayed on the card after the publisher dropped it. Delete rows not re-stamped by the latest read, and only when the read had no error and no next page. A failed or partial read keeps the last known rows. 14. **A cursor can skip; give a full read a button.** After the producer bug in step 4, the recovery was: scheduled reads stay incremental with overlap, an admin "Read now" does a full, idempotent read. Keep one. -15. **A check done once at publish time needs a retry.** An applicability check ran only when the - bulletin was published; the other app was down then, and the card said "not reachable" for three +15. **A check done once at publish time needs a retry.** An eligibility check ran only when the + announcement was published; the other app was down then, and the card said "not reachable" for three days until a "Re-check" action was added. 16. **One owner per fact; never invent the owner's fact on failure.** Two contradictions reached the UI: - a consumer's fallback seeded a record as "Approved, approved now" when its read failed, so the - same change was Approved in one app and Open in the owner; - - two apps counted "field cases for this change" each in their own way (5 in one, 6 in the + same order was Approved in one app and Open in the owner; + - two apps counted "support cases for this order" each in their own way (5 in one, 6 in the other: one counted app-wide cases). A consumer that has no answer shows "not visible here" or "never read", not a guess. A count of an owned fact is read from the owner's API, never recomputed. -17. **Size the cache columns from the real feed, not the example.** Part numbers up to 40 +17. **Size the cache columns from the real feed, not the example.** Item codes up to 40 characters arrived from the live publisher; the consumer's columns were 30. The mock never sent one that long. @@ -135,13 +142,13 @@ Climb from cheap to dear. A rung earns its cost only once the rung below is clea | Rung | What it proves | How (evidence) | Cost measured | |---|---|---|---| -| **1. Publisher contract tests** | Filters, limits, paging, 404/422, auth rule, version rule — against the publisher's own microflows | `mxcli test tests/api-.test.mdl`: change-notices 29 cases, service bulletins 17 | One module ~76 s; one file 9 min 21 s wall for 848 ms of tests (the injection into the `.mpr` is the cost) | +| **1. Publisher contract tests** | Filters, limits, paging, 404/422, auth rule, version rule — against the publisher's own microflows | `mxcli test tests/api-.test.mdl`: orders 29 cases, announcements 17 | Two measured runs: ~76 s wall for one module's suite, and 9 min 21 s wall for one file whose tests took 848 ms. Wall time is the injection into the `.mpr` and the build, not the tests | | **1b. Consumer logic tests** | Version rule, prune, "never read" vs "as of", `''` vs empty, feed-status line | Seed endpoint rows and stored copies in the test; call the SUB directly. Feed status: 4 cases (healthy, one failing, none ever read, failure before first success) | Same as rung 1 | | **2. Consumer vs fixture server** | The consumer's HTTP path: auth, paging, every failure mode, idempotent repeat | One fixture per upstream under `tools/mocks/` (Python or Node, loopback only, fixture password). Modes switched by `GET /mock-admin/mode?mode=normal\|down\|partial\|invalid\|version\|cursor-cycle`; `/mock-admin/stats` counts requests. 6 modes × 3 feeds = 18 cases. Point the consumer's base-URL constant at the fixture for the run only, and restore the constants file on exit | Seconds per case; no publisher running | | **3. Contract diff, mock vs live publisher** | That the mock still tells the truth | Same requests against the mock and the live publisher; diff keys, shapes and status codes. First run: **9 of 10** — the runtime's 560 on `cursor=%%%` (mock: 200) and an empty `limit=` (mock: 422, publisher: not given) | Minutes; needs the publisher running | | **4. Live smoke per app** | Auth and routing on the real port | `curl -u` per API: no credentials, wrong password, each reader account. Matrix of 16 of 16 as designed (401/401/200/403 per API), plus `limit=1` then `nextCursor` on every paged API (4 of 4) | Seconds; check the body as well as the status | -| **5. Cross-app consistency** **(recommendation)** | The same key gives the same status and counts in every app | For a handful of keys, read the owner's API and each consumer's stored copy (OQL or its page), compare status, version and counts, print a table with the denominator. Nothing in the evidence did this; a UX scan found the two contradictions in §2.16 by eye | Seconds once all apps run | -| **6. Cross-app journeys** | The few flows that truly span apps: publish → poll → act → poll back; upstream down then back | Browser journeys per app, in order: "upstream unreachable after a good read" 26/26; the full withdrawal story across three apps | Minutes to hours; run rarely | +| **5. Cross-app consistency** **(recommendation)** | The same key gives the same status and counts in every app | For a handful of keys, read the owner's API and each consumer's stored copy (OQL or its page), compare status, version and counts, print a table with the denominator. Nothing in the evidence did this; a UX scan found the two contradictions in §2 step 16 by eye | Seconds once all apps run | +| **6. Cross-app journeys** | The few flows that truly span apps: publish → poll → act → poll back; upstream down then back | Browser journeys per app, in order: "upstream unreachable after a good read" 26/26; the full cancellation story across three apps | Minutes to hours; run rarely | ### Cadence @@ -155,13 +162,16 @@ Climb from cheap to dear. A rung earns its cost only once the rung below is clea - **One `mxcli test --local` at a time on the machine, across all apps.** It binds the fixed ports 8081 and 8091, whichever project runs it. Runners check those ports and refuse when they are - taken; let them, and queue the next app's run behind the current one. Apps that must run tests in - parallel use `mxcli test --attach` against their own running app on their own ports. + taken; let them, and queue the next app's run behind the current one. - **The model-write guard is machine-wide too.** `exec.sh` refused because another project's raw - `mxcli exec` was running. Serialise model writes across apps. -- **Do not combine `run --test-endpoint` with `test --local` on the same model** (StackOverflowError - at startup), and never commit while an app started with `--test-endpoint` runs: it writes an - `MxTest` module and an after-startup setting into the model. + `mxcli exec` was running. Serialise model writes across apps. `mxcli test --attach` counts as a + model write (`testing-shape.md`), so it is not a way to run two apps' tests in parallel. +- **Never stop a test run with a signal** (BUG-157). `mxcli test` and `run --test-endpoint` point the + after-startup microflow at an injected `MxTest` flow and restore it only on a clean exit. A run + stopped by a kill or a background-task stop leaves the setting there, the next injection chains the + flow to itself, and the app dies at startup with a StackOverflowError. Check the setting before + the next run; BUG-157 has the repair. Never commit while an app started with `--test-endpoint` + runs: it writes an `MxTest` module and an after-startup setting into the model. - **`@cleanup rollback` is the default.** A seed that later tests use must say `@cleanup none`, and a cleanup test must remove it; delete by exact key, not by prefix (a prefix cleanup reached demo data that shared the prefix). @@ -192,51 +202,18 @@ Climb from cheap to dear. A rung earns its cost only once the rung below is clea --- -## 5. Failure register - -"Caught by" is the rung that found it. "Would have" is the cheapest rung that could have. -Source: **F** = a FINDINGS line in one of the three apps; **D** = another project document (a proof -file or the UX scan of 2026-10-12). - -| # | Failure | Caught by | Cheapest rung that would have | Src | -|---|---|---|---|---| -| 1 | Unset String query parameter arrives as `''`, rejected as invalid (7 of 29 → 29 of 29) | 1 | 1 | F | -| 2 | Per-test rollback removed a seed the later tests needed | 1 | 1 | F | -| 3 | Published REST has no auth clause in MDL; an auth property is ignored (BUG-152) | 4 | 4 | F | -| 4 | Malformed cursor gets runtime 560 before the microflow; the mock answered 200 | 3 | 3 | F | -| 5 | Mock rejected empty `limit=`; the publisher treats it as not given | 3 | 3 | F | -| 6 | Consumer never pruned rows the publisher stopped serving | the publisher team's confirmation request (cross-app, by hand) | 1b | F | -| 7 | Demo reset left stored copies hanging off recreated records | 6 (journey) | 1b | F | -| 8 | Non-2xx handler lost the status (401 logged as 0) and rolled back the endpoint row | 2 | 2 | D | -| 9 | Cache columns 30 characters, live feed sends 40 | 2, run against the live publisher instead of the mock | 3 | D | -| 10 | `LastError = ''` matched `!= empty`; healthy feeds shown as unreachable | live UI review | 1b | F | -| 11 | Producer hardcoded version and state; `since` missed the withdrawal timestamp; consumer cursor skipped the withdrawal | 6 (fresh cross-app story) | 1 | F | -| 12 | Stale mock process on the old port served old records; false test failure | 2 | 2 (reset fixtures first) | F | -| 13 | Two consumer tests pass on the mock and fail on the live publisher | 3 (by accident: run against both) | 3 | F | -| 14 | Consumer fallback seeded "Approved, now" on a failed read; owner holds the change Open | cross-app process trail and UX scan | 5 | F | -| 15 | Same change: 5 field cases in one app, 6 in another | recorded video, UX scan | 5 | F | -| 16 | Status card "Reachable · ok" from a 3-day-old probe while live calls returned 400 | UX scan | 1b (card fed by the call path) | D | -| 17 | "Last success" days old on APIs polled every 5 minutes; no last-error shown (cause not investigated) | UX scan | 1b + a stale marker | D | -| 18 | Applicability checked once at publish while the other app was down; never retried | live page review | 2 (`down` mode at first call) | F | -| 19 | Field sign in the contract example opposite to what the database stores | building rung 1 | 1 | F | -| 20 | Rows with an empty `updatedAt` never match the cursor | building rung 1 | 1 | F | -| 21 | Date-shaped JSON sample inferred DateTime; String mapping failed mxbuild (CE5015) | exec gate (mxbuild) | exec gate | F | -| 22 | Two apps on one host shared the session cookie; one login signed the other out | 6 | 6 | F | -| 23 | `test --local` while the same model runs with `--test-endpoint`: StackOverflowError at startup | 1 | — (process rule) | F | -| 24 | Model-write guard is machine-wide; an exec refused while another app wrote | any exec | — (serialise) | F | -| 25 | Consumed-test wrapper read only the first verdict | audit of the wrapper | — (read every verdict) | F | -| 26 | Journey order load-bearing: a mock reset in one journey broke the next (4 false failures) | 6 | — (order in the suite) | F | - -What the register says: rungs 1–4 caught 12 of 26; a journey (rung 6) caught 4; a human looking -(page review, UX scan, recorded video, another team's question) caught 7; the exec gate or an audit -of a runner caught 3. Of the 11 found by a journey or a human, 9 had a cheaper rung that would have -caught them, and two of those — the cross-app contradictions, 14 and 15 — needed the one rung -nobody ran: rung 5. +## 5. What the evidence says about the ladder + +Of 26 recorded failures, rungs 1–4 caught 12, a journey 4, a human looking 7, the exec gate or a +runner audit 3. Of the 11 found by a journey or a human, 9 had a cheaper rung that would have +caught them; the two cross-app contradictions needed rung 5, which nobody ran. The full register, +with the catching and the cheapest rung per failure, is +`process/rest-between-mendix-apps-register-2026-10.md`. ## Related - `rest-integration-first-time-right.md` — the mapping and page mechanics for one consumer - `learned-constants-and-secrets.md` — where reader passwords and base URLs live -- `testing-shape.md` — false-green register; `@cleanup` and test-runtime ports +- `testing-shape.md` — false-green register; test-runtime ports 8081/8091; `--attach` is a model write - `tool-output-is-not-ground-truth.md` — a smoke test checks the body, not the status code -- `bug-logs/mxcli-bugs.md` BUG-152 — published REST auth, resource list, mapping drops +- `bug-logs/mxcli-bugs.md` BUG-152 — published REST auth, resource list, mapping drops; BUG-157 — a signal-stopped test run breaks startup