litellm/tests/unit/test_setup_wizard.py
yuneng-jiang f6882246d4
test: move tests/test_litellm root and small trees into tests/unit (#43186)
* ci: run the unit_selection.sh shard files on every event instead of only fork pull requests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* ci: rename fork-flag to unit-flag now that it applies on every event

* test: move tests/test_litellm root and small trees into tests/unit

Pure renames, no content changes. Follow-up commits in this PR fix
references, merge the three files that already existed in tests/unit,
keep live-provider tests in tests/test_litellm and wire CI.

* test: carry tests/test_litellm conftest isolation into tests/unit

Callback lists, routing fallbacks, cached HTTP clients, logger state, AWS,
proxy-URL and keychain env, and session-end client cleanup now reset for
unit tests too. The environment isolation owns its MonkeyPatch so a test's
own monkeypatch is undone before the model-cost teardown runs.

* test: merge, split and prune the moved root and small-tree tests

Merge batches/test_batch_utils.py and the chat_completions and messages
dispatch tests into the files that already existed in tests/unit. Keep
the live Gemini interactions tests, the async image-fetch format test and
the OpenAI embedding scorer test in tests/test_litellm since they need
real network or keys. Put test_router.py under tests/unit/test_router so
the existing package no longer shadows it. Delete eight tests the audit
found superseded by stronger ones kept in this move.

* ci: run the moved root and small-tree tests under their legacy flags

Add the misc and responses-caching-types flags to unit_selection.sh and
CircleCI, extend enterprise-routing and mcp-integration, and point the
legacy GHA shards, Makefile, redis-compat workflow, merge smoke manifest
and change classifier at the new paths.

* test: make the new tests/unit directories packages

tests/unit/test_package_layout.py requires every directory to carry an
__init__.py, and without one the moved and retained
test_litellm_responses_bridge.py modules collide on import.

* test: scope the unit socket block to tests/unit in shared sessions

The GHA shards collect the legacy test-path and the unit selection in one
pytest session. The unit conftest's loopback-only block leaked into legacy
modules that reach the network at import. The legacy conftest now lifts the
restriction at collect and setup time, and the unit conftest re-applies it
when collecting its own modules.

* test: give the shard-script tests their own GITHUB_OUTPUT

They only passed where the runner set it. The CircleCI unit job's env
allowlist drops it, so the script's redirect failed there.

* test: point the router and module-deletion checks at tests/unit

router_code_coverage and code_qa_check_tests only searched tests/test_litellm,
so the moved router tests no longer counted. The two silent-experiment tests
the audit deleted were the only direct callers of those methods; they are
replaced with tests that assert the forwarded shadow request and the
recursion guard.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-25 11:30:43 -07:00

188 lines
5.9 KiB
Python

"""Unit tests for litellm.setup_wizard — pure functions only, no network calls."""
from litellm.setup_wizard import SetupWizard, _yaml_escape
# ---------------------------------------------------------------------------
# _yaml_escape
# ---------------------------------------------------------------------------
def test_yaml_escape_plain():
assert _yaml_escape("sk-abc123") == "sk-abc123"
def test_yaml_escape_double_quote():
assert _yaml_escape('sk-ab"cd') == 'sk-ab\\"cd'
def test_yaml_escape_backslash():
assert _yaml_escape("sk-ab\\cd") == "sk-ab\\\\cd"
def test_yaml_escape_combined():
assert _yaml_escape('ab\\"cd') == 'ab\\\\\\"cd'
def test_yaml_escape_newline():
assert _yaml_escape("sk-abc\ndef") == "sk-abc\\ndef"
def test_yaml_escape_carriage_return():
assert _yaml_escape("sk-abc\rdef") == "sk-abc\\rdef"
def test_yaml_escape_tab():
assert _yaml_escape("sk-abc\tdef") == "sk-abc\\tdef"
# ---------------------------------------------------------------------------
# SetupWizard._build_config
# ---------------------------------------------------------------------------
_OPENAI = {
"id": "openai",
"name": "OpenAI",
"env_key": "OPENAI_API_KEY",
"models": ["gpt-4o", "gpt-4o-mini"],
"test_model": "gpt-4o-mini",
}
_ANTHROPIC = {
"id": "anthropic",
"name": "Anthropic",
"env_key": "ANTHROPIC_API_KEY",
"models": ["claude-opus-4-6"],
"test_model": "claude-haiku-4-5-20251001",
}
_AZURE = {
"id": "azure",
"name": "Azure OpenAI",
"env_key": "AZURE_AI_API_KEY",
"models": [],
"test_model": None,
"needs_api_base": True,
"api_base_hint": "https://<resource>.openai.azure.com/",
"api_version": "2024-07-01-preview",
}
_OLLAMA = {
"id": "ollama",
"name": "Ollama",
"env_key": None,
"models": ["ollama/llama3.2"],
"test_model": None,
"api_base": "http://localhost:11434",
}
def test_build_config_basic_openai():
config = SetupWizard._build_config(
[_OPENAI],
{"OPENAI_API_KEY": "sk-test"},
"sk-master",
)
assert "model_list:" in config
assert "model_name: gpt-4o" in config
assert "model: gpt-4o" in config
assert "api_key: os.environ/OPENAI_API_KEY" in config
assert 'master_key: "sk-master"' in config
def test_build_config_skipped_provider_omitted():
"""Provider with no key in env_vars should not appear in model_list."""
config = SetupWizard._build_config(
[_OPENAI, _ANTHROPIC],
{"ANTHROPIC_API_KEY": "sk-ant-test"}, # OpenAI key missing
"sk-master",
)
assert "gpt-4o" not in config
assert "claude-opus-4-6" in config
def test_build_config_env_vars_written_escaped():
"""API keys with special chars should be YAML-escaped."""
config = SetupWizard._build_config(
[_OPENAI],
{"OPENAI_API_KEY": 'sk-ab"cd'},
"sk-master",
)
assert 'OPENAI_API_KEY: "sk-ab\\"cd"' in config
def test_build_config_master_key_quoted():
"""master_key must be quoted in YAML to handle special characters."""
config = SetupWizard._build_config(
[_OPENAI],
{"OPENAI_API_KEY": "sk-test"},
'sk-master"special',
)
assert 'master_key: "sk-master\\"special"' in config
def test_build_config_does_not_mutate_env_vars():
"""_build_config must not modify the caller's env_vars dict."""
env_vars = {
"AZURE_AI_API_KEY": "az-key",
"_LITELLM_AZURE_AI_API_BASE_AZURE": "https://my.azure.com",
"_LITELLM_AZURE_DEPLOYMENT_AZURE": "my-deployment",
}
original_keys = set(env_vars.keys())
SetupWizard._build_config([_AZURE], env_vars, "sk-master")
assert set(env_vars.keys()) == original_keys
def test_build_config_azure_uses_deployment_name():
env_vars = {
"AZURE_AI_API_KEY": "az-key",
"_LITELLM_AZURE_AI_API_BASE_AZURE": "https://my.azure.com",
"_LITELLM_AZURE_DEPLOYMENT_AZURE": "my-gpt4o",
}
config = SetupWizard._build_config([_AZURE], env_vars, "sk-master")
assert "model: azure/my-gpt4o" in config
assert "model_name: azure-my-gpt4o" in config
# api_base must be quoted to survive YAML special chars
assert 'api_base: "https://my.azure.com"' in config
def test_build_config_azure_no_deployment_skipped():
"""Azure without a deployment name should emit nothing (not fallback to gpt-4o)."""
env_vars = {"AZURE_AI_API_KEY": "az-key"} # no deployment sentinel
config = SetupWizard._build_config([_AZURE], env_vars, "sk-master")
# No azure model entry should be emitted when deployment name is absent
assert "model: azure/" not in config
def test_build_config_no_display_name_collision_openai_and_azure():
"""OpenAI gpt-4o and azure gpt-4o should get distinct model_name values."""
env_vars = {
"OPENAI_API_KEY": "sk-openai",
"AZURE_AI_API_KEY": "az-key",
"_LITELLM_AZURE_DEPLOYMENT_AZURE": "gpt-4o",
}
config = SetupWizard._build_config([_OPENAI, _AZURE], env_vars, "sk-master")
assert "model_name: gpt-4o" in config # OpenAI
assert "model_name: azure-gpt-4o" in config # Azure — qualified
def test_build_config_ollama_no_api_key_line():
"""Ollama has no env_key — config should not contain an api_key line for it."""
config = SetupWizard._build_config([_OLLAMA], {}, "sk-master")
assert "ollama/llama3.2" in config
assert "api_key:" not in config
def test_build_config_master_key_in_general_settings():
"""master_key is written to general_settings."""
config = SetupWizard._build_config([_OPENAI], {"OPENAI_API_KEY": "k"}, "sk-m")
assert 'master_key: "sk-m"' in config
def test_build_config_internal_sentinel_keys_excluded():
"""_LITELLM_ prefixed sentinel keys must not appear in environment_variables."""
env_vars = {
"OPENAI_API_KEY": "sk-real",
"_LITELLM_AZURE_AI_API_BASE_AZURE": "https://x.azure.com",
}
config = SetupWizard._build_config([_OPENAI], env_vars, "sk-master")
assert "_LITELLM_" not in config