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>
5.6 KiB
hxprobe: end-of-run summary footer — summary
Plan: docs/plans/2026-07-02-14-05-hxprobe-run-summary-footer.md.
Trigger: a follow-up to
docs/explanations/2026-07-02-13-25-hxprobe-worst-exit-code-and-render-loop.md
— does a single worst-code exit even make sense across multiple URLs with
different possible errors? The answer landed on: keep the scalar exit code
(it's a documented cross-implementation contract with latprobe/Go — see
hxprobe/USAGE.md's Exit codes table, asserted by 13 existing tests), and
instead add the missing visibility as an end-of-run summary footer.
What changed
All in hxprobe/hxprobe/cli.py unless noted, matching the plan exactly (no
deviations):
-
_EXIT_LABELS(new, next to_PHASE_EXIT): inverse mapping from exit code → short label (ok,dns,connect,timeout,tls,http), used to render the footer's→ exit N (label)line. -
Accumulation loop refactor: previously computed a single running
worstdirectly inside the per-URL loop via two different idioms (if c > worst: worst = cfor network failures,max(worst, EXIT_HTTP)for--fail). Now each URL first gets its owncode(EXIT_OKfolded up viamax()across its failed samples and, if--fail, its ≥400 successes), appended to a newurl_codes: list[int](index-aligned withurls), and then folded intoworst = max(worst, code). Unifies both idioms into one. -
_print_run_summary(urls, url_codes, worst, out)(new helper, next to_print_failure_summary): writes a separator line, anN URLs — X ok[, Y failed]header, one✗ {label:<8}: {count}line per non-OK class present (first-seen order, same dedup style as_summarize_failures), and the closing→ exit N (label)line. -
Call site:
elif len(urls) > 1: _print_run_summary(urls, url_codes, worst, stdout)— added after the per-URL loop, in theelsebranch of the existingif ns.json_out:check (so it's text-mode-only), right beforereturn worst. JSON path (json.dumps(json_items, ...)) is completely untouched — still a bare array, no top-level summary object, preserving the documented "same shape aslatprobe's JSON" contract.
Design decisions (confirmed with user before implementing)
- Exit code stays a scalar (worst/highest severity across URLs) — not
count-of-failed-URLs, not binary 0/1. Both alternatives were presented and
rejected because they'd break the documented 0–6 table, Go/
latprobeparity, and the 13 existing exit-code tests. - Summary is a text footer, multi-URL only (
len(urls) > 1) — not always shown, and not also duplicated into JSON as a top-level object (which would turn the JSON array into an object and break the documented array-shape parity). Single-URL text output is untouched; JSON output is untouched.
Tests
hxprobe/tests/test_cli.py: new TestCLIRunSummary class, 3 tests, reusing
the existing _OKHandler/_start_server/_free_port/_invoke harness:
test_multi_url_mixed_shows_summary— one OK URL + one connection-refused URL: assertsEXIT_CONNECT, and the footer strings ("Summary: 2 URLs","1 ok","1 failed","connect : 1","→ exit 3").test_multi_url_all_ok_summary— two OK URLs: assertsEXIT_OK,"Summary: 2 URLs — 2 ok", and no"✗"anywhere in output.test_single_url_has_no_summary— one OK URL: asserts"Summary:"is absent (locks the multi-URL-only rule).
All 33 pre-existing tests pass unedited (33 + 3 new = 36 total).
Verification
hxprobe/.venv/bin/python -m pytest tests/test_cli.py tests/test_probe.py -q→ 36 passed.hxprobe/.venv/bin/ruff check hxprobe/ tests/→ all checks passed.- Manual smoke tests, all matching the plan's expected behavior exactly:
hxprobe https://example.com https://example.org→ footerSummary: 2 URLs — 2 ok/→ exit 0 (ok).hxprobe https://example.com http://no.such.host.invalid→ footerSummary: 2 URLs — 1 ok, 1 failed/✗ dns : 1/→ exit 2 (dns); process exit code confirmed2viaecho $?.hxprobe https://example.com(single URL) → no footer, output byte-for-byte the same shape as before this change.hxprobe --json https://example.com https://example.org→ still a bare JSON array, no summary object.- Also captured a 3-URL mixed run (
example.comok, DNS failure, TLS failure againstself-signed.badssl.com) to confirm severity ordering in the footer: DNS (2) and TLS (5) both counted, exit reported as5 (tls)— the higher-severity class correctly wins the scalar while the footer still shows the DNS failure that the scalar alone would hide.
Docs updated (per CLAUDE.md conventions)
hxprobe/USAGE.md: new "Multi-URL summary footer" section with a real 3-URL mixed-outcome capture; refreshed the pre-existing "Multiple URLs" and-f configs/all-ok.txt/-f configs/dns-failure.txtexamples, which were captured before this feature existed and were now stale (missing the footer) — replaced with fresh live captures; added a sentence to the Exit codes section pointing at the new section.docs/usage/hxprobe.md: one-line addition to the exit-codes bullet pointing athxprobe/USAGE.md's new section.docs/explanations/2026-07-02-13-25-hxprobe-worst-exit-code-and-render-loop.md: appended an "Update (2026-07-02)" paragraph pointing forward to this work, per the plan's "optional — pointer instead of a new file" option.CHANGELOG.md: new entry at the top.- This file.
No deviations from the approved plan.