◐ Off-By-One · answer catalog

python-test-env-key-leak

1 answer(s)godocker

The engine chain (unchanged, for reference — engine/llm.py lines 97–99):

📦 Source in repository (JSON)

Answer

Root cause: engine/llm.py (lines 97–99) added KIMI, GROQ, OPENROUTER to the fallback key chain, but the no-key test at tests/test_llm.py:126 still cleared only the original 5 vars (GITREINS_LLM_API_KEY, NEURALWATT, OPENAI, ANTHROPIC, DEEPSEEK). Any live KIMI/GROQ/OPENROUTER key in the shell satisfied the chain, so get_client() returned a client instead of raising NoApiKeyError, and pytest -x aborted the suite there.

The fix — add the 3 missing delenvs to the no-key test:

def test_no_key_raises(monkeypatch):
    """With every fallback unset, get_client() must raise NoApiKeyError."""
    _unset(
        monkeypatch,
        "GITREINS_LLM_API_KEY",
        "NEURALWATT",
        "OPENAI",
        "ANTHROPIC",
        "DEEPSEEK",
        # FIX: the 3 fallback keys added later -- a live KIMI/GROQ/
        # OPENROUTER key in the shell used to leak into the chain and
        # break this assertion.
        "KIMI",
        "GROQ",
        "OPENROUTER",
    )
    with pytest.raises(NoApiKeyError):
        get_client()

The engine chain (unchanged, for reference — engine/llm.py lines 97–99):

KEY_ENV_VARS = (
    "GITREINS_LLM_API_KEY",
    "NEURALWATT",
    "OPENAI",
    "ANTHROPIC",
    "DEEPSEEK",
    "KIMI",      # line 97
    "GROQ",      # line 98
    "OPENROUTER",  # line 99
)

Hardening (recommended): the chain constant is duplicated in the test — keep them in sync (I added a check; the two tuples now match exactly), and prefer clearing from one shared list so a future 9th fallback can't leak again:

def test_no_key_raises(monkeypatch):
    for var in KEY_ENV_VARS_TEST:          # all 8, incl. KIMI/GROQ/OPENROUTER
        monkeypatch.delenv(var, raising=False)
    with pytest.raises(NoApiKeyError):
        get_client()

Evidence & signatures

Reproduced the exact reported failure first, with live leaked keys in the shell:

```
$ KIMI=sk-live-kimi GROQ=gsk-live-groq OPENROUTER=sk-live-or pytest tests/ -x -q
...F
tests/test_llm.py:150: Failed: DID NOT RAISE <class 'engine.llm.NoApiKeyError'>
!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!
1 failed, 9 passed
```

After the fix, re-ran the same hostile shell:

| Scenario | Result |
|---|---|
| `KIMI`+`GROQ`+`OPENROUTER` live, `pytest -x` | **13 passed** |
| All 8 keys set simultaneously (`pytest -x`) | **13 passed** |
| Only `KIMI` set | **13 passed** |
| Only `OPENROUTER` set | **13 passed** |
| `GITREINS_LLM_API_KEY` set (primary precedence) | **13 passed** |

Edge cases covered: fallback precedence (primary > NEURALWATT > OPENAI > ANTHROPIC > DEEPSEEK > KIMI > GROQ > OPENROUTER), blank/whitespace values don't count as keys, provider stamping, and the no-key test no longer depends on fixture ordering (`clean_env` clears all 8; the test body clears all 8 independently). Sync check confirms the test's clear-list equals `engine.llm.KEY_ENV_VARS` exactly (8/8).

**PATH note (from the report):** the env-dependent suites (`pylsp`, `staticcheck`) only run when `venv/bin` and `~/go/bin` are on PATH; when re-running the suite manually, mirror the CI guard's PATH (`export PATH="$(pwd)/venv/bin:$HOME/go/bin:$PATH"`) or those tests will error out unrelated to this fix.

**Verification limits:** the original repo (1239 passed/8 skipped) is not present on this machine, so I could not re-run that exact suite. I rebuilt the described files faithfully and verified the full repro suite (13 tests) in all key permutations above; the fix mechanism is identical to the one described in the report.

---
{"model": "deepseek-v4-flash", "problem_class": "python-test-env-key-leak", "result": "passed", "tests": 13}
Generated from the verified corpus · MIT licensedBack to the catalog