Skip to content

Sign-out and open-link actions: stop dropping them to Forms$NoAction - #389

Merged
ako merged 3 commits into
mainfrom
claude/mxcli-unit-test-perf-n7ggx8
Sep 4, 2026
Merged

Sign-out and open-link actions: stop dropping them to Forms$NoAction#389
ako merged 3 commits into
mainfrom
claude/mxcli-unit-test-perf-n7ggx8

Conversation

@ako

@ako ako commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Three client actions MDL could spell and neither engine could store. All three failed the same way — the model stayed valid, mx check reported 0 errors, and the button or menu entry simply did nothing.

CapTrackV2 FINDINGS §10 reported the first of these as "SIGN_OUT needs the legacy engine". The refusal was honest; the advice was not.

1. SIGN_OUT on a button (a54e8b40)

The default engine refused it — client action *pages.SignOutClientAction not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy. The legacy writer had no case for it either and fell through to a quiet default that returns Forms$NoAction, so the recommended escape hatch was the strictly worse path: a button that rendered, said "Sign out", and did nothing.

before after
modelsdk refused Forms$SignOutClientAction, DisabledDuringExecution: true
legacy Forms$NoAction identical document
describe page actionbutton btnOut (Caption: 'Sign out') … Action: sign_out
mx check 0 errors 0 errors

The two-key shape is pinned against a Studio Pro-authored button in ako/TestApp — a reference provably Studio Pro's rather than mxcli's, since until this change neither engine could emit the type.

2. OPEN_LINK (3bd242c4)

Same defect, plus two traps a reference settled and reasoning would not:

  • The storage name is Forms$OpenLinkClientAction. The semantic type is LinkClientAction and the executor stamped Forms$LinkClientAction — a wrong $Type that never reached disk only because nothing could write the action.
  • The address is not a string property but a nested Forms$StaticOrDynamicString.

Pinned against 31 Studio Pro link buttons (ako/TestApp, FeedbackModule) — exactly five keys, LinkType: "Web" in all 31:

{ "$Type": "Forms$OpenLinkClientAction",
  "Address": { "$Type": "Forms$StaticOrDynamicString",
               "AttributeRef": null, "IsDynamic": false,
               "Value": "https://www.mendix.com/" },
  "DisabledDuringExecution": true,
  "LinkType": "Web" }

6 of the 31 are dynamic — the address is read from an attribute at runtime. MDL cannot author that, so DESCRIBE flags such a button rather than printing its address as a literal, which would round-trip into a different link.

gen declares a fourth property on Forms$StaticOrDynamicString, Attribute, that none of the 31 documents carry. It is deliberately left unset — writing a key Mendix does not store is what produces a document mxbuild accepts and Studio Pro cannot open.

3. A sign-out menu item (d4d56271)

Raised separately: a navigation menu can carry a log-out item too. Fixing the button proved nothing here, because a menu item's action reaches storage through four switches that share no code with the button path:

before
menuActionToGen (menu document, modelsdk) NoAction default
navMenuAction (navigation profile, raw BSON) NoAction default
resolveMenuAction (modelsdk read) raw type name, unmapped
parseNavMenuItem (legacy read) raw type name, unmapped

The readers are the subtler half. Both had a fallback that stored the unmapped $Type, which looks like it preserves information — ActionType became "Forms$SignOutClientAction" — while breaking the round trip, because DESCRIBE and both writers key on "SignOutAction". A round trip closes only when the reader produces the exact string the writer consumes.

MENU ITEM 'Sign out' SIGN_OUT [ICON …] now works in both a menu document and a navigation profile. SIGN_OUT names no target, so the visitor reads it separately from the PAGE/MICROFLOW switch — folding it in would consume a qualifiedName and mis-assign a trailing ICON. That case is pinned in the example.

Measured on ako/TestApp (Mendix 11.14):

before   show navigation menu Responsive  ->  Item 5
after    show navigation menu Responsive  ->  Item 5 -> sign out

and describeexec now puts an identical Forms$SignOutClientAction back on disk, mx check 0 errors.

Notes on the controls

Two corrections worth recording, both caught rather than shipped:

  • The SIGN_OUT commit's "still unimplemented" controls named LinkClientAction. Implementing OPEN_LINK invalidated them, and the gate run failed on exactly those two tests. They now name ShowHomePageClientAction — no gen type, no metamodel counterpart, no MDL statement that builds one — so the control pins a structural impossibility rather than a feature with a shelf life.
  • My first attempt at the menu control was invalid: I broke a file syntactically, make build failed into /dev/null, and I was probing a stale binary that still showed the fix working. Redone properly, neutralising both readers takes Item 5 -> sign out back to Item 5.

Validation

go test ./... clean, make vet clean, make check-mdl clean, mx check 0 errors on every artefact produced (Mendix 11.13 and 11.14), check-findings 907 records OK. New examples: captrack-10-sign-out-action.mdl, captrack-10-open-link-action.mdl, captrack-10-sign-out-menu-item.mdl.

Branch restarted from main after #387 merged, so these three commits carry no already-merged history.

🤖 Generated with Claude Code

https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ


Generated by Claude Code

`ACTIONBUTTON … (Action: SIGN_OUT)` was refused by the default engine:

    client action *pages.SignOutClientAction not yet supported by the
    modelsdk engine — rerun with MXCLI_ENGINE=legacy

The refusal was honest. The advice was not. The legacy writer had no case for
the action either, and its default branch is QUIET — it returns Forms$NoAction
for anything unmatched — so the recommended escape hatch produced a button that
rendered, said "Sign out", and did nothing, with `mxcli check`, `exec` and
`mx check` all clean. Measured on Mendix 11.13, `describe page` came back
`actionbutton btnOut (Caption: 'Sign out')` with no action at all, and the
stored BSON held Forms$NoAction.

Both engines now write the same document, and DESCRIBE renders `sign_out` so it
round-trips:

    { "$Type": "Forms$SignOutClientAction", "DisabledDuringExecution": true }

Two keys and no more, pinned against a Studio Pro-authored sign-out button in
ako/TestApp. That reference is provably Studio Pro's rather than mxcli's,
because until this change NEITHER engine could emit the type — which is also
why the shape could not have been guessed from the writers.

Verified with the fix reverted, one engine at a time: modelsdk fails with the
refusal verbatim, legacy fails by writing Forms$NoAction. mx check is 0 errors
on both engines' output, before and after — this was never a build error, which
is exactly what made it dangerous.

The control pins the fallback separately. A test asserting "SIGN_OUT is no
longer NoAction" would also pass if someone had merely softened the default, so
OPEN_LINK — still unimplemented — is asserted to STILL hit Forms$NoAction on
legacy and still be refused on modelsdk.

OPEN_LINK is left unimplemented deliberately: gen calls it OpenLinkClientAction
and its Address is an element rather than a string, so it is a separate job.
`mxcli syntax page.action` listed it as available with no caveat; it now says
it is written by neither engine and points at a nanoflow instead.

Reported as CapTrackV2 FINDINGS §10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
`ACTIONBUTTON … (Action: OPEN_LINK 'https://…')` reached storage on neither
engine. modelsdk refused it; legacy fell through to its QUIET default and wrote
Forms$NoAction, so the button rendered, said "Docs", and did nothing — with
`mxcli check`, `exec` and `mx check` all clean. Same defect as the SIGN_OUT
case in the previous commit, and the syntax help listed the action as available
either way.

Two traps here that a Studio Pro reference settled and reasoning would not.

The STORAGE NAME is Forms$OpenLinkClientAction. The semantic type is
LinkClientAction and the executor stamped "Forms$LinkClientAction", which is
not what Mendix stores — a wrong $Type that never reached disk only because
nothing could write the action at all. And the address is not a string
property: it is a nested Forms$StaticOrDynamicString.

Pinned against 31 Studio Pro-authored link buttons (ako/TestApp,
FeedbackModule) — exactly five keys, LinkType "Web" in all 31:

    { "$Type": "Forms$OpenLinkClientAction",
      "Address": { "$Type": "Forms$StaticOrDynamicString",
                   "AttributeRef": null, "IsDynamic": false,
                   "Value": "https://www.mendix.com/" },
      "DisabledDuringExecution": true,
      "LinkType": "Web" }

6 of those 31 are DYNAMIC — the address is read from an attribute at runtime.
MDL cannot author that, so DESCRIBE flags such a button rather than printing
its address as a literal, which would round-trip into a different link.

gen declares a fourth property on Forms$StaticOrDynamicString, `Attribute`,
that not one of the 31 documents carries. It is deliberately left unset:
writing a key Mendix does not store is what produces a document mxbuild accepts
and Studio Pro cannot open.

The previous commit's "still unimplemented" controls named LinkClientAction,
which stops being a valid control the moment this lands — they now name
ShowHomePageClientAction, which has no gen type, no metamodel counterpart and
no MDL statement that builds one, so it is structurally unwritable rather than
merely not yet written.

Both engines emit the same document; mx check 0 errors on each.

Reported as CapTrackV2 FINDINGS §10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
A navigation menu can carry a log-out item. mxcli could neither author one nor
read one back:

  authoring  MDL's `menu item` took PAGE or MICROFLOW only, so there was no
             spelling for it at all.
  reading    ako/TestApp's sign-out menu item came back as a plain
             `menu item 'Item 5';`, so DESCRIBE -> exec turned a working log-out
             entry into a dead one — silently, with mx check clean.

Fixing the sign-out BUTTON proved nothing about this, because a menu item's
action reaches storage through four switches that share no code with the button
path — two writers (a standalone menu document and the menu inside a navigation
profile) and two readers. All four had to be wired:

    menuActionToGen      NoAction default        modelsdk, menu document
    navMenuAction        NoAction default        raw BSON, navigation profile
    resolveMenuAction    raw type name           modelsdk read
    parseNavMenuItem     raw type name           legacy read

The readers are the subtler half. Both had a fallback that stored the unmapped
$Type, which LOOKS like it preserves information — ActionType became
"Forms$SignOutClientAction" — while breaking the round trip, because DESCRIBE
and both writers key on "SignOutAction". A round trip closes only when the
reader produces the exact string the writer consumes.

Studio Pro stores the same element a button carries — Forms$SignOutClientAction,
DisabledDuringExecution true, nothing else — which is why SIGN_OUT sits beside
PAGE and MICROFLOW rather than getting a syntax of its own. It names no target,
so the visitor reads it separately from the PAGE/MICROFLOW switch; folding it in
would consume a qualifiedName and mis-assign a trailing ICON. That case is in
the example.

Measured on ako/TestApp (Mendix 11.14), and controlled by neutralising both
readers and re-reading it:

    before   show navigation menu Responsive  ->  Item 5
    after    show navigation menu Responsive  ->  Item 5 -> sign out

describe -> exec now puts an identical Forms$SignOutClientAction back on disk,
and mx check reports 0 errors on the result.

Reported as CapTrackV2 FINDINGS §10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ

ako commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

github-advanced-security is red here and it is not this PR's failure.

The job runs GitHub's Copilot autofix agent (COPILOT_AGENT_MODEL: sweagent-capi:gpt-5.3-codex) and exits 1 inside its own runtime, before touching anything in the diff — the log ends in the agent's own cleanup and orphan-process teardown, with no reference to any file this PR changes. The same failure, with the same signature, occurred on #387, which merged.

The actual code-scanning analysis passes: CodeQL plus all five Analyze jobs (go, actions, java-kotlin, javascript-typescript, python) are green.

There is no fix to port — the failure is in GitHub's own action, not in this repository — so I am standing down on it rather than re-running or working around it. Everything else on the head commit is green except build-and-test, still in progress; I'll follow up on that one.


Generated by Claude Code

@ako
ako merged commit 25be399 into main Sep 4, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants