diff --git a/CHANGELOG.md b/CHANGELOG.md index eabec440..6daaa946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +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: `/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(lint): **the toolkit's CONV020 (page actions give user feedback) is now `MXTK-UX003` (`mxtk_ux003_action_user_feedback.star`), because mxcli v0.28.0 ships a built-in CONV020 ("Created object never persisted", mendixlabs/mxcli#1518) and has no duplicate-ID guard.** Captured on a public model (`tests/wave2/fixtures/lint-conv020-v028/`): v0.28.0 reported two CONV020 findings, one per rule, `-l` listed CONV020 twice, and `CONV020: enabled: false` switched both off. `bin/sync-project.sh` migrates projects: an unedited old copy (md5 of any shipped version, CR-insensitive, or the receipt row) is replaced, `lint-baseline.json` findings whose message is the toolkit rule's are rekeyed to MXTK-UX003 (the built-in's stay CONV020), and a `lint-rules-disabled.txt` line follows the file. A lint-config opt-out of CONV020 is carried over too: a `lint-rules-disabled.txt` line for the new file keeps MXTK-UX003 off (the key stays; on v0.28.0 it switches off only the built-in). It is read the way mxcli v0.28.0 reads it (yaml.v3, captured per spelling): `enabled:` `false`/`False`/`FALSE` or `n`/`no`/`off` in any of yaml.v3's casings (`No`, `NO`, `Off`, `OFF`…, plain or quoted), as a direct child of a bare, single- or double-quoted `CONV020` key or in its flow map, from the first of `.claude/lint-config.yaml`, `lint-config.yaml`, `.lint-config.yaml`; an `enabled` nested under `options:` and a quoted `"false"` (mxcli fails to load that config) are not opt-outs. An edited copy is left in place with a warning and manual steps (with the `lint-rules-disabled.txt` line to keep such an opt-out), and the new rule is not installed beside it. A counts-only baseline, `lint-config.yaml` keys and `PROJECT.md` lines that name CONV020 get a NOTE, not a guess, printed once. New toolkit rules take an `MXTK-` id (`lint-rules/README.md` "Rule IDs"). After any mxcli upgrade, `lint-gate.sh --update-baseline` now records `mxcli_version`, and `bin/doctor.sh` prints the re-baseline hint when the installed mxcli differs from the recorded one (a baseline with no recorded version gets a note, no command; `skills/lint-that-actually-runs.md`). Decision #12 — mxcli v0.28.0 test pass - process(routing, mdl-agent): **the mdl helper no longer preloads the kind-specific MDL skills at Stage 5; it loads them per script through `bin/mdl-rules.sh`** — lazy-load PR 2b. `learned-microflow-patterns.md`, `microflow-preflight.md` and `ui-preflight-pages.md` move to on-demand; `learned-mdl-preflight.md`, `design-spacing.md`, `learned-detection-gaps.md`, `security-is-not-a-later-script.md` and `retesting-learned-rules.md` become on-demand for mdl only (`mdl?`), unchanged for every other role; a new baseline `mdl-rules` row (Stages 5 and 6, so a Stage 6 mdl pack keeps the detection-gaps and retesting rules; mdl Stage 6 preload 9,476 words) replaces them. Baseline 72,891 → 63,877 words; mdl Stage 5 preload 29,388 → 14,600 (per script, after reading the `mdl-rules` output, the helper reads ≈21.4k words for page only, ≈22.5k for microflow+access, ≈28.1k for page+microflow, ≈29.5k for all kinds: ≈0–8k saved by kind; the A/B covered rule sufficiency with kinds pre-selected, not step 4a) (the 3,347-word core of `bin/mdl-rules.sh` counted, like the runbook slice). The mdl stub's workflow step 4a and report line ask for "STOP rows for kinds K: n/n checked". `context-pack.sh`'s "Rules" list names `/bin/mdl-rules.sh ` with the step's kinds (page / microflow / entity) — after this change the only route `microflow-preflight`, `security-is-not-a-later-script`, `design-spacing`, `learned-detection-gaps` and `retesting-learned-rules` have to a pack-fed helper (fixture: a page step and a logic step). `gate-check.sh` build-ready item 2 accepts `bin/mdl-rules.sh` as well as `ui-preflight-pages`, so a project synced after this change does not flip to FAIL (fixture: both layouts). No skill text changed — lazy-load plan, option C (maintainer) - new(mdl-rules): **`bin/mdl-rules.sh | --for ` prints the MDL rules one script needs, by the kinds of statement it writes** — lazy-load PR 2a. `bin/lib/mdl-rule-kinds.tsv` maps page / microflow / access / workflow / entity / any to sections located by file and heading text (never line numbers): the STOP rows by their `Kind` column, the kind-specific gotchas, `learned-microflow-patterns.md`, `microflow-preflight.md`, `ui-preflight-pages.md`, `design-spacing.md` §1–3, the detection-gaps register rows by construct, `security-is-not-a-later-script.md` and `retesting-learned-rules.md`. Output is the core (Step 0, the `any` STOP rows, gotchas, final self-check: 3,347 words) plus the kind sections, closed by "Rules for kinds K: STOP rows n/n listed" (page 10,223 words, microflow 10,186, access 4,730, entity 5,183, workflow 3,732), against the 29,388 words the mdl helper preloads at Stage 5 today. `--for` reuses exec.sh's statement classifier (`exec.sh --subjects`; Java actions and mappings count as microflow, demo users as access) and exits 1 if it fails; a heading that is gone fails loudly, never prints empty. `skills/generated/rules-.md` are the same output for harnesses that read files (Copilot, Cursor, Windsurf); `--check` (CI) fails on drift, on any STOP row, gotcha, register row or `##` section that reaches no kind (a section left out on purpose needs an `exclude` row with its reason), and on any register ERE that matches no row by itself, and on an unclosed code fence in a file it reads. No routing change yet: PR 2b demotes the preloads. Behaviour suite: `build-packs.sh --rules ""` expands the script in a pack. Fixture `tests/wave2/test-mdl-rules.sh` — lazy-load plan, option C (maintainer) 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/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 55208809..fe12f83e 100755 --- a/project-bin/test-stack-up.sh +++ b/project-bin/test-stack-up.sh @@ -25,8 +25,9 @@ # # 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) # 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,9 +213,23 @@ 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. + # 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. # @@ -218,18 +251,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 +346,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 +661,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 +786,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,16 +796,53 @@ 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 + # 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 +# 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 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 ]