Skip to content

feat(assert): Accept a status class, list or range in --assert-status - #103

Merged
korya merged 1 commit into
masterfrom
korya-feat-status-spec
Aug 11, 2026
Merged

feat(assert): Accept a status class, list or range in --assert-status#103
korya merged 1 commit into
masterfrom
korya-feat-status-spec

Conversation

@korya

@korya korya commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Problem

Asking "did this request succeed" took either one run per acceptable status code or --assert-ok, which also passes on a 3xx.

--assert-status accepted exactly one integer, so a caller who meant any 2xx, either 200 or 204, or anything in 401-403 had no way to say it.

Separately, the flag accepted codes no response can carry. --assert-status 1000 exited 93 — the code that means the service answered and was wrong — when the real problem was a typo in the command line (#93).

Solution

Accept a spec instead of a code: a class, an inclusive range, a comma-separated list, or any mix of them.

$ http-assert --assert-status 2xx          https://example.com    # any 200-299
$ http-assert --assert-status 200,204      https://example.com    # either
$ http-assert --assert-status 401-403      https://example.com    # any of three
$ http-assert --assert-status 301,2xx,500-503 https://example.com # mixed

Every form reduces to a set of inclusive ranges, so one matcher serves all of them and an exact code is simply a range of one. The failure quotes the spec as written:

- status: expected 2xx, got 404 ("404 Not Found")

expected 2xx is the question the caller asked; expected 200-299 would be an answer they have to translate back.

The flag moves from int to string, which is what lets it carry the other forms. Nothing else about it changes — still single-valued, still command-line only, and an exact code behaves and reads exactly as it did.

#93's premise was wrong, and the plan changed because of it

#93 asks to reject "codes that can never match (-1, 999)" by bounding to 100-599. A probe says otherwise:

code 599   OK: client saw 599        code 0     server REFUSED: invalid WriteHeader code 0
code 600   OK: client saw 600        code 99    server REFUSED: invalid WriteHeader code 99
code 999   OK: client saw 999        code 1000  server REFUSED: invalid WriteHeader code 1000

A server can send 6xx-9xx and net/http reports them faithfully, so an assertion about one is answerable. Implementing the ticket as written would have made real responses unassertable. The bound enforced here is 100-999 — what a response can actually carry — so -1, 1000 and 099 exit 71 while 999 remains a legitimate assertion.

1xx and 6xx-9xx classes parse for the same reason and are deliberately not documented: answerable, but not useful, and advertising them would suggest otherwise.

Verification

Unit tests cover 12 accepted specs against matching and non-matching codes, and 17 rejected ones against the message each should produce. E2E covers every form end to end plus rejection before any request is made.

Mutation-checked — each of these fails the suite:

Mutation Caught
range end exclusive instead of inclusive
class spans 200 codes instead of 100
out-of-range codes accepted
reversed range accepted

Manually verified against live endpoints: 2xx, 4xx, 404,410 and 400-499 against real 200 and 404 responses, plus 999 still assertable and 1000 still rejected.

Other Changes

  • Failure.Expected for a status assertion is now the spec text rather than an int. No consumer exists yet (Feature: machine-readable output (--json) #45 is unbuilt); a type that varied by form would be worse for a future JSON consumer than one that is always a string.
  • assertions_test.go used AssertStatusEqual(1) as its never-matching case; 1 is no longer expressible, so it is now 599.
  • No visual change — CLI only.

Closes #93

🤖 Generated with Claude Code

The flag took one exact code, so asking "did this succeed" meant either naming
every acceptable code in separate runs or falling back to --assert-ok, which
also passes on a 3xx. A class, a list and a range cover what callers actually
mean, and they mix: 301,2xx,500-503 is one spec.

Every form reduces to a set of inclusive ranges, so one matcher serves all of
them and an exact code is a range of one. The failure quotes the spec as
written -- `expected 2xx` is the question that was asked, where `expected
200-299` would be an answer to translate back.

The flag changes from an int to a string, which is what lets it carry the other
forms. Nothing else about it moves: it stays single-valued and command-line
only, and an exact code behaves and reads exactly as before.

Parsing the spec also settles #93, which asked for codes that can never match
to be rejected at the flag rather than failing an assertion at exit 93. Its
suggested bound was wrong, though: it named 999 as unmatchable, and a server
can send 999. net/http reports 6xx-9xx faithfully and refuses only to write
below 100 or above 999, so those are the bounds enforced here -- an assertion
about a non-conformant status is answerable, and only a code no response can
carry is a typo.

1xx and 6xx-9xx classes parse for the same reason and are deliberately not
advertised: they are answerable rather than useful, and documenting them would
suggest otherwise.

Closes #93

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrknafJSP5hF8u865cbnqX
@korya
korya marked this pull request as ready for review August 11, 2026 12:23
@korya
korya merged commit 1fb2dfb into master Aug 11, 2026
8 checks passed
@korya
korya deleted the korya-feat-status-spec branch August 11, 2026 12:23
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.

--assert-status accepts codes that can never match (-1, 999)

1 participant