litellm/tests/unit/proxy/db/test_replica_identity.py
devin-ai-integration[bot] 102619d47a
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Deploy main to gateway-dev / dispatch (push) Waiting to run
Publish lint base counts / publish (basedpyright, scripts/type_check_gate.py) (push) Waiting to run
Publish lint base counts / publish (openapi-docs, scripts/openapi_docs_gate.py) (push) Waiting to run
Publish lint base counts / publish (ruff-strict, scripts/ruff_strict_gate.py) (push) Waiting to run
Publish lint base counts / publish (test-quality, scripts/test_quality_gate.py) (push) Waiting to run
Publish lint base counts / publish (type-discipline, scripts/type_discipline_gate.py) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
unit / proxy-server-core (push) Blocked by required conditions
unit / root (push) Blocked by required conditions
unit / proxy-utils (push) Blocked by required conditions
unit / mcp-elicitation (push) Blocked by required conditions
unit / proxy-auth (push) Blocked by required conditions
unit / proxy-endpoints (push) Blocked by required conditions
unit / proxy-extras (push) Blocked by required conditions
unit / proxy-feature-endpoints (push) Blocked by required conditions
unit / proxy-hooks-client (push) Blocked by required conditions
unit / proxy-infra (push) Blocked by required conditions
unit / proxy-infra-root (push) Blocked by required conditions
unit / responses-caching-types (push) Blocked by required conditions
unit / router (push) Blocked by required conditions
unit / rust-bridge-harness (push) Blocked by required conditions
unit / auth-checks (push) Blocked by required conditions
unit / budgets (push) Blocked by required conditions
unit / custom-logging (push) Blocked by required conditions
unit / enterprise-routing (push) Blocked by required conditions
unit / guardrails-hooks (push) Blocked by required conditions
unit / key-generation (push) Blocked by required conditions
unit / logging-misc (push) Blocked by required conditions
unit / assert-shard-coverage (push) Waiting to run
unit / llms-openai-meta (push) Blocked by required conditions
unit / llms-providers (push) Blocked by required conditions
unit / llms-vertex-ai (push) Blocked by required conditions
unit / ui-unit (push) Waiting to run
unit / docs (push) Waiting to run
unit / OpenAI 3.25.0 compatibility (push) Waiting to run
unit / coverage (push) Blocked by required conditions
unit / db-and-spend (push) Blocked by required conditions
unit / endpoints-and-responses (push) Blocked by required conditions
unit / jwt-and-keys (push) Blocked by required conditions
unit / mcp-oauth (push) Blocked by required conditions
unit / proxy-runtime (push) Blocked by required conditions
unit / rust-bridge (push) Waiting to run
unit / enterprise-managed-files (push) Blocked by required conditions
unit / proxy-server (push) Blocked by required conditions
unit / lens-python-310 (push) Blocked by required conditions
unit / caching-local (push) Blocked by required conditions
unit / core-utils (push) Blocked by required conditions
unit / endpoints (push) Blocked by required conditions
unit / enterprise-package (push) Blocked by required conditions
unit / enterprise-repositories-secrets (push) Blocked by required conditions
unit / integrations (push) Blocked by required conditions
unit / llms-anthropic (push) Blocked by required conditions
unit / llms-bedrock (push) Blocked by required conditions
unit / helm (push) Waiting to run
unit / OpenAI 2.20.0 compatibility (push) Waiting to run
unit / OpenAI 3.0.0 compatibility (push) Waiting to run
unit / unit passed (push) Blocked by required conditions
GitHub Actions Security Analysis / zizmor (push) Waiting to run
test: migrate proxy security, schema migration and MCP legacy tests (#45937)
* test: migrate proxy security, schema migration and MCP legacy tests

* test: run the MCP proxy e2e upstream under the SDK1 peer in integration-mcp

* test: warm the baseline store before timing the locked projection

* test(e2e): hold the stack exclusively while the semantic filter flag is flipped

* test(mcp): scope the in-process proxy e2e fixtures to their module

The session-scoped autouse fixture deleted DATABASE_URL and overrode LITELLM_MASTER_KEY for the rest of the worker, which broke every later test in the integration mcp suite

* test(e2e): assert the semantic filter keeps listed tools instead of a ranking

Which Datadog tool an OpenAI embedding ranks first is not something litellm owns, so the test now checks that the kept tools are a non-empty subset of the key's listing within top_k

* test(mcp): keep the in-process proxy e2e tests on one worker

loadgroup spread the file's tests across workers and interleaved them with other files, so the module-scoped in-process proxy was torn down and restarted between tests and requests timed out

* test(mcp): name the upstream requests that carried the wrong token

The bare all() assertion hid which request broke the forwarded token, which made an intermittent CircleCI failure undiagnosable

---------

Co-authored-by: yuneng <yuneng@berri.ai>
2026-10-11 16:17:24 -07:00

130 lines
4.2 KiB
Python

"""The opt-in REPLICA IDENTITY FULL step, without a database.
The behavior against real Postgres is covered by
tests/integration/database/migrations/test_replica_identity_full.py; these pin the two
things that hold with no database at all: the statement handed to the Prisma
CLI, and the promise that no failure of this optional step escapes into a
migration run that already succeeded.
"""
import subprocess
from pathlib import Path
from unittest.mock import patch
import pytest
from litellm_proxy_extras.replica_identity import (
REPLICA_IDENTITY_FULL_ENV_VAR,
apply_replica_identity_full,
)
from litellm_proxy_extras.utils import ProxyExtrasDBManager
def test_hands_the_alter_statement_to_the_prisma_cli():
captured = {}
def capture(cmd, **kwargs):
captured["cmd"] = cmd
captured["sql"] = Path(cmd[cmd.index("--file") + 1]).read_text()
return subprocess.CompletedProcess(cmd, 0)
with patch(
"litellm_proxy_extras.replica_identity.run_prisma", side_effect=capture
):
applied = apply_replica_identity_full(
schema_path="/somewhere/schema.prisma",
prisma_command="prisma",
prisma_env={"DATABASE_URL": "postgresql://x/y"},
)
assert applied is True
assert captured["cmd"][:3] == ["prisma", "db", "execute"]
assert captured["cmd"][-2:] == ["--schema", "/somewhere/schema.prisma"]
sql = captured["sql"]
assert "ALTER TABLE %s REPLICA IDENTITY FULL" in sql
assert r"c.relname LIKE 'LiteLLM\_%'" in sql
assert "c.relreplident <> 'f'" in sql
assert "lock_timeout" in sql
@pytest.mark.parametrize(
"failure",
[
subprocess.CalledProcessError(1, "prisma", stderr="must be owner of table"),
subprocess.TimeoutExpired("prisma", 60),
OSError(2, "No such file or directory"),
PermissionError(13, "Read-only file system"),
],
ids=["rejected", "timed-out", "cli-missing", "read-only-fs"],
)
def test_every_failure_is_reported_instead_of_raised(failure):
with patch(
"litellm_proxy_extras.replica_identity.run_prisma", side_effect=failure
):
assert (
apply_replica_identity_full(
schema_path="/somewhere/schema.prisma",
prisma_command="prisma",
prisma_env={},
)
is False
)
def test_an_unusable_migrations_dir_skips_the_step_instead_of_killing_the_run(
tmp_path, monkeypatch
):
"""LITELLM_MIGRATION_DIR makes the step copy the migrations tree before it
can run, and that copy is filesystem work that can fail on its own."""
blocker = tmp_path / "blocker"
blocker.write_text("not a directory")
monkeypatch.setenv(REPLICA_IDENTITY_FULL_ENV_VAR, "true")
monkeypatch.setenv("LITELLM_MIGRATION_DIR", str(blocker / "migrations"))
assert ProxyExtrasDBManager.apply_replica_identity_full_if_requested() is False
def test_reports_an_unrunnable_prisma_cli_without_raising(tmp_path):
"""A deployment without the Prisma CLI on PATH must still finish its
migration run instead of dying on the optional replication step."""
assert (
apply_replica_identity_full(
schema_path=str(tmp_path / "schema.prisma"),
prisma_command=str(tmp_path / "no-such-prisma"),
prisma_env={},
)
is False
)
def test_setup_database_applies_after_a_successful_migration_run(monkeypatch):
applied = []
monkeypatch.setattr(
ProxyExtrasDBManager, "_run_migrations", staticmethod(lambda **kwargs: True)
)
monkeypatch.setattr(
ProxyExtrasDBManager,
"apply_replica_identity_full_if_requested",
staticmethod(lambda: applied.append(True)),
)
assert ProxyExtrasDBManager.setup_database(use_migrate=True) is True
assert applied == [True]
def test_setup_database_skips_replica_identity_when_migrations_fail(monkeypatch):
applied = []
monkeypatch.setattr(
ProxyExtrasDBManager, "_run_migrations", staticmethod(lambda **kwargs: False)
)
monkeypatch.setattr(
ProxyExtrasDBManager,
"apply_replica_identity_full_if_requested",
staticmethod(lambda: applied.append(True)),
)
assert ProxyExtrasDBManager.setup_database(use_migrate=True) is False
assert applied == []