Skip to content

Add black-box e2e suite pinning the CLI config contract - #57

Merged
korya merged 2 commits into
masterfrom
korya-test-e2e-config-contract
Aug 7, 2026
Merged

korya merged 2 commits into
masterfrom
korya-test-e2e-config-contract

Conversation

@korya

@korya korya commented Aug 7, 2026 •

Copy link
Copy Markdown
Owner

Problem

Removing viper (#54) would be a leap of faith: nothing tests how http-assert resolves configuration, and main.go has zero coverage.

The 39% figure was not a testing-effort problem — it was one architectural fact showing up as a number. assertions.go and utils.go were already at 100%; every single function at 0.0% lived in main.go, unreachable because die() calls os.Exit and terminates the process on every error path. So the code that resolves flags, reads the environment, builds the request and renders failures was entirely unverified, and a refactor of it had nothing to check itself against.

Solution

Add a black-box suite that drives the compiled binary and pins all 19 config options across the command line and the environment.

Every test asserts only on exit code, stdout and stderr. Nothing references package internals, so the suite must keep passing verbatim across #54, #55 and #56 — that property is the deliverable, not a side effect.

Coverage:

Before After
Total 39.0% 99.3%
main.go functions at 0.0% 18 of 22 2 (logWarn, logError — dead, no callers)

The binary is built with -cover; its counters merge with the unit-test profile via go tool covdata, so subprocess execution counts. Coverage is now literally 100% minus the dead code.

The contract

TestE2EConfigContract runs all 19 options × {unset, command line, environment}. The unset and cli cases are a built-in mutation check — a vacuous predicate fails one of them, so the matrix cannot pass by being empty.

Options CLI Env
verbose silent log-level insecure max-time maphost ✓ ✓
request header data + all 10 assert-* ✓ ✗

envSupported records what the tool does today, not what it should do. When #54 makes this uniform, flipping those 13 booleans is the diff that proves the change worked.

Two behaviours that would otherwise have been lost silently

HTTP_ASSERT_MAPHOST splits on whitespace, not commas. This is undocumented and counter-intuitive, and a reimplementation reaching for strings.Split(v, ",") — the obvious choice — breaks every multi-mapping user with nothing to catch it:

export HTTP_ASSERT_MAPHOST="a:80=h1:8443 b:80=h2:8080"   # two mappings
export HTTP_ASSERT_MAPHOST="a:80=h1:8443,b:80=h2:8080"   # one malformed mapping, exit 71

The decoy in TestE2EConfigEnvSliceSeparator is listed first and points at a dead port, so a pass proves both entries parsed rather than just the leading one.

A flag always beats the environment, asserted in both directions — a build that ignored the environment entirely would pass a one-directional test.

Characterised defects

e2e_known_issues_test.go pins 14 filed bugs (#17 #18 #19 #20 #22 #23 #25 #26 #27 #28 #31 #32 #33 #34). They are deliberately not t.Skip-able: when an issue is fixed the test fails, and that failure is the signal to update the expectation in the same commit. Each carries a greppable // Characterizes #NN comment and a characterizes(t, NN, …) tag, so go test -run TestKnown selects the set.

Writing them caught an error in my own earlier report: #33 was wrong. -H 'BareHeader' does not drop the header — it sends Bareheader: with an empty value. The original finding came from a case-sensitive grep against a canonicalised name. The issue now carries a dated correction and the real curl comparison.

201 leaf assertions across 36 tests. The suite adds ~10.7s to go test (0.5s unit-only → 11.3s total, measured with -count=1); most of that is the deliberate sleeps the --max-time and precedence cases need, since --max-time is integer-seconds. CI runs it three times — test, test-race, test-cover — and the job completes in 1m10s.

No visual change — this is a headless CLI.

Other Changes

  • Justfile — test-cover and test-coverage now merge unit and end-to-end profiles; without this the e2e runs would report as uncovered. Adds test-cover-func for a per-function breakdown.
  • README.md — the environment-variable section claimed the HTTP_ASSERT_ prefix works generally. Corrected to tabulate the six supported variables, name the command-line-only options, and state the precedence and whitespace-separator rules. Every claim is now asserted by the matrix, so docs and binary cannot drift apart without a test failing.

Related:

🤖 Generated with Claude Code

korya and others added 2 commits August 7, 2026 16:20
Raises coverage from 39.0% to 99.3% and, more importantly, makes the configuration behaviour
executable so that removing viper (#54) can be verified instead of hoped.

Every test drives the compiled binary as a subprocess and asserts only on exit code, stdout and
stderr. Nothing touches package internals, so the suite must keep passing verbatim across the
planned rearchitecture (#54 viper removal, #55 run() extraction, #56 assertion constructors) --
that property is the point, not a side effect.

TestE2EConfigContract is the centrepiece: all 19 options x {unset, command line, environment}.
The unset and command-line cases together act as a mutation check, since a vacuous predicate
fails one of them. EnvSupported records that only 6 of the 19 options read the environment
today; when #54 makes this uniform, flipping those 13 booleans is the diff that proves it.

Previously the coverage number could not include main.go at all, because die() calls os.Exit and
every error path terminated the process. Now the binary is built with -cover and its counters are
merged with the unit-test profile, so `just test-cover` reports both. The only statements left
uncovered are logWarn and logError, which no caller reaches.

Two behaviours are pinned that would otherwise be lost silently:

- HTTP_ASSERT_MAPHOST separates values on whitespace, not commas. A reimplementation reaching
  for strings.Split(v, ",") breaks every multi-mapping user; the decoy mapping in
  TestE2EConfigEnvSliceSeparator is listed first so a pass proves both entries parsed.
- A command-line flag always wins over the environment, asserted in both directions so a build
  that ignored the environment entirely cannot pass.

e2e_known_issues_test.go characterises 14 filed defects. These are deliberately not skipped: when
an issue is fixed the corresponding test fails, which is the signal to update the expectation in
the same commit. Writing them surfaced that #33 was wrong -- `-H 'BareHeader'` sends an
empty-valued header rather than dropping it -- and that issue has been corrected.

Out of scope: fixing any characterised defect, and deleting the dead logWarn/logError helpers
that account for the remaining 0.7%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EMMhgmTkbzAsmeNy97PrP
The environment-variable section claimed the HTTP_ASSERT_ prefix works generally. It does not:
6 of the 19 options honour it and 13 silently ignore it, so a reader following the docs could set
HTTP_ASSERT_REQUEST=POST and watch the tool keep sending GET.

Previously three examples were listed with no statement of scope, which read as illustrative
rather than exhaustive. Now the six supported variables are tabulated, the command-line-only
options are named, and two rules that were undocumented are stated: a flag beats the environment,
and HTTP_ASSERT_MAPHOST separates multiple mappings with whitespace rather than commas -- the
comma form parses as one malformed mapping and exits 71.

Every claim here is asserted by TestE2EConfigContract, so the docs and the binary cannot drift
apart without a test failing.

Scope: documents current behaviour only. Making the environment uniform across all 19 options is
#54, and this section will need revisiting when that lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EMMhgmTkbzAsmeNy97PrP
@korya
korya marked this pull request as ready for review August 7, 2026 20:26
@korya
korya merged commit 0f44633 into master Aug 7, 2026
1 check passed
@korya
korya deleted the korya-test-e2e-config-contract branch August 7, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Test coverage is 39% with no end-to-end CLI tests

1 participant