From 09809666de50f934bdef37615605936f4a5f6888 Mon Sep 17 00:00:00 2001 From: MendixMau Date: Fri, 23 Oct 2026 12:53:27 +0200 Subject: [PATCH 1/2] feat(test-stack-up): local lane without containers; runtime-agnostic container calls (refs #525) - test-stack-up.sh: a down app starts with mxcli run --local --watch --detach when no container runtime answers (or --local / MXTK_APP_LANE=local); a free mxbuild serve port per project (6543 is fixed in mxcli); restarts use the project's APP_PORTS when the handshake is gone; the split-model unit count is compared before and after. - Every direct container call goes through MXCLI_CONTAINER_CLI, else docker (Desktop, colima, OrbStack), else podman. - mendix_at: a themed index.html with no Mendix words was read as 'not serving'; the runtime's /mxclientsystem/mxui/mxui.js is the fallback marker. - check-requirements.sh: a missing container runtime is a note, not a missing requirement. Field test (macOS, colima, Docker Desktop removed): cold start on the local lane 43-76 s, ownership verified, five apps served side by side, model unchanged. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + bin/check-requirements.sh | 12 ++++- project-bin/test-stack-up.sh | 85 +++++++++++++++++++++++++++++++----- 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43dfd52e..d21164d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m `docs/RELEASE-PROCESS.md` §8. ## Unreleased +new(test-stack-up, check-requirements): a local lane — a down app starts with `mxcli run --local --watch --detach` (auto when no container runtime answers, or `--local` / `MXTK_APP_LANE=local`), free mxbuild serve port per project, restarts on the project's own APP_PORTS; container calls follow MXCLI_CONTAINER_CLI / docker (Desktop, colima, OrbStack) / podman; a missing runtime is no longer a missing requirement; a themed index.html no longer hides a Mendix app (refs #525) - fix(progress-report): **`progress-report.py` no longer crashes with `TypeError: '<' not supported between instances of 'dict' and 'dict'` when two exec'd scripts share a leading number** (#513). `script_phases()` sorted `(number, phase dict)` pairs, so `10-data.mdl` and `10b-data-flows.mdl` (or one script logged under two paths), both matched to a plan phase, compared the dicts and every `exec.sh` run ended with `WARN: progress-report.html not updated`. It now sorts on (number, script name) only; an unmatched numbered script still takes the phase of the nearest lower-numbered matched one, ties broken by name. Fixture: `test-progress-report.sh` T17 (fails 3/3 on master). — jopterhorst (#513) - new(canvas): **`project-bin/mf-canvas-facts.py` prints how a microflow or nanoflow lies on the Studio Pro canvas: labels on top of each other, lines through objects, note lines leaving the side that faces away, flows that run right-to-left or curl, captions wider than their shape, and decision captions that ask the opposite of their true branch (SENSE).** It prints facts, one line each, and no verdicts; read-only (it reads units through `mxpatch.py`), exit 2 when a flow could not be read. `verify-module.sh` runs it per module as an info rung (1c, log `14-canvas-facts.log`; a missing script, Python or `.mpr`, an exit 2 or a timeout is an INFO row reading NOT MEASURED, never a gate). Fixture `tests/wave2/test-mf-canvas-facts.sh` runs on a model mxcli v0.27.0 wrote (units and `.mpr` captured); `test-verify-module-446.sh` gains V8 for the rung. Calibration is from Studio Pro 10.24 screenshots, not verified on Studio Pro 11. Field evidence, measured 2026-10-21 on one Mendix 10.24 app: 1,588 flows read in one pass; on the 36 flows an mxcli-built branch added or changed in one module, OVERLAP 19, LINE 27, NOTE-SIDE 17, LEFTWARD 3, TURN 4, SENSE 9 before repair — saingge diff --git a/bin/check-requirements.sh b/bin/check-requirements.sh index 84a86de0..b9a622ed 100755 --- a/bin/check-requirements.sh +++ b/bin/check-requirements.sh @@ -107,8 +107,16 @@ else gone "playwright" "not installed — every browser rung will fault (rc 2)" note "fix: (cd $ROOT && npm i -D playwright && npx playwright install chromium)" fi -command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1 \ - && ok "docker" "responding" || gone "docker" "not responding — no app, no DB, no Jaeger" +# The app needs a runtime, not a container: `mxcli run --local` serves it with none, so a missing +# container runtime is a note, never a missing requirement. Same CLI choice as mxcli +# (MXCLI_CONTAINER_CLI, else docker — Docker Desktop, colima, OrbStack — else podman). +_ctr="${MXCLI_CONTAINER_CLI:-}"; [ -n "$_ctr" ] || { command -v docker >/dev/null 2>&1 && _ctr=docker; } || { command -v podman >/dev/null 2>&1 && _ctr=podman; } +if [ -n "$_ctr" ] && "$_ctr" info >/dev/null 2>&1; then + ok "container runtime" "$_ctr responding (optional: mxcli docker, Jaeger)" +else + ok "container runtime" "${_ctr:-none} not responding — fine: the app runs with mxcli run --local" + note "Jaeger trace assertions need one; test-stack-up.sh starts the app on the local lane without it" +fi # --- the work itself --------------------------------------------------------------------- jn=$(ls "$ROOT"/journeys/*.journey.json 2>/dev/null | wc -l | tr -d ' ') diff --git a/project-bin/test-stack-up.sh b/project-bin/test-stack-up.sh index 55208809..a18e1b78 100755 --- a/project-bin/test-stack-up.sh +++ b/project-bin/test-stack-up.sh @@ -27,6 +27,7 @@ # bin/test-stack-up.sh --check # report only, change nothing. Exit 0 iff required deps are up. # bin/test-stack-up.sh # report, then bring up what is missing (may run a Docker build) # bin/test-stack-up.sh --no-docker # bring up auxiliaries only; never build. "SP is already running it". +# bin/test-stack-up.sh --local # app down: start it with `mxcli run --local --watch --detach`, never a container # bin/test-stack-up.sh --restart # skip the hot reload; rebuild and restart the container (old path) # bin/test-stack-up.sh --track [label] # record a background test process THIS project started # bin/test-stack-up.sh --reap # stop recorded processes older than MXTK_STALE_HOURS; nothing else @@ -104,6 +105,7 @@ # project-specific in /.claude/loop/stack.conf, which this script sources if present: # # APP_PORTS="8080 8081" # fallback scan list; ownership beats it +# RUN_LOCAL_SERVE_PORT=6644 # local lane's mxbuild port (default: first free from 6543) # JAEGER_PORT=16686 # OTel collector UI (optional) # MOCK_HEALTH_URL="http://localhost:3001/api/health" # set to enable the mock rung # MOCK_PORT=3001 @@ -139,6 +141,22 @@ BOOT_TIMEOUT="${BOOT_TIMEOUT:-240}" LOGDIR="${LOGDIR:-${TMPDIR:-/tmp}}" MOCK_LOG="$LOGDIR/${PROJ}-mock.log" DOCKER_LOG="$LOGDIR/${PROJ}-docker-run.log" +LOCAL_LOG="$LOGDIR/${PROJ}-run-local.log" + +# APP LANE — how a down app is brought up. local = `mxcli run --local --watch --detach` (no container +# runtime at all); container = `mxcli docker run`. Default auto: the container lane when a runtime +# answers, else the local lane. MXTK_APP_LANE=local (or --local) skips containers even when one runs. +APP_LANE="${MXTK_APP_LANE:-auto}" +case "$APP_LANE" in auto|local|container) ;; *) echo "MXTK_APP_LANE takes auto, local or container" >&2; exit 2 ;; esac + +# The container CLI, for every direct call below. mxcli itself honours MXCLI_CONTAINER_CLI, so this +# follows the same override; otherwise docker (Docker Desktop, colima, OrbStack, Rancher all ship it), +# else podman. Compose labels are com.docker.compose.* under docker compose and podman's +# docker-compose provider; podman-compose (python) labels differently and is not detected. +if [ -n "${MXCLI_CONTAINER_CLI:-}" ]; then CTR="$MXCLI_CONTAINER_CLI" +elif command -v docker >/dev/null 2>&1; then CTR=docker +elif command -v podman >/dev/null 2>&1; then CTR=podman +else CTR=""; fi # mxcli lives in the project root on a wired project, but may be on PATH instead. if [ -x "$ROOT/mxcli" ]; then MXCLI="$ROOT/mxcli" @@ -149,6 +167,7 @@ MODE="up" case "${1:-}" in --check) MODE="check" ;; --no-docker) MODE="nodocker" ;; + --local) MODE="up"; APP_LANE=local ;; --restart) MODE="restart" ;; --reap) MODE="reap" ;; --track) MODE="track" ;; @@ -194,6 +213,10 @@ mendix_at() { case "$body" in *mxui*|*mx-name*|*"Mendix"*|*mxclientsystem*) return 0 ;; esac + # A themed index.html can carry none of those words (field project, 2026-10-23: its custom page has + # only a title and
). The runtime's own client bundle cannot be themed away. + code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 4 "http://localhost:$p/mxclientsystem/mxui/mxui.js" 2>/dev/null)" + [ "$code" = 200 ] && return 0 done return 1 } @@ -218,18 +241,18 @@ mendix_at() { # the pin is missing.) owned_app_port() { local want="$ROOT/.docker" name dir port - command -v docker >/dev/null 2>&1 || return 1 + [ -n "$CTR" ] || return 1 while IFS='|' read -r name dir port; do [ "$dir" = "$want" ] || continue [ -n "$port" ] || continue echo "$port"; return 0 done </dev/null | while read -r n; do [ -n "$n" ] || continue printf '%s|%s|%s\n' "$n" \ - "$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$n" 2>/dev/null)" \ - "$(docker inspect -f '{{with index .NetworkSettings.Ports "8080/tcp"}}{{(index . 0).HostPort}}{{end}}' "$n" 2>/dev/null)" + "$("$CTR" inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$n" 2>/dev/null)" \ + "$("$CTR" inspect -f '{{with index .NetworkSettings.Ports "8080/tcp"}}{{(index . 0).HostPort}}{{end}}' "$n" 2>/dev/null)" done) EOF return 1 @@ -313,7 +336,9 @@ pid_alive() { run_local_ports() { local a="" m="" if pick_local_hs; then a="$(hs_field appPort)"; m="$(sed -n 's/^ "adminPort": *\([0-9][0-9]*\).*/\1/p' "$LOCAL_HS" 2>/dev/null | head -1)"; fi - a="${RUN_LOCAL_APP_PORT:-${a:-8080}}" + # No handshake (a `run stop` removes it): the project's first configured APP_PORTS entry, so a + # restart lands where it always runs instead of on 8080, which another project may own. + a="${RUN_LOCAL_APP_PORT:-${a:-${APP_PORTS%% *}}}"; a="${a:-8080}" m="${RUN_LOCAL_ADMIN_PORT:-${m:-$((a + 10))}}" echo "$a $m" } @@ -626,9 +651,10 @@ fi # Confirmed: container->localhost:3001 = 000, container->host.docker.internal:3001 = 200. # Runs in --check too: this is exactly what you need to know BEFORE deciding to test. CTR_WARNED=0 -CONTAINER="$(docker ps --filter 'label=com.docker.compose.service=mendix' --format '{{.Names}}' 2>/dev/null | head -1)" +CONTAINER="" +[ -n "$CTR" ] && CONTAINER="$("$CTR" ps --filter 'label=com.docker.compose.service=mendix' --format '{{.Names}}' 2>/dev/null | head -1)" if [ -n "$CONTAINER" ] && [ $MOCK_REQUIRED -eq 1 ] && [ $MOCK_OK -eq 0 ]; then - FROM_CTR="$(docker exec "$CONTAINER" sh -c \ + FROM_CTR="$("$CTR" exec "$CONTAINER" sh -c \ "curl -s -o /dev/null -w '%{http_code}' --max-time 4 '$MOCK_HEALTH_URL'" 2>/dev/null)" if [ "$FROM_CTR" = "000" ] || [ -z "$FROM_CTR" ]; then CTR_WARNED=1 @@ -750,8 +776,9 @@ if [ $APP_FOUND -ne 0 ]; then # `mxcli docker run` fail with a daemon error the agent then reports as "cannot start Docker". # A cloud container ships the docker CLI with no daemon; that is normal, not a blocker. # Bounded: a stopped Docker Desktop can leave `docker info` silent for minutes. - _RT=docker; command -v docker >/dev/null 2>&1 || _RT=podman - if ! command -v "$_RT" >/dev/null 2>&1; then _DK=1; else + _RT="$CTR" + if [ "$APP_LANE" = local ]; then _DK=3 + elif [ -z "$_RT" ] || ! command -v "$_RT" >/dev/null 2>&1; then _DK=1; else "$_RT" info >/dev/null 2>&1 & _dk_pid=$!; _dk_w=0; _DK= while kill -0 "$_dk_pid" 2>/dev/null; do [ "$_dk_w" -ge 15 ] && { kill "$_dk_pid" 2>/dev/null; _DK=2; break; } @@ -759,7 +786,42 @@ if [ $APP_FOUND -ne 0 ]; then done [ -n "$_DK" ] || { wait "$_dk_pid"; _DK=$?; } fi - if [ "$_DK" != 0 ]; then + [ "$_DK" != 0 ] && [ "$APP_LANE" = container ] && { bad "App down, MXTK_APP_LANE=container, and no container runtime answers ($([ -n "$_RT" ] && echo "$_RT info failed" || echo 'none installed'))."; exit 1; } + # LOCAL LANE: start the app without any container. Measured with mxcli v0.26.0 (2026-10-23): a + # split model (mprcontents/, 3563 units) served by run --local --watch for hours stayed split and + # git-clean. Older builds consolidated it, so the unit count is compared before and after anyway. + if [ "$_DK" != 0 ] && [ -n "$MXCLI" ]; then + _U0=$(find "$MODEL_DIR/mprcontents" -name '*.mxunit' 2>/dev/null | wc -l | tr -d ' ') + check_run_local_ports || { bad "App not serving yet — this project's own runtime holds its port. Re-run --check when it answers."; exit 1; } + read -r _LA _LM <<<"$(run_local_ports)" + # mxbuild --serve listens on 6543 unless told otherwise, so a second project's local lane would + # collide with the first. RUN_LOCAL_SERVE_PORT (stack.conf) wins; else the first free port from 6543. + _LS="${RUN_LOCAL_SERVE_PORT:-}" + if [ -z "$_LS" ]; then + for _p in $(seq 6543 6599); do mxtk_port_holder "$_p" >/dev/null 2>&1 || { _LS=$_p; break; }; done + fi + echo "→ App is down. Starting it without a container: mxcli run --local --watch --detach (:$_LA, admin :$_LM, build :${_LS:-6543})..." + [ "$_DK" = 3 ] || echo " (no container runtime answered; the local lane needs none)" + T0=$(date +%s) + "$MXCLI" run --local --watch --detach -p "$MPR" --app-port "$_LA" --admin-port "$_LM" ${_LS:+--serve-port "$_LS"} >"$LOCAL_LOG" 2>&1 + LOCAL_RC=$? + if [ $LOCAL_RC -ne 0 ]; then + bad "mxcli run --local failed (rc=$LOCAL_RC) — see $LOCAL_LOG"; tail -20 "$LOCAL_LOG" + echo " A missing database is the usual cause: it must exist before the first run (createdb, or --db-type hsqldb)." + exit 1 + fi + _U1=$(find "$MODEL_DIR/mprcontents" -name '*.mxunit' 2>/dev/null | wc -l | tr -d ' ') + if [ "$_U0" -gt 0 ] && [ "$_U1" -lt "$_U0" ]; then + warn "this mxcli consolidated the split model ($_U0 → $_U1 mprcontents units): a build artifact." + warn " → never commit it; restore with: git checkout -- $(basename "$MPR") mprcontents/" + fi + if prove_app; then + APP_FOUND=0; record_served + ok "App serving on :$APP_PORT in $(( $(date +%s) - T0 ))s (ownership $APP_OWNERSHIP, $APP_SOURCE) — --watch keeps it current" + else + bad "mxcli run --local returned, but no Mendix of this project answers over HTTP — see $LOCAL_LOG"; exit 1 + fi + elif [ "$_DK" != 0 ]; then check_run_local_ports || { bad "App not serving yet — this project's own runtime holds its port. Re-run --check when it answers."; exit 1; } bad "App down and no Docker/Podman reachable, so this script cannot build the app container." echo " Normal in a cloud container, and fine on a desktop without one. Run the app without it:" @@ -769,6 +831,9 @@ if [ $APP_FOUND -ne 0 ]; then echo " Snapshot first — mxcli run --local consolidates a split-model .mpr. Then re-run with --check." exit 1 fi +fi +# CONTAINER LANE — still down after the local lane, so a runtime answered and the lane allows it. +if [ $APP_FOUND -ne 0 ]; then if [ -z "$MXCLI" ]; then bad "App down and no mxcli found (neither ./mxcli nor on PATH) — cannot build." exit 2 From a989a9d38d06e353244d54f0286b988acd8402e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 20:40:24 +0000 Subject: [PATCH 2/2] fix(test-stack-up): mxui.js fallback must be JavaScript; fixtures for the local lane (PR #568 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mendix_at's mxui.js fallback accepted any 200, so an SPA catch-all (Express/Vite answering index.html on every path) read as Mendix — the false green the function exists to prevent. New mxui_js_at(): 200, a JavaScript content type, and a body that does not open with `<`. - Local lane with no mxcli now says "no mxcli found" and how to fix it, not "no Docker/Podman". - Fixtures T17 (SPA negative + themed-index positive), T18 (--local with a stubbed mxcli: --serve-port passed, app proven by its run-local handshake), T19 (--local, no mxcli). T17 and T19 fail on 09809666; all pass now (66/66). - Usage header line, app-restart.sh mendix_at comment, CHANGELOG bullet and credit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr --- CHANGELOG.md | 3 +- project-bin/app-restart.sh | 3 +- project-bin/test-stack-up.sh | 27 ++++++++---- tests/wave2/test-stack-up-local.sh | 69 ++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d21164d8..86ac263e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m `docs/RELEASE-PROCESS.md` §8. ## Unreleased -new(test-stack-up, check-requirements): a local lane — a down app starts with `mxcli run --local --watch --detach` (auto when no container runtime answers, or `--local` / `MXTK_APP_LANE=local`), free mxbuild serve port per project, restarts on the project's own APP_PORTS; container calls follow MXCLI_CONTAINER_CLI / docker (Desktop, colima, OrbStack) / podman; a missing runtime is no longer a missing requirement; a themed index.html no longer hides a Mendix app (refs #525) - +- new(test-stack-up, check-requirements): a local lane — a down app starts with `mxcli run --local --watch --detach` (auto when no container runtime answers, or `--local` / `MXTK_APP_LANE=local`), free mxbuild serve port per project, restarts on the project's own APP_PORTS; container calls follow MXCLI_CONTAINER_CLI / docker (Desktop, colima, OrbStack) / podman; a missing runtime is no longer a missing requirement; a themed index.html no longer hides a Mendix app: `/mxclientsystem/mxui/mxui.js` counts only when served as JavaScript, so an SPA catch-all answering 200 HTML on every path stays not-Mendix (refs #525). Fixture `test-stack-up-local.sh` T17–T19. Field run: macOS with Docker Desktop removed and colima running, local-lane cold start 76 s then 43 s, five apps side by side — MendixMau - fix(progress-report): **`progress-report.py` no longer crashes with `TypeError: '<' not supported between instances of 'dict' and 'dict'` when two exec'd scripts share a leading number** (#513). `script_phases()` sorted `(number, phase dict)` pairs, so `10-data.mdl` and `10b-data-flows.mdl` (or one script logged under two paths), both matched to a plan phase, compared the dicts and every `exec.sh` run ended with `WARN: progress-report.html not updated`. It now sorts on (number, script name) only; an unmatched numbered script still takes the phase of the nearest lower-numbered matched one, ties broken by name. Fixture: `test-progress-report.sh` T17 (fails 3/3 on master). — jopterhorst (#513) - new(canvas): **`project-bin/mf-canvas-facts.py` prints how a microflow or nanoflow lies on the Studio Pro canvas: labels on top of each other, lines through objects, note lines leaving the side that faces away, flows that run right-to-left or curl, captions wider than their shape, and decision captions that ask the opposite of their true branch (SENSE).** It prints facts, one line each, and no verdicts; read-only (it reads units through `mxpatch.py`), exit 2 when a flow could not be read. `verify-module.sh` runs it per module as an info rung (1c, log `14-canvas-facts.log`; a missing script, Python or `.mpr`, an exit 2 or a timeout is an INFO row reading NOT MEASURED, never a gate). Fixture `tests/wave2/test-mf-canvas-facts.sh` runs on a model mxcli v0.27.0 wrote (units and `.mpr` captured); `test-verify-module-446.sh` gains V8 for the rung. Calibration is from Studio Pro 10.24 screenshots, not verified on Studio Pro 11. Field evidence, measured 2026-10-21 on one Mendix 10.24 app: 1,588 flows read in one pass; on the 36 flows an mxcli-built branch added or changed in one module, OVERLAP 19, LINE 27, NOTE-SIDE 17, LEFTWARD 3, TURN 4, SENSE 9 before repair — saingge - learn(preflight, teamserver-alignment): **a flow's return-type change and its callers' update go in one script (STOP row 33), and Studio Pro 10.24.18's "Merge feature branch" merges `origin/`, not the local branch.** On a copy of a Mendix 10.24.18 app with mxcli v0.27.0, the first script alone left `mx check` with CE0112 at a caller stored the way Studio Pro stores a call, while `check` and `exec` stayed silent; Studio Pro's own git log shows the merge of the remote branch. Text only; baseline-routed, +101 words. — saingge diff --git a/project-bin/app-restart.sh b/project-bin/app-restart.sh index 46900b92..e28c8d26 100755 --- a/project-bin/app-restart.sh +++ b/project-bin/app-restart.sh @@ -106,7 +106,8 @@ pid_alive() { case "$(sed 's/.*) //' "/proc/$1/stat" 2>/dev/null | cut -d' ' -f1)" in Z) return 1 ;; esac ;; esac } -# Mendix identification and the bundle check. Same markers as test-stack-up.sh's mendix_at(). +# Mendix identification and the bundle check. Same markers as test-stack-up.sh's mendix_at(), but NOT +# its mxui_js_at() fallback, so an app with a themed index.html reads not-Mendix here; port that if it bites. mendix_at() { local p="$1" path code body for path in /login.html /index.html; do diff --git a/project-bin/test-stack-up.sh b/project-bin/test-stack-up.sh index a18e1b78..fe12f83e 100755 --- a/project-bin/test-stack-up.sh +++ b/project-bin/test-stack-up.sh @@ -25,7 +25,7 @@ # # Usage: # bin/test-stack-up.sh --check # report only, change nothing. Exit 0 iff required deps are up. -# bin/test-stack-up.sh # report, then bring up what is missing (may run a Docker build) +# bin/test-stack-up.sh # report, then bring up what is missing (a container build, or the local lane when no runtime answers) # bin/test-stack-up.sh --no-docker # bring up auxiliaries only; never build. "SP is already running it". # bin/test-stack-up.sh --local # app down: start it with `mxcli run --local --watch --detach`, never a container # bin/test-stack-up.sh --restart # skip the hot reload; rebuild and restart the container (old path) @@ -215,11 +215,21 @@ mendix_at() { esac # A themed index.html can carry none of those words (field project, 2026-10-23: its custom page has # only a title and
). The runtime's own client bundle cannot be themed away. - code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 4 "http://localhost:$p/mxclientsystem/mxui/mxui.js" 2>/dev/null)" - [ "$code" = 200 ] && return 0 + # It must BE JavaScript: an SPA catch-all (Express static fallback, Vite dev server) answers 200 + # with its index.html on every path, so a bare 200 was the Express false green above again + # (reproduced, PR #568 review). JS content type, and a body that does not open with `<`. + mxui_js_at "$p" && return 0 done return 1 } +# mendix_at fallback: the runtime client bundle, served as JavaScript. +mxui_js_at() { + local u="http://localhost:$1/mxclientsystem/mxui/mxui.js" meta first + meta="$(curl -s -o /dev/null -w '%{http_code} %{content_type}' --max-time 4 "$u" 2>/dev/null)" + case "$meta" in 200\ *[Jj]ava[Ss]cript*|200\ *[Ee]cma[Ss]cript*) ;; *) return 1 ;; esac + first="$(curl -s --max-time 4 "$u" 2>/dev/null | head -c 512 | tr -d ' \t\r\n' | cut -c1)" + [ -n "$first" ] && [ "$first" != "<" ] +} # Ask Docker which port OUR container publishes, instead of scanning a guessed list. # @@ -822,13 +832,12 @@ if [ $APP_FOUND -ne 0 ]; then bad "mxcli run --local returned, but no Mendix of this project answers over HTTP — see $LOCAL_LOG"; exit 1 fi elif [ "$_DK" != 0 ]; then + # Reached only with no mxcli: with one, the local lane above ran. check_run_local_ports || { bad "App not serving yet — this project's own runtime holds its port. Re-run --check when it answers."; exit 1; } - bad "App down and no Docker/Podman reachable, so this script cannot build the app container." - echo " Normal in a cloud container, and fine on a desktop without one. Run the app without it:" - echo " Studio Pro: Run Locally. No Studio Pro:" - echo " ./mxcli run --local --watch -p $(basename "$MPR") # flags: skills/cloud-dev-environment.md" - echo " This script then sees it as yours (verified) and --watch keeps it current." - echo " Snapshot first — mxcli run --local consolidates a split-model .mpr. Then re-run with --check." + if [ "$_DK" = 3 ]; then bad "App down, local lane asked for, and no mxcli found (neither ./mxcli nor on PATH) — cannot start it." + else bad "App down, no container runtime answers, and no mxcli found (neither ./mxcli nor on PATH) — cannot start it."; fi + echo " Put mxcli in the project root (./mxcli) or on PATH and re-run; it starts the app without a container." + echo " Or run it yourself — Studio Pro: Run Locally — then re-run with --check; this script sees it as yours." exit 1 fi fi diff --git a/tests/wave2/test-stack-up-local.sh b/tests/wave2/test-stack-up-local.sh index 5dec473b..af19b7e9 100755 --- a/tests/wave2/test-stack-up-local.sh +++ b/tests/wave2/test-stack-up-local.sh @@ -23,6 +23,10 @@ # loop (here the fake app itself, behind a dead handshake — Studio Pro's runtime in the field) → # exit 4, "INSTRUMENT BLOCKED", the holder named with its pid, and it is still alive afterwards. # +# T17-T19 (PR #568 review): the mxui.js fallback rejects an SPA catch-all (200 HTML on every path) +# and keeps a themed Mendix index; --local with a stubbed mxcli passes --serve-port and is proven by +# its handshake; --local with no mxcli names the missing mxcli. +# # usage: test-stack-up-local.sh /path/to/test-stack-up.sh # No pipefail: every assertion is `printf '%s' "$OUT" | grep -q …`, and grep -q exits on the first match — # with pipefail the printf's SIGPIPE on a long $OUT then failed a passing assertion (seen on T2). @@ -345,6 +349,71 @@ kill -0 "$REUSED" 2>/dev/null && ok "reused pid (start time mismatch) untouched" kill -0 "$DECOY14" 2>/dev/null && ok "untracked look-alike untouched" || bad "untracked process killed" [ "$(cut -f1 "$BG" | tr '\n' ' ')" = "$YOUNG " ] && ok "record pruned to the live entry" || bad "record not pruned" "$(cat "$BG")" +# srv_any : spa = every path answers 200 text/html (an Express/Vite +# catch-all); files = a plain static server over . +srv_any() { + local pf="$WORK/$1.port" + "$PY" -I - "$3" "$4" "$pf" >/dev/null 2>&1 <<'EOF2' & +import functools, http.server, sys +mode, root, pf = sys.argv[1:4] +class Spa(http.server.BaseHTTPRequestHandler): + def do_GET(self): + b = b'Shop
' + self.send_response(200); self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b) + def log_message(self, *a): pass +H = Spa if mode == "spa" else functools.partial(http.server.SimpleHTTPRequestHandler, directory=root) +s = http.server.ThreadingHTTPServer(("127.0.0.1", 0), H) +open(pf, "w").write(str(s.server_address[1])) +s.serve_forever() +EOF2 + PIDS="$PIDS $!"; printf -v "$2" '%s' "$!" + for _ in $(seq 1 50); do [ -s "$pf" ] && break; sleep 0.1; done + printf -v "$1" '%s' "$(cat "$pf" 2>/dev/null)" +} + +echo "== T17 (PR #568 review): mxui.js fallback — an SPA catch-all is NOT Mendix; a themed Mendix index still is ==" +srv_any SPA SPAPID spa "$WORK" +P="$(mkproj t17 root)"; printf 'APP_PORTS="%s"\n' "$SPA" >> "$P/.claude/loop/stack.conf" +OUT="$(run "$P")"; RC=$? +[ "$(envv "$P" APP_PORT)" != "$SPA" ] && ! printf '%s' "$OUT" | grep -q ":$SPA matched by scan" \ + && ok "SPA catch-all (200 text/html on every path) not identified as Mendix" || bad "SPA catch-all identified as Mendix (rc=$RC)" "$OUT" +mkdir -p "$WORK/themed/mxclientsystem/mxui" +printf 'Shop
\n' > "$WORK/themed/index.html" +printf '\n (function(){window.mx=window.mx||{};})();\n' > "$WORK/themed/mxclientsystem/mxui/mxui.js" +srv_any THM THMPID files "$WORK/themed" +P="$(mkproj t17b root)"; printf 'APP_PORTS="%s"\n' "$THM" >> "$P/.claude/loop/stack.conf" +OUT="$(run "$P")"; RC=$? +[ "$(envv "$P" APP_PORT)" = "$THM" ] && [ "$(envv "$P" APP_OWNERSHIP)" = unverified ] \ + && ok "themed index.html + mxui.js served as JavaScript -> found (scan, unverified)" || bad "themed Mendix not found (rc=$RC)" "$OUT" + +echo "== T18 (PR #568 review): --local, stubbed mxcli -> starts with --serve-port, found by its handshake ==" +P="$(mkproj t18 root)" +cat > "$P/mxcli" < "$P/mxcli.args" +mpr=""; while [ \$# -gt 0 ]; do [ "\$1" = -p ] && { mpr="\$2"; shift; }; shift; done +d="\$(cd "\$(dirname "\$mpr")" && pwd)"; mkdir -p "\$d/.mxcli" +sed -e 's/^ "pid": [0-9]*/ "pid": $WATCH_PID/' -e 's/^ "appPort": [0-9]*/ "appPort": $APP/' \ + -e 's/^ "adminPass": "[^"]*"/ "adminPass": "$ADMIN_MARK"/' "$GOLDEN" > "\$d/.mxcli/run-local.json" +EOF2 +chmod +x "$P/mxcli" +OUT="$( (cd "$P" && LOGDIR="$WORK" bash bin/test-stack-up.sh --local 2>&1) )"; RC=$? +ARGS="$(cat "$P/mxcli.args" 2>/dev/null)" +case "$ARGS" in "run --local --watch --detach -p "*"--serve-port 65"[4-9][0-9]*) ok "mxcli run --local --watch --detach … --serve-port given" ;; *) bad "mxcli not started as expected: '$ARGS'" "$OUT" ;; esac +printf '%s' "$OUT" | grep -q "App serving on :$APP .*ownership verified, run-local" && ok "app proven on :$APP via the run-local handshake" || bad "app not proven via handshake (rc=$RC)" "$OUT" +[ "$(envv "$P" APP_SOURCE)" = run-local ] && [ "$(envv "$P" APP_OWNERSHIP)" = verified ] && ok "stack.env: verified run-local" || bad "stack.env wrong" "$(cat "$P/.claude/loop/stack.env" 2>&1)" + +echo "== T19 (PR #568 review): --local with no mxcli -> says no mxcli, not 'no Docker' ==" +if command -v mxcli >/dev/null 2>&1; then + echo " [skip] T19: an mxcli is on PATH here" +else + P="$(mkproj t19 root)" + OUT="$( (cd "$P" && LOGDIR="$WORK" bash bin/test-stack-up.sh --local 2>&1) )"; RC=$? + [ "$RC" -ne 0 ] && printf '%s' "$OUT" | grep -q "no mxcli found" && ! printf '%s' "$OUT" | grep -q "Docker/Podman" \ + && ok "exit $RC, names the missing mxcli" || bad "wrong no-mxcli message (rc=$RC)" "$OUT" +fi + echo echo "stack-up-local: $PASS passed, $FAIL failed" [ "$FAIL" -eq 0 ]