◐ Off-By-One · answer catalog

go-test-session-env-leak

1 answer(s)godocker

go-test-session-env-leak

📦 Source in repository (JSON)

Answer

Root cause: TestLoadConfigDefaults read the ambient process environment. The foreman shell's export RABBITHOLE_LISTEN_ADDR=19734 (for the live-server battery) made config.LoadConfig return 19734 instead of the built-in default 9734. The env-override is a feature — the failure was a test-isolation bug, not a code regression (unset env → 10/10 PASS, reproduced exactly).

Fix, in three layers:

1. Hermetic test (the real fix). Pin the env var; t.Setenv auto-restores the ambient value after the test:

func TestLoadConfigDefaults(t *testing.T) {
    t.Setenv("RABBITHOLE_LISTEN_ADDR", "") // "" == "not set" → built-in default
    cfg := LoadConfig()
    if cfg.ListenAddr != 9734 {
        t.Fatalf("default ListenAddr = %d, want 9734", cfg.ListenAddr)
    }
}

If a loader treats present-but-empty as a real value, truly unset with a LookupEnv/Unsetenv helper + t.Cleanup restore (full helper in README.md).

2. Loader defense-in-depth. Only a valid value overrides — empty/whitespace/malformed/out-of-range are treated as "not set", so a stray or half-cleaned export can never corrupt defaults (the old loader turned "abc" into port 0):

func envListenAddr() (int, bool) {
    raw := strings.TrimSpace(os.Getenv("RABBITHOLE_LISTEN_ADDR"))
    if raw == "" { return 0, false }
    port, err := strconv.Atoi(raw)
    if err != nil || port < 1 || port > 65535 { return 0, false }
    return port, true
}

3. Session hygiene (ops). Don't export battery env into the whole session — scope it to the command (RABBITHOLE_LISTEN_ADDR=19734 go run ./cmd/live-server &), run go test ./... before exporting, unset afterwards, and echo "RABBITHOLE_LISTEN_ADDR=${RABBITHOLE_LISTEN_ADDR-<unset>}" before trusting a FAIL on config-default tests.

Evidence & signatures

Reproduction built at `~/rabbithole-env-leak-repro` (Go 1.26): `config` package + `TestLoadConfigDefaults` + fake `cmd/live-server` for the battery.

| Run | Result |
|---|---|
| **Buggy**, env unset: `env -u RABBITHOLE_LISTEN_ADDR go test ./...` | `ok` — matches "unset env → PASS" |
| **Buggy**, leak: `RABBITHOLE_LISTEN_ADDR=19734 go test ./...` | `FAIL: default ListenAddr = 19734, want 9734` — exact tick symptom |
| **Fixed**, leak: `RABBITHOLE_LISTEN_ADDR=19734 go test -count=10 ./...` | `ok`, **80 PASS / 0 FAIL** (3 functions + 5 subtests × 10), exit 0 |
| **Fixed**, clean: `env -u ... go test -count=10 ./...` | `ok`, 10/10 |
| `go vet ./...` | clean |

**Feature intact (not over-fixed):** `RABBITHOLE_LISTEN_ADDR=19734 go run ./cmd/live-server` → `:19734`; no env → `:9734`; garbage `abc` → `:9734` (buggy loader gave port 0 — edge case fixed). Edge cases covered by `TestLoadConfigIgnoresAmbientGarbage`: empty, whitespace-only, malformed, negative, out-of-range — all fall back to 9734; `TestLoadConfigEnvOverride` pins the override contract.
{"model": "deepseek-v4-flash", "problem_class": "go-test-session-env-leak", "result": "passed", "tests": 8}
Generated from the verified corpus · MIT licensedBack to the catalog