Skip to content

Warn when a local test run will recompile a running app's classes - #362

Merged
ako merged 1 commit into
mainfrom
claude/mxcli-unit-test-perf-n7ggx8
Sep 1, 2026
Merged

Warn when a local test run will recompile a running app's classes#362
ako merged 1 commit into
mainfrom
claude/mxcli-unit-test-perf-n7ggx8

Conversation

@ako

@ako ako commented Sep 1, 2026

Copy link
Copy Markdown
Owner

mxcli-formula1 FINDINGS §81. §62's blanking is fixed; the deployment tree has another half that is shared, and this one cannot be fixed — only reported.

First, a correction to the finding, because it reads as contradicting two others. It was measured on a739d2e2, which is the merge of #361 — so the runner works there (71/71 and 34/34, bundle surviving) because that PR fixed it. §81 credits 2c0aa8d2 fix(test): give a local test run its own deployment tree; that is the commit which broke mxcli test --local for every project (mxcli-ledger §150, mxcli-sudoku §51). §81 is a residual finding against the fixed build, not a disagreement with those two.

What is still shared

A local test run recompiles the project's Java into deployment/run/bin, which is the classpath a live mxcli run --local is holding open. Measured directly on a real 11.13 project — after one test run, all 134 class files have new inodes and byte-identical content. Every one deleted and rewritten. A JVM loads classes lazily, so one it has not reached yet can fail afterwards:

java.lang.NoClassDefFoundError: odatapushdown/QueryObject

What that costs is diagnosis, not the breakage. The microflows behind it then answer HTTP 200 with an empty body — not a 500, not an error page — while source-backed resources keep working. Half the app is fine and half returns nothing, which is not a shape that suggests a test run did it. In the reporting project it surfaced as 21 of 34 tests failing in a different app, and 108 log lines went by before the two were connected.

Why this reports rather than fixes

Same constraint as §150: mxbuild's Gradle pass owns the compile, and the deployment directory cannot be moved — mxbuild writes it to <app dir>/deployment and takes no option to change it. There is nowhere else to compile to.

So the collision is named before the build, with the symptom and the remedy:

Warning: `mxcli run --local` is serving this project on port 8080 (pid 4477).
  This test run recompiles the project's Java into deployment/run/bin, which is
  that app's classpath — every class file is rewritten. A class the running app
  has not loaded yet can then fail with NoClassDefFoundError, and the microflows
  behind it answer HTTP 200 with an EMPTY BODY rather than an error, so the app
  looks half-working (mxcli-formula1 §81).
  If anything it serves stops returning data, restart that app.
  The deployment directory cannot be separated: mxbuild always writes it to
  <app dir>/deployment.

It warns rather than refuses. The warm loop exists so an app can stay up while you work on it, and the reporting project runs two apps that way as a matter of course — refusing would break the workflow the feature is for, to prevent something whose remedy is one restart. Neither --attach nor --skip-build builds, so neither warns.

The mistake this nearly shipped, and how it was caught

Detecting the dev loop needs one fact: is a mxcli run --local serving this project? I wrote a new RunState file at .mxcli/run-local.json for it.

Then a real run --local was started to verify the producer half — and the file that appeared was not mine. devLoopHandshake already lives at that exact path, published by every run --local for mxcli constant set --apply, carrying the same pid-liveness check and project identity, plus adminPass and bootConfig.

The new reader parsed it fine, because JSON ignores unknown fields. The new writer would have silently dropped those two keys and broken --apply and --attach. The duplicate is deleted and the existing handshake is read instead — correct, and less code.

Every unit test was green against the duplicate. What caught it was running the producer for real, which is the same lesson #361 was about: assert the artefact, not the option.

Staleness is the feasibility question, not a detail

A run --local that was killed, crashed, or ended by its development licence (§60 — measured lifetimes under six hours) leaves the file behind. A warning driven by the file alone would fire forever, and a warning that is always wrong teaches the reader to skip it. readDevLoopHandshake already refuses a dead pid, a corrupt file and an absent one; the warning inherits that rather than reading the file itself, and a test pins it.

Verification

End-to-end against a genuinely running mxcli run --local, not a fixture:

  • The warning names that app's real pid and port, read from the handshake it actually published.
  • Its handshake still carries adminPass and 9 bootConfig keys after the test run — the clobbering above would have removed both.
  • Control: with the dev loop stopped (the handshake is removed on exit), the same command is silent — 0 warnings.
  • Control: a stale handshake with a dead pid is silent too.

Unit tests cover the message content (port, "recompil", "restart", and the 200 symptom that nobody guesses), plus all three silent cases.

go test ./... green, make vet clean, make check-mdl 458 examples passing.

🤖 Generated with Claude Code

https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ


Generated by Claude Code

§62's blanking is fixed, and the deployment tree has another half that is
shared. A local test run recompiles the project's Java into
`deployment/run/bin`, which is the classpath a live `mxcli run --local` is
holding open. Measured on a real 11.13 project: after one test run all 134
class files have NEW INODES and byte-identical content — every one deleted
and rewritten. A JVM loads classes lazily, so one it has not reached yet
can fail afterwards:

    java.lang.NoClassDefFoundError: odatapushdown/QueryObject

What that costs is diagnosis, not the breakage. The microflows behind it
answer HTTP 200 with an EMPTY BODY — not a 500, not an error page — while
source-backed resources keep working, so half the app is fine and half
returns nothing, which is not a shape that suggests a test run did it. In
the reporting project it surfaced as 21 of 34 tests failing in a DIFFERENT
app, and 108 log lines went by before the two were connected.

mxcli cannot prevent it: mxbuild's Gradle pass owns the compile, and the
deployment directory cannot be moved — mxbuild writes it to
`<app dir>/deployment` and takes no option to change it. So it reports the
collision, which is the part that was missing, and says what the symptom
looks like and that the remedy is restarting that app.

It warns rather than refuses. The warm loop exists so an app can stay up
while you work on it, and the reporting project runs two apps that way as
a matter of course; refusing would break the workflow the feature is for.
Neither --attach nor --skip-build builds, so neither warns.

The fact needed — is a dev loop serving this project? — was already
published. `mxcli run --local` writes devLoopHandshake to
.mxcli/run-local.json for `mxcli constant set --apply`, carrying the pid
liveness check and the project identity this wants. A second state file
was written before that was noticed, and it would have CLOBBERED the
existing one, dropping the admin password and boot config that --apply and
--attach depend on. Reading what is already there is both correct and less
code.

Staleness is the whole feasibility question, not a detail: a `run --local`
killed, crashed, or ended by its development licence (§60, measured
lifetimes under six hours) leaves the file behind, and a warning driven by
the file alone would fire forever — a warning that is always wrong teaches
the reader to skip it. readDevLoopHandshake already refuses a dead pid, a
corrupt file and an absent one.

Verified end-to-end against a real `mxcli run --local`: the warning names
that app's actual pid and port, and the handshake still carries its
adminPass and 9 bootConfig keys afterwards. Controls: with the dev loop
stopped (handshake removed on exit) the same command is silent, and so is
a stale handshake with a dead pid.

Reported as mxcli-formula1 FINDINGS §81.
@ako
ako merged commit 5918d94 into main Sep 1, 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