mando-cli mock down + WireMock healthcheck (2026-05-06)
Two related docker-lifecycle fixes landed in mando-cli on 2026-05-06: a mando mock down implementation that mirrors mock up, and a fix to the WireMock container’s healthcheck endpoint that was causing the container to be reported unhealthy on first start.
For Agents
This note pins the canonical 7-step pattern for any docker-backed lifecycle command in commands/, and the idempotent teardown rule in runtime/docker_client.rs. Use both verbatim when adding a new lifecycle command — do not invent local variants.
Commits
SHA
Title
0be3458
feat: implement mando mock down with idempotent teardown
f8a54bf
fix: target /__admin/health for wiremock healthcheck
0f85923
fix: add missing Down variant to cli::MockAction (companion to 0be3458)
Fix 1 — mando mock down
Files
src/commands/mock.rs — new down() function
src/runtime/docker_client.rs — stop_and_remove made idempotent (404 = success)
src/cli.rs — added MockAction::Down variant
The 7-step pattern for docker-backed lifecycle commands
Canonical shape — copy this for every new lifecycle command
DockerClient::connect() + .ping(). On failure return CommandError::Other with a clear message — do not let bollard errors leak unwrapped.
Resolve the canonical container spec/name from input via the runtime::infra helper for the service. For wiremock that is infra::wiremock_spec_with_port(port) — single source of naming truth. Never hand-build format!("mando-wiremock-{port}") inside a command.
find_container(&spec.name) — returns Option.
Build Vec<Element> (Header + Spacer) AFTER the find call, not before. The header should reflect what we actually found.
let Some(_) = container else { ... } early return for the not-found case, rendering a StatusRow::Warn. Do not Fail for “nothing to do”.
Wrap the docker action with ctx.cli.buffer.wrap_future(label, fut).await. This drives the spinner and adds the success/fail badge.
Match the result. Render exactly one StatusRow per exit path — never two, never zero.
This shape mirrors what mock up already does. New lifecycle commands should diff cleanly against mock.rs::up.
Idempotent teardown rule
docker_client.rs:317-328 — stop_and_remove now treats a 404 from remove_container as success, mirroring how inspect_detail already swallows 404 from inspect_container.
Why this matters
Without 404-as-success, races cause spurious Fail rows even though the end-state is correct:
Two concurrent mando down invocations
Container auto-exit between our find_container and our remove_container
User manually docker rm’d the container between the inspect and the remove
All three end with “container is gone” — which is exactly what the user asked for. We should not lie about a failure.
Anti-patterns observed during implementation
Don't do these
let name: &str = name.into().as_str() — the temporary String from .into() is dropped while still borrowed. Use &str directly through the call chain.
join_all over a sync iterator chain that returns futures from .filter — .filter is sync; the futures it filters on are never polled. Use futures::stream::StreamExt with filter_map if you really need async filtering. Better: avoid the problem entirely via canonical-name resolution (step 2 above) so you never need to filter by name fragment.
Building the Vec<Element> before the operation runs — hides the status until late and forces awkward “now insert at index 1” code when the result lands. Build the header up-front, push status rows as they happen.
Fix 2 — WireMock healthcheck endpoint
Symptoms
After the 2026-04-28 mock-command rollout the wiremock 3.x image (with wget, curl, bash, etc. baked in) was being reported unhealthy. The healthcheck command was:
wget --spider -q http://localhost:8080/__admin
Exit code 8 (HTTP error response).
Root cause
/__admin returns 302 → redirects to /__admin/
/__admin/ returns 404
wget --spider follows the redirect and treats 404 as an error
So the endpoint we picked never returned 200 in the first place — the previous setup had been masking the bug because nothing was actually checking the FailingStreak yet.
Fix
Target /__admin/health (returns 200 OK with {"status":"healthy"}) and switch to curl for crisper failure semantics:
-f makes curl exit non-zero on HTTP errors instead of printing the body, -s is silent, -S keeps error messages on stderr if anything goes wrong. Cleaner than wget --spider for this use case.
shows the FailingStreak count and the ExitCode of every probe in Log[].
wget exit code
meaning
0
success
4
network failure (server not reachable yet — keep waiting)
5
SSL verification failure
6
username/password authentication failure
8
HTTP error response (server reachable, but returned 4xx/5xx)
Exit code 8 specifically tells you “the endpoint exists but is returning the wrong status” — which pinpointed /__admin as the wrong target almost immediately.
mando-cli-docker-lifecycle — the original docker lifecycle wiring; this note formalises the 7-step pattern that mando up and mando down already followed implicitly