Skip to content

Make the end-to-end suite opt-in and a separate CI step - #58

Merged
korya merged 1 commit into
masterfrom
korya-test-e2e-opt-in
Aug 7, 2026
Merged

korya merged 1 commit into
masterfrom
korya-test-e2e-opt-in

Conversation

@korya

@korya korya commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Problem

The end-to-end suite ran three times per CI job — about 32s of the runtime — and slowed go test ./... from 0.5s to 11s for everyone.

pre-commit chains test, test-race and test-cover, and each pulled in the full suite. Two of those runs bought almost nothing:

  • test and test-cover are near-duplicates. test-cover runs the identical tests and additionally reports coverage.
  • test-race was checking the wrong process. -race instruments the test binary, but the harness builds the CLI with a separate plain go build, so the race detector covered the harness and the httptest servers — not one line of http-assert. (A race build is 12.1 MB vs 9.9 MB plain; the child is the plain one.)

The redundancy predates the suite — pre-commit always ran three test passes. Adding e2e just turned a latent waste into a visible one.

Solution

Gate the suite behind an opt-in -e2e flag and give it a CI step of its own, so it runs exactly once.

just test        0.5s   unit only
just test-e2e   10.4s   the suite, explicitly
just pre-push          both, for the CI contract locally
- name: Run pre-commit checks     # build, vet, lint, unit tests, race, gosec
  run: just pre-commit

- name: Run end-to-end tests      # its own step, its own line in the CI UI
  run: just test-e2e

CI e2e time: ~32s → ~10s, and a failure now names itself instead of hiding inside a composite recipe.

Why a flag and not a build tag

A //go:build e2e file is invisible to go vet and golangci-lint unless every invocation passes the tag. Tested on a file carrying both a Printf argument mismatch and dead code:

Command Result
go vet ./... clean
golangci-lint run ./... 0 issues.
go vet -tags=e2e ./... catches the Printf bug
golangci-lint run --build-tags=e2e ./... 2 issues: govet 1, unused 1

Tagging would drop 1,100 lines of test code out of both linters, silently, because the recipes are plain go vet ./... and golangci-lint run ./.... A registered flag gives the same opt-in default with every file still compiled and analysed.

Why the check lives in binary()

Putting it in the one function every e2e test already routes through means a test added later inherits the gate automatically. There is no per-file marker to forget, and no way to write an e2e test that quietly runs in the fast loop.

Coverage is unaffected: test-cover opts into -e2e itself and still reports 99.3%.

Other Changes

Adds just pre-push (pre-commit + test-e2e) — the full CI contract in one local command.

One consequence worth naming: just pre-commit no longer exercises the suite, so a break there surfaces in CI rather than before the commit. That is the intended trade — a fast inner loop, with pre-push available for anyone who wants to find out earlier.

Related:

🤖 Generated with Claude Code

The suite ran three times per CI job -- once each under test, test-race and test-cover -- for
about 32s of the runtime. Now it runs once, in a CI step of its own, and `go test ./...` no longer
pays for it at all.

Previously every test recipe pulled in the end-to-end runs. Now they are gated behind a registered
-e2e flag and skipped by default, so `just test` is a 0.5s unit loop and `just test-e2e` is the
explicit 10s gate. `just pre-push` runs both for anyone who wants the CI contract locally.

The gate is a flag rather than a build tag deliberately. A //go:build-tagged file is invisible to
`go vet` and `golangci-lint` unless every invocation passes the tag: a file carrying both a Printf
arg mismatch and dead code passes each tool clean when tagged, and only fails under -tags. Tagging
would therefore drop 1100 lines of test code out of both linters, silently. The flag keeps every
file compiled and analysed while giving the same opt-in default.

Placing the check inside binary() rather than in each test means an end-to-end test added later
inherits it automatically; there is no per-file marker to forget.

Coverage is unaffected -- test-cover opts into -e2e itself and still reports 99.3%.

One consequence worth naming: `just pre-commit` no longer exercises the end-to-end suite, so a
break there surfaces in CI rather than before the commit. `just pre-push` exists for anyone who
prefers to find out earlier.

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:54
@korya
korya merged commit 9df95a8 into master Aug 7, 2026
1 check passed
@korya
korya deleted the korya-test-e2e-opt-in branch August 7, 2026 20:54
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.

1 participant