Ninth CI run cleared every ``Your ID=None`` failure (the master_key env fix worked end-to-end) and exposed the next thin layer of failures: ``/key/regenerate`` returns 500 "Regenerating Virtual Keys is an Enterprise feature" in CI because the proxy can't see a ``LITELLM_LICENSE``. Locally my license is set, so the matrix passes. The behavior matrix is supposed to pin authz, not licensing — so flip ``proxy_server.premium_user = True`` directly, both before and after the lifespan (the lifespan re-runs ``_license_check.is_premium()`` and would otherwise reset it). With premium gating disabled, the regenerate matrix exercises the same authz path /key/update does. Whole-suite still green locally (130 tests, ~6.3s). |
||
|---|---|---|
| .. | ||
| mutmut_triage | ||
| regression_replay | ||
| __init__.py | ||
| actors.py | ||
| conftest.py | ||
| README.md | ||
| test_aaa_world_seed.py | ||
| test_key_delete.py | ||
| test_key_generate.py | ||
| test_key_info.py | ||
| test_key_list.py | ||
| test_key_regenerate.py | ||
| test_key_update.py | ||
| test_no_management_imports.py | ||
| test_scratch_teardown.py | ||
| test_smoke.py | ||
Management-endpoint behavior-pinning suite
HTTP-boundary regression tests for
litellm/proxy/management_endpoints/key_management_endpoints.py (PR1 — Key
Tier-1). Runs against the real proxy app via in-process httpx.ASGITransport,
connected to a real Postgres pointed at by DATABASE_URL. No mocks —
auth runs, prisma runs, integrations run. Test bodies make HTTP calls and
assert at the API boundary.
The eventual goal (across PR1–PR3) is to pin every authorization /
cross-tenant / budget-bypass boundary on the key + team management
surfaces. PR1 covers six Tier-1 key endpoints (/key/generate, /key/info,
/key/list, /key/update, /key/regenerate, /key/delete). See the
Notion plan for
the full scope.
Local repro
Identical to the three commands the CI workflow
(.github/workflows/test-unit-proxy-mgmt-behavior.yml, which delegates
to _test-unit-services-base.yml) runs:
# 1. Bring up Postgres
docker run --rm -d --name litellm-test-pg \
-e POSTGRES_USER=litellm -e POSTGRES_PASSWORD=litellm -e POSTGRES_DB=litellm_test \
-p 5432:5432 postgres:14
# 2. Migrate the schema (one-time per fresh DB)
export DATABASE_URL=postgresql://litellm:litellm@localhost:5432/litellm_test
uv run prisma generate --schema litellm/proxy/schema.prisma
uv run prisma db push --schema litellm/proxy/schema.prisma --accept-data-loss
# 3. Run the suite
uv run pytest tests/proxy_behavior/management/
Whole-suite wall-time is ~6s on a warm cache (one ~1.5s session setup + ~0.01–0.04s per test). Re-running back-to-back produces identical pass counts — the scratch-namespace teardown leaves no rows behind.
Single scenario / inner loop
uv run pytest tests/proxy_behavior/management/test_key_update.py -k self/owner -v
Layout
tests/proxy_behavior/management/
├── conftest.py # session ASGI client, world seed, scratch fixture
├── actors.py # 8-actor enum + seed_world() helper
├── test_smoke.py # liveness + key/generate de-risk smoke
├── test_world_seed.py # every seeded actor key authenticates
├── test_scratch_teardown.py # scratch namespace cleanup invariants
├── test_no_management_imports.py # G3 — strict-import grep as a test
├── test_key_generate.py # Slice 7 — actor × target matrix
├── test_key_info.py # Slice 8
├── test_key_list.py # Slice 9
├── test_key_update.py # Slice 10
├── test_key_regenerate.py # Slice 11
├── test_key_delete.py # Slice 12
├── regression_replay/README.md # G4 — fix-PR → catching-scenario mapping
└── mutmut_triage/pr1.md # G5 — survivor classification protocol
Conventions
- Async fixture / loop scope.
pyproject.tomlsetsasyncio_default_fixture_loop_scope = "session", but the default test loop scope is per-function. Addpytestmark = pytest.mark.asyncio(loop_scope="session")at the top of every test file so the AsyncClient and prisma connection (both session-scoped) share a loop with the test body. - Forbidden imports (G3). No
from litellm.proxy.management_endpoints, nomock/patchonuser_api_key_auth. Enforced bytest_no_management_imports.pyas a pytest item. - Read-world vs scratch-world. The
worldfixture seeds an immutable read-world under thebehavior-pin-prefix; tests must not mutate those rows. Thescratchfixture gives a per-testscratch-<uuid>prefix and tears down any row tagged with it. Write scenarios always tag their creates withscratch.prefix. - Behavior pinning, not behavior judging. Expected status codes are
pinned against current handler behavior. The suite's job is to make
changes to that behavior visible — not to assert what the codes
should be. Comments above each
_SCENARIOSblock call out surprising or potentially-buggy behaviors for human review.
Gate evidence
PR1's evidence for each G1–G5 + PR1.M1–M3 gate lives in:
- G1 — CI run on the PR's workflow
test-unit-proxy-mgmt-behavior(green). - G2 —
pytest --durations=…summary in the PR description (≤ 10 min). - G3 —
test_no_management_imports.pyis part of the suite itself. - G4 —
regression_replay/README.md. - G5 —
mutmut_triage/pr1.md, filled in after the first manually-triggeredmutation-test.ymlrun. - PR1.M1 — total scenario count, this README's "Layout" section.
- PR1.M2 — this README + the workflow YAML are the local-repro contract.
- PR1.M3 —
mutmut_triage/pr1.md"Baseline metrics" table.