Repository navigation
Replace viper with an explicit environment lookup - #59
Merged
Merged
Conversation
The config matrix asks "did this option take effect?" -- a boolean -- so it cannot see how a value was parsed, only that it was. Five behaviours therefore had no coverage at all: empty variables, unparseable values, non-canonical booleans, precedence on five of the six options, and log levels other than error. Those gaps matter now because removing viper (#54) turns on exactly those paths. Without these tests a replacement could reject an empty variable, or parse "1" differently, and all 57 existing subtests would still pass. Adds 32 subtests, written against the current viper build so they record real behaviour rather than intended behaviour. The uncomfortable one is HTTP_ASSERT_MAX_TIME=abc: viper casts it to 0, and a zero http.Client.Timeout means no timeout at all, so a typo silently removes the deadline from a tool whose job is enforcing one. That is pinned as-is here and rejected in a later commit, so the change of behaviour shows up as a visible edit to this file rather than as a silent difference. Empty variables turned out to be nearly unobservable from outside: every option's default coincides with its zero value, and 0 vs 20 seconds cannot be distinguished without a 20-second request. The test still earns its place by guaranteeing an empty variable never becomes an error, which is the regression that would otherwise slip through once unparseable values are rejected. Also silences the TLS handshake warnings the test server logged whenever a case deliberately connected without --insecure. That is the expected result of those cases, not a failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EMMhgmTkbzAsmeNy97PrP
Drops 35 of the module's 38 non-stdlib dependencies to configure six flags from the environment. viper was used for nothing but AutomaticEnv -- no config files, no remote providers, no watching -- and pulled in fsnotify, afero, mapstructure, go-toml, locafero, conc, gotenv, cast and yaml to do it. The replacement is one 25-line function over the flag set. Previously six options were read through viper and the other thirteen straight off cobra, so HTTP_ASSERT_INSECURE worked while HTTP_ASSERT_REQUEST silently did nothing, with no rule a reader could infer. Now every option is read from cobra and a single named list, envFlags, says which ones also consult the environment. That list stays at six on purpose. Repeatable options take several values from one variable by splitting on whitespace, which is right for host mappings and wrong for header values, since those routinely contain spaces: HTTP_ASSERT_HEADER="X-Foo: bar baz" would become three headers. Issue #54 proposed making the environment uniform across all nineteen options; that part of it should not be done, and the issue has been updated. Behaviour is unchanged, including the parts that are arguably wrong. An empty variable still counts as unset, whitespace still separates repeated values, the command line still wins, and an unparseable value still becomes the type's zero value rather than an error. The last of those is reproduced deliberately in six lines and removed in the next commit, so the fix arrives as its own reviewable change. The proof is that no test file is touched by this commit. All 233 subtests, including the 32 added in the previous commit specifically to cover environment parsing, pass unedited. dependencies 38 -> 3 (cobra, pflag, mousetrap) packages 235 -> 193 binary 9.93 -> 9.01 MB coverage 99.3% -> 99.4% Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EMMhgmTkbzAsmeNy97PrP
A typo in HTTP_ASSERT_MAX_TIME used to disable the request timeout instead of reporting an error.
The value was cast to the type's zero value, and a zero http.Client.Timeout means no timeout at
all, so `HTTP_ASSERT_MAX_TIME=abc` left a monitoring check able to hang indefinitely -- in a tool
whose entire purpose is enforcing a deadline.
Previously an unparseable value was applied silently: max-time became 0, booleans became false. So
`HTTP_ASSERT_VERBOSE=yes` quietly turned verbosity off, because "yes" is not a Go boolean. Now the
value is rejected with the parser's own reason and exit code 71, matching how --log-level and
--maphost already report bad input.
$ HTTP_ASSERT_MAX_TIME=abc http-assert --assert-ok https://api.example.com
Error: Invalid value for HTTP_ASSERT_MAX_TIME="abc": strconv.ParseInt: parsing "abc": invalid syntax
$ echo $?
71
This is a deliberate behaviour change, and the edits to e2e_config_test.go are its record: the
cases that previously asserted silent coercion now assert rejection. Empty variables are
unaffected and still count as unset, which the neighbouring test guards.
Adds explicit rejection cases for every typed option and for the shell-style boolean spellings,
and documents both rules in the README.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EMMhgmTkbzAsmeNy97PrP
Neither has ever had a caller. Both were kept alive with //nolint:unused, which silenced the linter that was right rather than removing the code it was pointing at. Coverage now reaches 100.0%, since these four statements were the only ones in the repo no test could reach. The LWarn level itself stays. `--log-level warn` is part of the documented flag surface, and removing the level would change the CLI contract rather than delete dead code -- so the oddity that warn and error behave identically survives, still pinned by TestKnownIssueWarnLevelIsDead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EMMhgmTkbzAsmeNy97PrP
korya
marked this pull request as ready for review
August 7, 2026 21:29
Nothing checked that go.mod and go.sum matched the imports. A stale requirement could survive indefinitely -- exactly the state this branch started from, with viper listed long after it was the only thing pulling in nine transitive modules. Adds a tidy-check recipe running `go mod tidy -diff`, which prints the changes it would make and exits non-zero when the diff is not empty, without touching either file. Wired into pre-commit rather than added as a separate CI step so that local runs and CI check the same thing through the same command, which is how every other gate in this repo already works. Verified in both directions: an unused require makes `just pre-commit` exit 1, and removing it returns to 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EMMhgmTkbzAsmeNy97PrP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Viper contributes 35 of the module's 38 non-stdlib packages to read six environment variables.
It is used for nothing but
AutomaticEnv— no config files, no remote providers, no watching — and for that the module graph carriesfsnotify,afero,mapstructure,pelletier/go-toml,locafero,sourcegraph/conc,gotenv,castandyaml. It also produced a split-brain: six options were read through viper and thirteen straight off cobra, soHTTP_ASSERT_INSECUREworked whileHTTP_ASSERT_REQUESTsilently did nothing, with no rule a reader could infer.Solution
Replace viper with one 25-line function over the flag set, and name the six environment-backed options in a single list.
Four commits, ordered so the risky part is provable
Commit 2 touches no test file. All 233 subtests pass unedited, including the 32 added in commit 1 specifically to cover environment parsing. That is the evidence the swap changed nothing — not an assertion in a commit message.
Commit 1 exists because the existing matrix asks "did this option take effect?" — a boolean — so it could not see how a value was parsed, only that it was. Empty variables, unparseable values, non-canonical booleans, precedence on five of six options, and log levels other than
errorhad no coverage at all, and those are precisely the paths a viper replacement turns on.Why the environment list stays at six
Preserving
--maphost's existing behaviour requires splitting one variable into several values on whitespace (viper's semantics, verified against the library). Applied uniformly — as issue #54 originally proposed — that corrupts every option whose values contain spaces:Header values routinely contain spaces; host mappings never do. Uniformity would trade one inconsistency for a silent value-corrupting footgun across ten flags. The inconsistency is instead fixed where it actually hurt — it is now a named list in the source and a table in the README, rather than an accident of which flags happened to get a
BindPFlagline. #54 has been updated to record this rather than leaving the issue describing something we deliberately did not build.The one deliberate behaviour change
A typo in
HTTP_ASSERT_MAX_TIMEused to disable the request timeout. The value was cast to the type's zero value, and a zerohttp.Client.Timeoutmeans no timeout at all — so a monitoring check could hang indefinitely, in a tool whose entire purpose is enforcing a deadline. LikewiseHTTP_ASSERT_VERBOSE=yesquietly turned verbosity off, becauseyesis not a Go boolean.Before:
After:
Exit code 71 matches how
--log-leveland--maphostalready report bad input. Empty variables are unaffected and still count as unset. This lands in its own commit, and the test diff is its record.No visual change — this is a headless CLI.
Other Changes
go.modis now enforced tidy. Nothing checked that it matched the imports, which is exactly how viper survived as a requirement long after it stopped earning its place. Atidy-checkrecipe runsgo mod tidy -diff— it reports what it would change and exits non-zero without modifying either file — wired intopre-commitrather than added as a separate CI step, so local runs and CI check the same thing through the same command. Verified in both directions: an unused require makesjust pre-commitexit 1.Coverage reaches 100.0%.
logWarnandlogErrorhad no callers and were kept alive with//nolint:unused, silencing a linter that was right. Their four statements were the only ones in the repo no test could reach. TheLWarnlevel stays, since--log-level warnis documented flag surface.README documents that an empty variable counts as unset, that unparseable values are rejected, and which boolean spellings are accepted.
Silences the TLS handshake warnings the test server logged whenever a case deliberately connected without
--insecure— the expected result of those cases, not a failure.Related:
🤖 Generated with Claude Code