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

SHATitle
0be3458feat: implement mando mock down with idempotent teardown
f8a54bffix: target /__admin/health for wiremock healthcheck
0f85923fix: 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.rsstop_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

  1. DockerClient::connect() + .ping(). On failure return CommandError::Other with a clear message — do not let bollard errors leak unwrapped.
  2. 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.
  3. find_container(&spec.name) — returns Option.
  4. Build Vec<Element> (Header + Spacer) AFTER the find call, not before. The header should reflect what we actually found.
  5. let Some(_) = container else { ... } early return for the not-found case, rendering a StatusRow::Warn. Do not Fail for “nothing to do”.
  6. Wrap the docker action with ctx.cli.buffer.wrap_future(label, fut).await. This drives the spinner and adds the success/fail badge.
  7. 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-328stop_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:

curl -fsS http://localhost:8080/__admin/health > /dev/null

-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.

Diagnostic technique — health probe inspection

When a container's healthcheck fails

docker inspect <container> --format '{{json .State.Health}}'

shows the FailingStreak count and the ExitCode of every probe in Log[].

wget exit codemeaning
0success
4network failure (server not reachable yet — keep waiting)
5SSL verification failure
6username/password authentication failure
8HTTP 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.

Cross-references

Updates to existing notes: