Skip to content

Implement @cleanup rollback, and stop --local displacing the app's after-startup microflow - #113

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

Implement @cleanup rollback, and stop --local displacing the app's after-startup microflow#113
ako merged 3 commits into
mainfrom
claude/mxcli-unit-test-perf-n7ggx8

Conversation

@ako

@ako ako commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Follow-up to #109. Two changes to mxcli test, the second from field feedback.

1. @cleanup rollback is now real

The annotation has documented rollback as its default since the runner shipped, but TestCase.Cleanup was parsed and then used nowhere — every test committed.

That was not an oversight so much as a missing seam. The after-startup runner executes tests inside the startup action, so there is no context the runner owns to open a transaction on. The test endpoint creates that seam, because it builds the IContext each test runs on.

The handler now wraps the call in startTransaction() / rollbackTransaction() when the runner asks for it, in a finally — so a test that throws, the one most likely to leave half-written data, is rolled back too. @cleanup none commits, for when the writes are the point.

Verified against Postgres, not the endpoint's own claim. A suite with one rollback test and one @cleanup none test, run against an emptied table:

PASS the default is rollback — this Person does not survive the run [rolled back]
PASS stating rollback explicitly does the same thing               [rolled back]
PASS @cleanup none commits — the control                           [committed]

$ psql -c 'SELECT firstname FROM "app$person";'
 PersistedProbe        ← 1 row

Same microflow, same run, only the annotation differs. The none case is the control that rules out "nothing persisted for an unrelated reason".

Two failure modes this closes rather than opens:

  • A typo is now a parse error. @cleanup rollbak would otherwise mean "don't roll back" — data left behind, run still green, nothing explaining why. Rejected at parse time so --list catches it and no runtime boots for a file that cannot run correctly. The .mdl and .md parsers are separate code paths and both are covered.
  • A rollback that fails is reported, per test and in a summary. --verbose tags each result [rolled back] / [committed] / [ROLLBACK FAILED]. An endpoint too old to know the parameter is called out separately — plausible under --attach, where the host is whatever mxcli started it.

Behaviour change worth flagging: tests that previously committed now roll back by default. That makes behaviour match the documented contract, but a suite where one test depends on data seeded by an earlier one will start failing; @cleanup none is the escape hatch. Docker is unaffected — it always commits.

2. --local no longer displaces the app's after-startup microflow

Reported from a real project (mxcli-formula1 findings #19):`` a suite passed under --attach and failed under `--local`, with cached-service assertions seeing zero rows. The app loads its cache from an after-startup microflow, and the `--local` runner displaced that microflow with its own.

It was a deliberate choice — a test run wants a known baseline — but it was invisible: the run printed only After-startup set to MxTest.RegisterEndpoint, never that the user's startup logic had been displaced, so the failure looked like a bug in the code under test.

It was also inconsistent: the hosted --test-endpoint path already chained the project's own microflow, which is exactly why the two modes disagreed. --local now chains it too, so a suite behaves the same either way and tests see the app in the state it really boots into. --skip-app-startup opts out for a deterministic empty baseline, and the run always prints which of the two it did:

… (registers the endpoint; runs no tests, then runs your MyModule.ASU_Startup)
… (registers the endpoint; runs no tests; --skip-app-startup, so MyModule.ASU_Startup will NOT run)

Verified with the finding's own shape — a seeding after-startup microflow and a test asserting on its row: PASS chained, FAIL under --skip-app-startup, from an emptied table.

That control initially passed when it should have failed, which surfaced something worth documenting: the startup microflow's writes are not covered by @cleanup rollback — they happen at boot, outside any test's transaction. Now stated in the docs.

Also from the same report (#15)

mxcli test … --list bypassed resolveTestPaths, so a project-relative path resolved for execution but not for listing. Confirmed against the pre-fix binary:

$ mxcli-old test tests/ -p /…/SpikeApp.mpr --list
Error: stat tests/: no such file or directory     ← before
Found 1 test(s): …                                 ← after

Verification

  • Every new test was checked to fail against a stubbed guard (removing startTransaction, defaulting to no-rollback, dropping the client parameter, disabling validation).
  • Both behavioural claims verified against a live Mendix 11.13.0 runtime and its Postgres database, with a within-run control in each case rather than a code revert.
  • Full suite green; merged with current main (Aggregates broke when SET became optional (upstream CI regression) #112) — the fix-issue.md symptom table merged via the union driver with both sides' rows intact and no duplicates.

Docs

CLI help for test, mxcli syntax test, the test-microflows skill, docs-site running-tests, CLAUDE.md, two symptom-table rows, and a worked example at mdl-examples/doctype-tests/cleanup-rollback.test.mdl.

Not addressed

The other two items in findings #15 are outside the test runner and untouched here: the MDL-ODATA01 hint omits Countable / SkipSupported / TopSupported, and .ai-context/skills/ goes stale after an mxcli upgrade with no staleness detection.

🤖 Generated with Claude Code

https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ


Generated by Claude Code

claude added 3 commits August 7, 2026 21:29
The @cleanup annotation has documented rollback as its default since the
runner shipped, but TestCase.Cleanup was parsed and then used nowhere: every
test committed. The after-startup runner had no seam to implement it — tests
execute inside the startup action, so there is no context the runner owns.
The test endpoint creates that seam, because it builds the IContext each test
runs on.

The handler now wraps the call in startTransaction()/rollbackTransaction()
when the runner asks for it, in a finally so a throwing test — the one most
likely to leave half-written data — is rolled back too. @cleanup none commits,
for when the writes are the point.

Verified against Postgres rather than the endpoint's own claim: a suite with
one rollback test and one @cleanup none test, run against an emptied table,
leaves exactly the "none" row behind. Same microflow, same run, only the
annotation differs.

Two failure modes this closes rather than opens:

  - An unknown strategy (@cleanup rollbak) is now a parse error. Treating it
    as "not rollback" would leave the data behind while the run still reported
    a clean pass. Rejected at parse time, so --list catches it and no runtime
    is booted for a file that cannot run correctly. The .mdl and .md parsers
    are separate code paths and both are covered — the first version of this
    only reached one of them.
  - A rollback that fails is reported per test and summarised at the end,
    never swallowed. --verbose tags every result [rolled back] / [committed] /
    [ROLLBACK FAILED]. An endpoint too old to know the parameter is called out
    specifically, since --attach can meet one.

Rollback applies to --local and --attach; Docker keeps committing, and the
docs say so. It matters most under --attach, where the database belongs to
the developer's running app.

Each new test was verified to fail against a stubbed guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Reported from a real project (mxcli-formula1 findings #19): a suite passed
under --attach and failed under --local, with cached-service assertions
seeing zero rows. The app loads its cache from an after-startup microflow,
and the --local runner displaced that microflow with its own.

That was a deliberate choice — a test run wants a known baseline — but it
was invisible. The run printed only "After-startup set to
MxTest.RegisterEndpoint", never that the user's startup logic had been
displaced, so the failure looked like a bug in the code under test.

It was also inconsistent: the hosted --test-endpoint path already chained
the project's own microflow, which is precisely why the two modes disagreed.
--local now chains it too, so a suite behaves the same either way and tests
see the app in the state it really boots into. --skip-app-startup opts out
for a deterministic empty baseline, and the run always prints which of the
two it did:

  … (registers the endpoint; runs no tests, then runs your MyModule.ASU_Startup)
  … (registers the endpoint; runs no tests; --skip-app-startup, so … will NOT run)

Verified live with a seeding after-startup microflow and a test asserting on
its row: PASS chained, FAIL under --skip-app-startup, from an emptied table.
Note the startup microflow's writes are not covered by @cleanup rollback —
they happen at boot, outside any test's transaction.

Also from the same report (#15): mxcli test --list bypassed resolveTestPaths,
so a project-relative path resolved for execution but not for listing.
Confirmed against the pre-fix binary, which fails with "stat tests/: no such
file or directory" on the command that now works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Brings the branch up to date with main (PR #112) so the PR merges cleanly and
CI runs against the current base. No conflicts; the fix-issue.md symptom table
merged via the union driver with both sides' rows intact and no duplicates.
Full suite green on the merged tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
@ako
ako merged commit ec5d388 into main Aug 8, 2026
5 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