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>
106 lines
5.6 KiB
Markdown
106 lines
5.6 KiB
Markdown
# 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):
|
||
|
||
1. **`_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.
|
||
|
||
2. **Accumulation loop refactor**: previously computed a single running
|
||
`worst` directly inside the per-URL loop via two different idioms
|
||
(`if c > worst: worst = c` for network failures, `max(worst, EXIT_HTTP)`
|
||
for `--fail`). Now each URL first gets its own `code` (`EXIT_OK` folded up
|
||
via `max()` across its failed samples and, if `--fail`, its ≥400 successes),
|
||
appended to a new `url_codes: list[int]` (index-aligned with `urls`), and
|
||
*then* folded into `worst = max(worst, code)`. Unifies both idioms into one.
|
||
|
||
3. **`_print_run_summary(urls, url_codes, worst, out)`** (new helper, next to
|
||
`_print_failure_summary`): writes a separator line, an
|
||
`N 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.
|
||
|
||
4. **Call site**: `elif len(urls) > 1: _print_run_summary(urls, url_codes,
|
||
worst, stdout)` — added after the per-URL loop, in the `else` branch of the
|
||
existing `if ns.json_out:` check (so it's text-mode-only), right before
|
||
`return worst`. JSON path (`json.dumps(json_items, ...)`) is completely
|
||
untouched — still a bare array, no top-level summary object, preserving the
|
||
documented "same shape as `latprobe`'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/`latprobe`
|
||
parity, 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: asserts `EXIT_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: asserts `EXIT_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` → footer
|
||
`Summary: 2 URLs — 2 ok` / `→ exit 0 (ok)`.
|
||
- `hxprobe https://example.com http://no.such.host.invalid` → footer
|
||
`Summary: 2 URLs — 1 ok, 1 failed` / `✗ dns : 1` / `→ exit 2 (dns)`;
|
||
process exit code confirmed `2` via `echo $?`.
|
||
- `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.com` ok, DNS failure, TLS
|
||
failure against `self-signed.badssl.com`) to confirm severity ordering in
|
||
the footer: DNS (2) and TLS (5) both counted, exit reported as `5 (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.txt` examples, 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 at `hxprobe/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.
|