litellm/tests/proxy_behavior/management
Yuneng Jiang 0c73928a6d
ci(proxy-mgmt-behavior): await LiteLLM_VerificationTokenView creation in fixture
Fourth CI run still failed because the proxy's lifespan kicks off
``prisma_client.check_view_exists()`` as a fire-and-forget background
task — that task is what creates ``LiteLLM_VerificationTokenView``, the
SQL view ``user_api_key_auth`` queries to resolve a token to its
user_id / user_role / team.

On a fresh Postgres (CI), the first test races the background task. The
view doesn't exist when the first auth call runs, the resolver falls
through to a degraded path that returns ``user_id=None``, and every
matrix test that depends on the seeded actor's identity then fails
confusingly with "Got user_id=X, Your ID=None" 403s. Locally the view
persists across pytest runs so the race is invisible.

Fix: await ``prisma_client.check_view_exists()`` explicitly inside the
session ``proxy_app`` fixture, after the lifespan enters but before the
fixture yields. Deterministic regardless of whether the underlying DB is
fresh (CI) or warm (local).
2026-05-19 22:26:52 -07:00
..
mutmut_triage chore(mutmut): include the behavior suite in tests_dir + G5 triage stub 2026-05-19 21:57:43 -07:00
regression_replay docs(proxy_behavior): G4 regression-replay table for Key Tier-1 2026-05-19 21:56:51 -07:00
__init__.py test(proxy_behavior): scaffold session-scoped async ASGI client + liveness smoke 2026-05-19 21:21:00 -07:00
actors.py test(proxy_behavior): pin /key/generate authz matrix (18 scenarios) 2026-05-19 21:40:45 -07:00
conftest.py ci(proxy-mgmt-behavior): await LiteLLM_VerificationTokenView creation in fixture 2026-05-19 22:26:52 -07:00
README.md docs(proxy_behavior): suite README with local-repro + conventions + gates 2026-05-19 21:58:34 -07:00
test_aaa_world_seed.py ci(proxy-mgmt-behavior): diag — run world-seed test first + bump max-failures 2026-05-19 22:23:54 -07:00
test_key_delete.py ci(proxy-mgmt-behavior): seed scratch keys via proxy_admin actor, not master 2026-05-19 22:15:17 -07:00
test_key_generate.py test(proxy_behavior): pin /key/generate authz matrix (18 scenarios) 2026-05-19 21:40:45 -07:00
test_key_info.py test(proxy_behavior): pin /key/info authz matrix (24 scenarios) 2026-05-19 21:43:38 -07:00
test_key_list.py test(proxy_behavior): pin /key/list default-visibility matrix (8 scenarios) 2026-05-19 21:45:29 -07:00
test_key_regenerate.py ci(proxy-mgmt-behavior): seed scratch keys via proxy_admin actor, not master 2026-05-19 22:15:17 -07:00
test_key_update.py ci(proxy-mgmt-behavior): seed scratch keys via proxy_admin actor, not master 2026-05-19 22:15:17 -07:00
test_no_management_imports.py style(proxy_behavior): apply black to G3 grep test 2026-05-19 21:36:15 -07:00
test_scratch_teardown.py test(proxy_behavior): per-test scratch namespace + targeted delete_many teardown 2026-05-19 21:35:05 -07:00
test_smoke.py test(proxy_behavior): per-test scratch namespace + targeted delete_many teardown 2026-05-19 21:35:05 -07:00

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.toml sets asyncio_default_fixture_loop_scope = "session", but the default test loop scope is per-function. Add pytestmark = 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, no mock/patch on user_api_key_auth. Enforced by test_no_management_imports.py as a pytest item.
  • Read-world vs scratch-world. The world fixture seeds an immutable read-world under the behavior-pin- prefix; tests must not mutate those rows. The scratch fixture gives a per-test scratch-<uuid> prefix and tears down any row tagged with it. Write scenarios always tag their creates with scratch.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 _SCENARIOS block 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.py is part of the suite itself.
  • G4 — regression_replay/README.md.
  • G5 — mutmut_triage/pr1.md, filled in after the first manually-triggered mutation-test.yml run.
  • 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.