Plans (docs/plans/): - 2026-07-01-23-47-py-hxprobe-httpx.md — initial httpx probe design - 2026-07-02-09-32 through 14-05 — standalone project, toolchain, usage doc + Makefile, file input (-f), simplification pass, run-summary footer Summaries (docs/summaries/): one per completed feature, recording what was actually built, deviations from the plan, and verification steps Explanations (docs/explanations/): two deep-dives written during review — hxprobe concurrency model and worst-exit-code + render-loop analysis Usage (docs/usage/hxprobe.md): overview with pointer to hxprobe/USAGE.md for the full runnable reference Walkthrough (docs/py-latprobe-walkthrough.md): narrative tour of the latprobe Python package for interview / code-review context CHANGELOG.md: entries for all hxprobe features (toolchain, usage doc, file input, simplification, run-summary footer) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.4 KiB
hxprobe: end-of-run summary footer for multi-URL runs
Context
A single scalar exit code is inherently lossy when probing multiple URLs with
different failure classes: today run() returns the numeric max across all
URLs (e.g. DNS-fail on one URL + TLS-fail on another → exit 5, and the DNS
failure is invisible in the code). The failures are all printed per-URL, but
there's no consolidated view — for many URLs you must scroll and eyeball each
block to know what happened overall.
The exit code's job is a coarse pass/fail + severity hint for scripts, and it's
a deliberate cross-implementation contract shared with latprobe and the
Go version (documented in hxprobe/USAGE.md:475-488, asserted by 13 tests in
tests/test_cli.py). So we keep the worst-code exit unchanged and instead give
humans the full picture the scalar can't: an end-of-run summary footer that
tallies every URL's outcome and shows how the exit code was derived.
Decisions (confirmed with the user):
- Exit code: unchanged (worst/highest severity across URLs).
- Summary: text footer, multi-URL only (
len(urls) > 1). Single-URL text output stays byte-identical; JSON output stays a bare array (unchanged).
Design
Per-URL classification (reuses existing helpers)
Each URL gets one "worst outcome code" using the existing mapping — no new severity scheme:
- start at
EXIT_OK; - for each failed sample:
code = max(code, _phase_code(r.fail_phase))(_phase_codeatcli.py:31); - if
--fail: for each succeeded sample withstatus_code >= 400:code = max(code, EXIT_HTTP).
A URL is "ok" iff its code is EXIT_OK, else "failed" and bucketed by its code.
A partially-failed URL (some samples ok, some failed) classifies by its worst
sample — consistent with how its own block and the global exit code already
behave.
Footer format (text, only when len(urls) > 1)
Printed once after the last URL block, before return worst. Failure classes
use the same ✗ bullet style as _print_failure_summary (cli.py:219-224).
The final → exit N (label) line explicitly ties the tally to the returned
code — directly answering "why is the exit code what it is". Example (3 URLs):
https://example.com (200)
... phase rows ...
http://no.such.host.invalid (FAILED)
✗ dns: [Errno 8] nodename nor servname provided
https://self-signed.badssl.com (FAILED)
✗ tls: certificate verify failed
─────────────────────────────────────────────────
Summary: 3 URLs — 1 ok, 2 failed
✗ dns : 1
✗ tls : 1
→ exit 5 (tls)
All-ok multi-URL run → Summary: 3 URLs — 3 ok, no ✗ lines, → exit 0 (ok).
Changes
All in hxprobe/hxprobe/cli.py unless noted.
-
Reverse label map next to the exit-code constants (
cli.py:16-32): a small_EXIT_LABELS: dict[int, str]mappingEXIT_DNS→"dns",EXIT_CONNECT→"connect",EXIT_TIMEOUT→"timeout",EXIT_TLS→"tls",EXIT_HTTP→"http",EXIT_OK→"ok". (Inverse of the existing forward mapping; kept explicit for readability, matching the codebase's style.) -
Refactor the accumulation loop (
cli.py:454-462) to compute a per-URL code and fold it intoworst, collectingurl_codes: list[int](one per URL, index-aligned withurls). This also unifies the two "running max" idioms flagged indocs/explanations/2026-07-02-13-25-hxprobe-worst-exit-code-and-render-loop.md(if c > worstvsmax(...)) into one — a small simplification bonus. -
New
_print_run_summary(urls, url_codes, worst, out)helper (near the other_print_*renderers): builds the ok/failed tally + per-class counts fromurl_codesand writes the footer. No-op guard is the caller'slen(urls) > 1check. -
Call site after the loop (
cli.py:470-475): in text mode only (if not ns.json_out and len(urls) > 1:), call_print_run_summary(...)beforereturn worst. JSON path untouched — stilljson.dumps(json_items)as a bare array.
Tests (hxprobe/tests/test_cli.py)
Add hermetic tests (reuse the existing _OKHandler/_start_server/_invoke
harness and _free_port for a refused connection):
- multi-URL mixed — one OK server URL + one
http://127.0.0.1:<free>(connection refused): assertcode == EXIT_CONNECT, and the footer strings are present ("Summary: 2 URLs","1 ok","1 failed","connect : 1","→ exit 3"). - multi-URL all ok — two OK URLs: assert
code == EXIT_OKand"Summary: 2 URLs — 2 ok"present; verify no"✗"in the footer region. - single URL has NO footer — one OK URL: assert
"Summary:"NOT inout(locks the multi-URL-only rule).
Backward-compat guard: the footer contains no "200" substring, so the
existing test_reads_urls_from_file assertion out.count("200") == 2 still
holds; all 13 existing exit-code tests are unaffected (worst-code unchanged).
Docs / project conventions (per CLAUDE.md)
- Copy this approved plan into
docs/plans/<yyyy-mm-dd-hh-mm>-hxprobe-run-summary-footer.mdas the first implementation step (before code) — see thefeedback_plan_mode_docs_plansmemory note. hxprobe/USAGE.md: add a short "Multi-URL summary footer" subsection with a real captured example; add a sentence to the Exit-codes section noting the footer shows the per-class breakdown behind the scalar. Exit-code table itself is unchanged.docs/usage/hxprobe.md: one-line mention if it lists features.- New
docs/explanations/<ts>-hxprobe-run-summary-footer.mdis optional; the existing worst-exit-code explanation can get a short "Update:" pointer. CHANGELOG.md: new timestamped entry.docs/summaries/<ts>-hxprobe-run-summary-footer.md: implementation summary.
Verification
cd hxprobe && .venv/bin/python -m pytest tests/test_cli.py tests/test_probe.py -q— all existing + new tests pass..venv/bin/ruff check hxprobe/— clean.- Manual, capturing exit codes:
.venv/bin/python -m hxprobe https://example.com https://example.org # footer, exit 0 .venv/bin/python -m hxprobe https://example.com http://no.such.host.invalid; echo $? # footer w/ dns:1, exit 2 .venv/bin/python -m hxprobe https://example.com # single URL: NO footer, unchanged .venv/bin/python -m hxprobe --json https://example.com https://example.org # bare JSON array, NO footer