Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"cmd/mxcli","date":"2026-10-03","symptom":"`go test ./cmd/mxcli/` on any OS appended session_start/session_end lines to the developer's real ~/.mxcli/logs/mxcli-<date>.log, and could delete their older log files","cause":"in-process command tests (e.g. runCheckFiles in TestCheck_TestFileResolvesTheRunnersModule) reach newLoggedExecutorTo -> diaglog.Init, which writes to logDirectory() (MXCLI_LOG_DIR, else ~/.mxcli/logs) and runs cleanOldLogs on it. The MXCLI_LOG_DIR hook existed but only the two subprocess tests set it, and the HOME-only overrides in other tests do not move the log on Windows (os.UserHomeDir reads USERPROFILE)","file":"`cmd/mxcli/testmain_test.go` (TestMain); `mdl/diaglog/diaglog.go` (logDirectory, cleanOldLogs)","insight":"A package-level TestMain that points MXCLI_LOG_DIR at a temp dir (unless already set) is the one place that covers every present and future in-process test; per-test Setenv only protects the tests whose author remembered. cleanOldLogs makes the leak destructive, not just noisy. Prove it with a sentinel home: run go test with HOME, USERPROFILE, APPDATA and LOCALAPPDATA all pointed at a fresh temp dir and find it for .mxcli/logs before and after (before: one mxcli-<date>.log with one session in check mode, because diaglog is one session per process; after: nothing). Setting HOME alone proves nothing on Windows","refs":["mendixlabs/mxcli#1273","mendixlabs/mxcli#1256"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "mendixlabs/mxcli#1284: on Windows `mxcli test --attach` always says the app that published `.mxcli/test-endpoint.json` \"is no longer running\" although the pid is alive; the dev loop's `run-local.json` \"already serving\" detection never fires either", "cause": "pid liveness was `os.FindProcess(pid)` + `Signal(0)`; Go supports no signal but Kill on Windows and returns EWINDOWS for the rest, so every pid read dead", "file": "`internal/procalive/` (Alive, build-tagged unix/windows); callers `cmd/mxcli/testrunner/handshake.go`, `cmd/mxcli/devloop_handshake.go`, `cmd/mxcli/docker/procgroup_windows.go`", "insight": "The correct Windows check (OpenProcess(SYNCHRONIZE) + WaitForSingleObject(h, 0) == WAIT_TIMEOUT) already existed in docker/procgroup_windows.go since the waitReady incident, but two pid-based copies of the Signal(0) idiom were written separately and never moved over; Linux-only CI could not see it. Four tests were already red on a native Windows run (TestHandshakeRoundTrip, TestAttachUsesTheAdminPasswordNotTheEndpointToken, TestDevLoopHandshake_RoundTrip, TestWarnIfDevLoopServing_FiresForALiveDevLoop). Grep for `Signal(syscall.Signal(0))` / `Signal(0)` before trusting any new liveness check; use procalive.Alive for a bare pid.", "refs": ["mendixlabs/mxcli#1284", "#897"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "cmd/mxcli", "date": "2026-10-09", "symptom": "Windows: `mxcli run stop -p App.mpr` prints \"failed: 1 process(es) of pid N's run are still alive: [N]\" (exit 1) for a run that is shutting down or gone; a few seconds later the pid no longer exists and no ports are held", "cause": "docker.PidAlive (session_other.go, used by run stop / run status) treated \"os.FindProcess can open the pid\" as alive on Windows. Windows keeps an exited process object, and so an openable pid, for as long as anyone holds a handle to it (the parent that started it, typically), so a terminated run read as alive. It arrived after the #1284 handshake fix was written, so procalive did not cover it", "fix": "PidAlive delegates to internal/procalive.Alive: OpenProcess(SYNCHRONIZE) + WaitForSingleObject(h, 0) == WAIT_TIMEOUT on Windows, the same Signal(0) as before elsewhere off Linux; Linux keeps its /proc PidAlive", "insight": "\"Can I open it\" is not \"is it running\" on Windows: an exited process is openable until the last handle closes. The regression test holds its own SYNCHRONIZE handle, kills the child, waits for WAIT_OBJECT_0, then asks; the old PidAlive says alive. When porting a liveness fix onto a newer main, grep for every pid-liveness helper (processAlive, PidAlive, Signal(0)), not just the ones the original PR named. E2E on Windows 11 / Mendix 11.13.0: `run stop` went from \"failed: … still alive\" on every stop to \"stopped: pid N in 400ms\"", "file": "cmd/mxcli/docker/session_other.go (PidAlive)", "test": "cmd/mxcli/docker/session_windows_test.go (TestPidAlive_ExitedProcessWithAnOpenHandleIsNotAlive)", "refs": ["mendixlabs/mxcli#1284"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-10-09","symptom":"`create or replace navigation` turning page item 'Work' into a sub-menu 'Work' reports success (plus `kept the stored action of menu item 'Work' (Forms$FormAction): MDL cannot express it`), then `mx check` fails with CE0548 \"Items with subitems cannot have an action themselves.\" (v0.25.0 regression; a different sub-menu caption avoids it)","cause":"`keepStoredMenuAction` had a sub-menu branch that kept ANY stored non-NoAction, on the reasoning that a sub-menu \"takes no OnClick, so a stored action on one is never stated\". Pairing is by caption only, so the stored page item's action was carried onto the new sub-menu. Navigation profiles and menu documents share the decision","file":"`mdl/executor/cmd_navigation.go` (`keepStoredMenuAction`)","insight":"**A keep-what-MDL-cannot-say rule must only keep what the target element can legally hold.** \"The script states no action\" is not \"the script wants the stored one\" when the element's kind changed under the same caption — a sub-menu can hold no action at all, so the carry is never right there. The tell in the output was the false reason: `Forms$FormAction` is printable MDL, yet the message said MDL cannot express it. Guards `TestNavigationRewrite_SubMenuDoesNotKeepAStoredAction` / `TestCreateMenu_SubMenuDoesNotKeepAStoredAction` (control: a leaf still keeps an unprintable action, #980). Measured on 11.14.0 with `mdl-examples/bug-tests/1341-navigation-submenu-keeps-page-action.mdl`: unfixed binary -> docker check CE0548; fixed -> 0 errors","refs":["mendixlabs/mxcli#1341","ako/mxcli#980"],"ce":["CE0548"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"internal/testutil","date":"2026-10-02","symptom":"On Windows, `go test` of cmd/mxcli, cmd/mxcli/docker, cmd/mxcli/marketplace, internal/auth and mdl/executor wrote into the developer's REAL `%USERPROFILE%\\.mxcli` (a fixture PAT in `auth.json`, fake `mxbuild/99.99.98`, `mxbuild/99.99.99`, `runtime/99.99.99`, `marketplace-catalog-default.json`), and `TestAuthList_EmptyStore`, `TestResolve_NoCredential`, `TestClientFor_NoCredential` failed because an earlier run had left a credential behind","cause":"Thirteen test files isolated the home directory with `t.Setenv(\"HOME\", dir)`. `os.UserHomeDir()` reads `USERPROFILE` on Windows and `HOME` everywhere else, so on Windows the override was ignored and the tests used the real profile; the leaked state persisted between runs, which made the failures order- and history-dependent","file":"`internal/testutil/home.go` (`SetHome` sets HOME and USERPROFILE); `internal/testutil/guard_test.go` (`TestNoBareHomeSetenv`)","insight":"It went unnoticed because CI runs the tests on Linux, where HOME is what UserHomeDir reads, and the Windows CI job is `-run`-scoped (#897) so it never executes these tests. Two files (`mdl/diaglog`, `cmd/mxcli/docker/download_test.go`) had already been fixed locally with a per-OS helper, but a copy-local fix teaches the next test author nothing: the repair is one shared helper plus a guard that fails on a bare `Setenv(\"HOME\"`. To prove a home-isolation fix, run the suite with USERPROFILE and HOME pointing at an empty sentinel directory and list `.mxcli` in it afterwards (before the fix: auth.json, mxbuild, runtime, catalog cache; after: only `logs/`, written by `TestCheck_TestFileResolvesTheRunnersModule`, which never sets a home at all). `diaglog.logDirectory` also honours `MXCLI_LOG_DIR`, the other lever for tests that execute cobra commands","refs":["mendixlabs/mxcli#1256","#897"]}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **A navigation sub-menu no longer keeps the action of the page item it replaces** (mendixlabs/mxcli#1341) — turning menu item `'Work'` into sub-menu `'Work'` with `create or replace navigation` (or `create or modify menu`) paired the two by caption and carried the page item's `Forms$FormAction` onto the sub-menu, reporting it as an action "MDL cannot express"; `mx check` then failed with CE0548 "Items with subitems cannot have an action themselves." A sub-menu is now always written with no action (measured on 11.14.0).
- **`mxcli test --attach` and the dev loop's "already serving" detection see a live app on Windows** (mendixlabs/mxcli#1284) — both read a pid from a handshake file (`.mxcli/test-endpoint.json`, `.mxcli/run-local.json`) and tested it with `Signal(0)`, which Go supports only as Kill on Windows, so every pid read as dead: `test --attach` always refused with "the app that published … is no longer running" while the app was up, and a command that looks for a dev loop already serving the project (the recompile warning, for one) never found it. Liveness is now `internal/procalive.Alive`, which uses OpenProcess and WaitForSingleObject on Windows (the check `run --local` already used for mxbuild) and `Signal(0)` elsewhere. No change on Linux or macOS.
- **`run stop` on Windows no longer reports a run that has shut down as still alive** — it printed "failed: 1 process(es) of pid N's run are still alive" and exited 1 for a run that had stopped, because its liveness check counted any pid Windows could still open as alive, and an exited process stays openable while its parent holds a handle to it. It now uses the same check as `test --attach` (above), and reports "stopped: pid N".
- **`retrieve $u from $obj/System.owner` passed check and exec, then failed the build with CE0136** (mendixlabs/mxcli#1358) — `owner` and `changedBy` are entity flags, not modelled associations: mxbuild resolves the name in a retrieve by association but derives no entity from it, `[CE0136] "Retrieve object must specify the 'Entity' property."`, with or without `owner: AutoOwner` (measured on 11.12.2, `System.changedBy` alike). `check` and `exec` now refuse it as **MDL-RETRIEVE02** and name the form that builds: `retrieve $u from System.User where [id = $obj/System.owner] first;`.
- **`run --local` and `test --local` work on Windows without Developer Mode or admin rights** (mendixlabs/mxcli#1286) — the first run for a new Mendix version failed with `linking runtime into mxbuild cache: symlink …: A required privilege is not held by the client.` mxcli links the downloaded runtime into the mxbuild cache, and an ordinary Windows user may create a symbolic link only with Developer Mode or an elevated token. When Windows refuses the symlink, mxcli now makes a directory junction instead, which needs no privilege (the same link `mklink /J` makes; it needs `cmd.exe`, which every Windows has). The other link site, the PAD files `docker build` links into the mxbuild cache, gets the same fallback. A cache that was already linked by hand with `New-Item -ItemType Junction` is left as it is.
- **`run --local --watch` applies a domain model change on Windows** (mendixlabs/mxcli#1342) — page and microflow edits hot-reloaded, but adding an entity or an attribute failed every rebuild with "The process cannot access the file '…\deployment\web' because it is being used by another process", and the app then served 404 until `run --local` was restarted. A domain model change makes mxbuild recreate `deployment/web`, and the incremental web client bundler runs inside it, which Windows will not let anyone delete. On that failure the watch loop now stops the bundler, builds again, and starts a fresh bundler on the new `web/`; page edits keep the incremental path. Applies to Mendix versions whose deployment has `web/rollup.config.mjs` (reported on 11.12.4, reproduced on 11.13.0); 11.12.6 and 11.14+ bundle in mxbuild and were never affected.
- **`run stop` and `run status` find a detached run's processes on every machine** — the guard against a reused session id rebuilt each process's start time from the kernel's boot time, which is whole seconds, and allowed only one second of slack. On a machine that booted late in a second, a run's own processes read as older than the run and were skipped, so `run stop` could report "was not running" for a live run. The slack is now two seconds. This was also the intermittent `TestRunStop_*` / `TestSessionMembers_*` failure on CI.
- **The catalog names a call's and a delete's target** (mendixlabs/mxcli#1305) — `activities_for()` and `CATALOG.ACTIVITIES` returned every microflow, nanoflow, Java action and JavaScript action call with `action_ref=""`, and every delete with `entity_ref=""`, although `refs_from()` had both targets; a loop-scoped lint rule could not follow a call out of the loop. `action_ref` / `ActionRef` is now the called document, `entity_ref` / `EntityRef` a delete's entity (when the flow types the variable: a parameter, a create or retrieve output, a loop iterator), and the new `queue_ref` / `QueueRef` the task queue a microflow or Java action call runs in, so a rule can skip a call that runs asynchronously. A nanoflow's JavaScript action call now has a `refs_from()` `call` row (`target_type` `"JAVASCRIPT_ACTION"`), so `show callers` sees it. The catalog schema is bumped to 23; a cached catalog rebuilds.
- **A DataGrid 2 column's `Visible:` expression is written** — `Visible: $showPrices`, `Visible: if … then … else …`, `visible: not(…)` and `Visible: [cond]` on a column passed `check` and `exec` and were stored as `true`, so the column was always visible; only the old quoted `Visible: '<expr>'` was kept. `alter page … set (Visible: <expression>) on grid column(…)` was refused as "column property VisibleIf not found", and an inserted column dropped `Visible: false` too. `describe` now prints the bare expression. A column's visibility is evaluated once for the grid, with no row object, so `$currentObject` there is CE0117 at build; `check` refuses it as **MDL-WIDGET43** (measured on 11.14.0). Use a page variable or parameter. Projects regenerate their widget definitions (generator version 18).
Expand Down
3 changes: 2 additions & 1 deletion cmd/mxcli/cmd_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"testing"

"github.com/mendixlabs/mxcli/internal/auth"
"github.com/mendixlabs/mxcli/internal/testutil"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
Expand All @@ -24,7 +25,7 @@ import (
func withTestHome(t *testing.T) string {
t.Helper()
home := t.TempDir()
t.Setenv("HOME", home)
testutil.SetHome(t, home)
t.Setenv(auth.EnvPAT, "")
t.Setenv(auth.EnvProfile, "")
return home
Expand Down
5 changes: 3 additions & 2 deletions cmd/mxcli/cmd_marketplace_install_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"testing"

"github.com/mendixlabs/mxcli/internal/marketplace"
"github.com/mendixlabs/mxcli/internal/testutil"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -48,7 +49,7 @@ func buildLocalMPK(t *testing.T, name string, entries map[string]string) string
// placeholder .mpr — the widget path only checks that the project exists.
func runInstallFile(t *testing.T, args ...string) (string, error) {
t.Helper()
t.Setenv("HOME", t.TempDir())
testutil.SetHome(t, t.TempDir())

origFactory := marketplaceClientFactory
marketplaceClientFactory = func(_ context.Context, _ *cobra.Command) (*marketplace.Client, error) {
Expand Down Expand Up @@ -183,7 +184,7 @@ func TestInstallFile_NoContentIDAndNoFile(t *testing.T) {
// and still reaches the client factory (which this helper makes fatal).
func TestInstallFile_ContentIDStillUsesTheClient(t *testing.T) {
mpr := placeholderProject(t)
t.Setenv("HOME", t.TempDir())
testutil.SetHome(t, t.TempDir())
called := false
origFactory := marketplaceClientFactory
marketplaceClientFactory = func(_ context.Context, _ *cobra.Command) (*marketplace.Client, error) {
Expand Down
3 changes: 2 additions & 1 deletion cmd/mxcli/cmd_marketplace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"testing"

"github.com/mendixlabs/mxcli/internal/marketplace"
"github.com/mendixlabs/mxcli/internal/testutil"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
Expand All @@ -37,7 +38,7 @@ func runMarketplace(t *testing.T, handler http.HandlerFunc, args ...string) (str
t.Helper()
// Isolate the catalog cache (~/.mxcli/...) into a temp HOME so tests never
// read or write the real user cache.
t.Setenv("HOME", t.TempDir())
testutil.SetHome(t, t.TempDir())
ts := httptest.NewServer(handler)
t.Cleanup(ts.Close)

Expand Down
18 changes: 3 additions & 15 deletions cmd/mxcli/devloop_handshake.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (
"fmt"
"os"
"path/filepath"
"syscall"
"time"

"github.com/mendixlabs/mxcli/internal/procalive"
)

// devLoopHandshakeName is what `mxcli run --local` publishes so another mxcli
Expand Down Expand Up @@ -95,22 +96,9 @@ func readDevLoopHandshake(projectPath string) (devLoopHandshake, error) {
if err := json.Unmarshal(body, &h); err != nil {
return h, fmt.Errorf("%s is not valid JSON: %w", path, err)
}
if !processAlive(h.PID) {
if !procalive.Alive(h.PID) {
return h, fmt.Errorf("%s refers to process %d, which is no longer running\n"+
" (the dev loop was stopped without cleaning up; start a new one)", path, h.PID)
}
return h, nil
}

// processAlive reports whether a pid exists. Signal 0 is delivered to no one but
// still performs the existence and permission checks.
func processAlive(pid int) bool {
if pid <= 0 {
return false
}
p, err := os.FindProcess(pid)
if err != nil {
return false
}
return p.Signal(syscall.Signal(0)) == nil
}
4 changes: 3 additions & 1 deletion cmd/mxcli/docker/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"path/filepath"
"runtime"
"testing"

"github.com/mendixlabs/mxcli/internal/testutil"
)

func TestCheck_SkipUpdateWidgets(t *testing.T) {
Expand Down Expand Up @@ -275,7 +277,7 @@ func TestCheck_UpdateWidgetsReceivesAbsolutePath(t *testing.T) {

func TestResolveMxForVersion_PrefersExactCachedVersion(t *testing.T) {
dir := t.TempDir()
setTestHomeDir(t, dir)
testutil.SetHome(t, dir)
setTestApplicationsDir(t, t.TempDir()) // prevent real macOS Studio Pro from matching
// Point PATH at an empty temp dir (rather than clearing it) so exec.LookPath
// still works for any other testing infrastructure but can't find mx.
Expand Down
4 changes: 3 additions & 1 deletion cmd/mxcli/docker/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"path/filepath"
"runtime"
"testing"

"github.com/mendixlabs/mxcli/internal/testutil"
)

func TestResolveMxBuild_ExplicitPath(t *testing.T) {
Expand Down Expand Up @@ -285,7 +287,7 @@ func TestResolveMxBuild_PrefersStudioProOverCache(t *testing.T) {

func TestResolveMxBuild_PrefersExactCachedVersion(t *testing.T) {
dir := t.TempDir()
setTestHomeDir(t, dir)
testutil.SetHome(t, dir)
setTestApplicationsDir(t, t.TempDir()) // prevent real macOS Studio Pro from matching
// Point PATH at an empty temp dir (rather than clearing it) so exec.LookPath
// still works for any other testing infrastructure but can't find mxbuild.
Expand Down
Loading
Loading