litellm/tests/unit/proxy/client/cli/test_codex_settings.py
devin-ai-integration[bot] 39e31958f8
test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy (#43998)
* test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy

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

* test(proxy): stub HIBP through respx by disabling the aiohttp transport

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

* test(proxy): share the httpx transport fixture across proxy unit tests

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

* test(proxy): restore proxy globals without a missing-value sentinel

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

* test(proxy): package moved dirs and stub the login breach check at the HTTP boundary

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

* test(proxy): isolate the mcp server manager per test

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

---------

Co-authored-by: yuneng <yuneng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-01 10:11:45 -07:00

341 lines
16 KiB
Python

import json
import stat
from collections.abc import Callable
from pathlib import Path
from typing import Final
import pytest
import tomlkit
from litellm.litellm_core_utils.private_json import commit_staged_json
from litellm.proxy.client.cli.commands import codex_settings as codex_settings_module
from litellm.proxy.client.cli.commands.agents import (
agent_launch_args,
codex_config_path,
)
from litellm.proxy.client.cli.commands.codex_settings import (
CodexSettingsError,
_snapshot,
_with,
codex_configure_state_path,
configure_codex_settings,
preflight_codex_settings,
unconfigure_codex_settings,
)
GATEWAY: Final = "https://gateway.example.com/team"
KEY: Final = "sk-test-new-gateway-key"
MODEL: Final = "gateway-codex-model"
@pytest.mark.parametrize("path,existing,first_value,second_value", [
("model", 'model = "original" # starting model\n', 'value = "first"\n', 'value = "second"\n'),
("model_providers.litellm", '', '[value]\nname = "first"\n', '[value]\nname = "second"\n'),
("model_providers.litellm", '[model_providers.litellm]\nname = "original" # provider\n',
'[value]\nname = "first"\n', '[value]\nname = "second"\n'),
])
def test_toml_transitions_leave_source_and_independent_results_unchanged(
path: str, existing: str, first_value: str, second_value: str
) -> None:
source: Final = tomlkit.parse('# user settings\n' + existing + '[profiles.work]\nmodel = "keep" # profile\n')
original_bytes: Final = source.as_string().encode()
first: Final = _with(source, path, first_value)
first_bytes: Final = first.as_string().encode()
second: Final = _with(source, path, second_value)
second_bytes: Final = second.as_string().encode()
removed: Final = _with(first, path, None)
first_snapshot: Final = _snapshot(first, path)
second_snapshot: Final = _snapshot(second, path)
assert source.as_string().encode() == original_bytes
assert first.as_string().encode() == first_bytes
assert second.as_string().encode() == second_bytes
assert first_snapshot is not None and tomlkit.parse(first_snapshot) == tomlkit.parse(first_value)
assert second_snapshot is not None and tomlkit.parse(second_snapshot) == tomlkit.parse(second_value)
assert _snapshot(removed, path) is None
for result in (first, second, removed):
assert result["profiles"] == source["profiles"]
assert '# user settings' in result.as_string()
assert '# profile' in result.as_string()
def test_persistent_provider_is_complete_and_preserves_unrelated_toml(tmp_path: Path) -> None:
path: Final = tmp_path / "config.toml"
path.write_text(
'# user settings\nmodel = "old-model" # starting model\n'
'model_provider = "openai"\nprofile = "work"\n'
'[model_providers.litellm]\nname = "old gateway"\n'
'base_url = "https://old.example.com/v1"\nenv_key = "OLD_KEY"\n'
'experimental_bearer_token = "sk-old"\nrequires_openai_auth = true\n'
'[model_providers.litellm.auth]\ncommand = "old-token-helper"\n'
'[model_providers.other]\nname = "Keep me" # other provider\n'
'[profiles.work]\nmodel = "work-model"\n'
'[[hooks.Stop]]\nhooks = [{type = "command", command = "echo done"}]\n'
)
original: Final = tomlkit.parse(path.read_text())
configure_codex_settings(GATEWAY, KEY, MODEL, path)
configured: Final = tomlkit.parse(path.read_text())
assert configured["model"] == MODEL
assert configured["model_provider"] == "litellm"
assert "profile" not in configured
assert configured["model_providers"]["litellm"] == {
"name": "LiteLLM proxy",
"base_url": GATEWAY + "/v1",
"wire_api": "responses",
"supports_websockets": False,
"requires_openai_auth": False,
"http_headers": {"Authorization": "Bearer " + KEY},
}
assert configured["model_providers"]["other"] == original["model_providers"]["other"]
assert configured["profiles"] == original["profiles"]
assert configured["hooks"] == original["hooks"]
assert "# user settings" in path.read_text()
assert "# other provider" in path.read_text()
assert KEY not in codex_configure_state_path(path).read_text()
assert stat.S_IMODE(path.stat().st_mode) == 0o600
assert stat.S_IMODE(codex_configure_state_path(path).stat().st_mode) == 0o600
assert stat.S_IMODE(codex_configure_state_path(path).parent.stat().st_mode) == 0o700
outcome: Final = unconfigure_codex_settings(path)
assert not outcome.kept and not outcome.file_removed
assert tomlkit.parse(path.read_text()) == original
assert "# starting model" in path.read_text()
assert "# other provider" in path.read_text()
assert not codex_configure_state_path(path).exists()
@pytest.mark.parametrize("original", [None, "", "# my preferences\n", '[model_providers]\n'])
def test_undo_distinguishes_missing_empty_and_existing_tables(tmp_path: Path, original: str | None) -> None:
path: Final = tmp_path / "config.toml"
if original is not None:
path.write_text(original)
configure_codex_settings(GATEWAY, KEY, MODEL, path)
outcome: Final = unconfigure_codex_settings(path)
assert outcome.file_removed == (original is None)
assert path.exists() == (original is not None)
if original is not None:
assert tomlkit.parse(path.read_text()) == tomlkit.parse(original)
assert original.strip() in path.read_text()
def test_repeat_setup_preserves_original_and_undo_keeps_user_edits(tmp_path: Path) -> None:
path: Final = tmp_path / "config.toml"
path.write_text('model = "original"\nmodel_provider = "openai"\n')
configure_codex_settings(GATEWAY, KEY, MODEL, path)
configure_codex_settings(GATEWAY + "/second", "sk-second", "second-model", path)
assert tomlkit.parse(path.read_text())["model"] == "second-model"
path.write_text(path.read_text().replace('model = "second-model"', 'model = "my-custom-model"'))
outcome: Final = unconfigure_codex_settings(path)
assert outcome.kept == ("model",)
assert tomlkit.parse(path.read_text()) == {"model": "my-custom-model", "model_provider": "openai"}
def test_repeat_setup_restores_the_user_value_it_displaced(tmp_path: Path) -> None:
path: Final = tmp_path / "config.toml"
path.write_text('model = "original"\n')
configure_codex_settings(GATEWAY, KEY, MODEL, path)
path.write_text(path.read_text().replace('model = "gateway-codex-model"', 'model = "user-edited"'))
configure_codex_settings(GATEWAY, "sk-rotated", "third-model", path)
unconfigure_codex_settings(path)
assert tomlkit.parse(path.read_text()) == {"model": "user-edited"}
def test_undo_keeps_provider_credentials_and_endpoint_together(tmp_path: Path) -> None:
path: Final = tmp_path / "config.toml"
path.write_text('[model_providers.litellm]\nbase_url = "https://old.example.com/v1"\n'
'http_headers = { Authorization = "Bearer old-key" }\n')
configure_codex_settings(GATEWAY, KEY, MODEL, path)
path.write_text(path.read_text().replace(GATEWAY, "https://user.example.com"))
outcome: Final = unconfigure_codex_settings(path)
provider: Final = tomlkit.parse(path.read_text())["model_providers"]["litellm"]
assert outcome.kept == ("model_providers.litellm",)
assert provider["base_url"] == "https://user.example.com/v1"
assert provider["http_headers"] == {"Authorization": "Bearer " + KEY}
assert "old-key" not in path.read_text()
def test_user_deleted_config_is_not_recreated_to_restore_profile(tmp_path: Path) -> None:
path: Final = tmp_path / "config.toml"
path.write_text('profile = "old-profile"\n')
configure_codex_settings(GATEWAY, KEY, MODEL, path)
path.unlink()
outcome: Final = unconfigure_codex_settings(path)
assert outcome.file_removed and outcome.restored == ()
assert not path.exists()
def test_user_comment_in_new_config_survives_undo(tmp_path: Path) -> None:
path: Final = tmp_path / "config.toml"
configure_codex_settings(GATEWAY, KEY, MODEL, path)
path.write_text("# keep my note\n" + path.read_text())
assert not unconfigure_codex_settings(path).file_removed
assert "# keep my note" in path.read_text()
def test_code_home_and_symlink_aliases_share_receipt_and_write_target(tmp_path: Path) -> None:
target: Final = tmp_path / "real-config.toml"
target.write_text('model = "old"\n')
custom_home: Final = tmp_path / "codex-home"
custom_home.mkdir()
alias: Final = codex_config_path({"CODEX_HOME": str(custom_home)})
alias.symlink_to(target)
configure_codex_settings(GATEWAY, KEY, MODEL, alias)
assert alias.is_symlink()
assert codex_configure_state_path(alias) == codex_configure_state_path(target)
assert tomlkit.parse(target.read_text())["model"] == MODEL
unconfigure_codex_settings(target)
assert alias.is_symlink()
assert tomlkit.parse(alias.read_text()) == {"model": "old"}
@pytest.mark.parametrize("invalid", [
'token = "sk-secret\n',
'model_providers = "sk-secret"\n',
'[model_providers]\nlitellm = "sk-secret"\n',
])
def test_invalid_settings_are_unchanged_and_errors_hide_content(tmp_path: Path, invalid: str) -> None:
path: Final = tmp_path / "config.toml"
path.write_text(invalid)
with pytest.raises(CodexSettingsError) as caught:
configure_codex_settings(GATEWAY, KEY, MODEL, path)
assert "sk-secret" not in str(caught.value)
assert path.read_text() == invalid
assert not codex_configure_state_path(path).exists()
def test_invalid_receipt_fails_preflight_before_settings_change(tmp_path: Path) -> None:
path: Final = tmp_path / "config.toml"
path.write_text('model = "keep"\n')
state: Final = codex_configure_state_path(path)
state.parent.mkdir()
state.write_text('{"previous": "sk-secret"}')
with pytest.raises(CodexSettingsError) as caught:
preflight_codex_settings(path)
assert "sk-secret" not in str(caught.value)
assert path.read_text() == 'model = "keep"\n'
@pytest.mark.parametrize("configured_before", [False, True])
@pytest.mark.parametrize("failed_target", ["receipt", "settings"])
def test_failed_commit_restores_receipt_and_cleans_private_staging(
tmp_path: Path, configured_before: bool, failed_target: str
) -> None:
path: Final = tmp_path / "config.toml"
path.write_text('model = "old"\n')
state: Final = codex_configure_state_path(path)
if configured_before:
configure_codex_settings(GATEWAY, KEY, MODEL, path)
before: Final = path.read_bytes()
receipt_before: Final = state.read_bytes() if state.exists() else None
def failing_commit(staged: str, destination: str) -> None:
if destination == str(state if failed_target == "receipt" else path.resolve()):
raise OSError("sk-secret OS error")
commit_staged_json(staged, destination)
with pytest.raises(CodexSettingsError) as caught:
configure_codex_settings(GATEWAY, "sk-replacement", "new-model", path, commit=failing_commit)
assert "sk-secret" not in str(caught.value)
assert path.read_bytes() == before
assert (state.read_bytes() if state.exists() else None) == receipt_before
assert not tuple(tmp_path.rglob(".tmp-*"))
if configured_before:
unconfigure_codex_settings(path)
assert tomlkit.parse(path.read_text()) == {"model": "old"}
def test_failed_undo_keeps_the_settings_and_receipt_for_retry(tmp_path: Path) -> None:
path: Final = tmp_path / "config.toml"
path.write_text('model = "old"\n')
configure_codex_settings(GATEWAY, KEY, MODEL, path)
before: Final = path.read_bytes()
def fail(staged: str, destination: str) -> None:
raise OSError("cannot replace")
with pytest.raises(CodexSettingsError):
unconfigure_codex_settings(path, commit=fail)
assert path.read_bytes() == before
assert codex_configure_state_path(path).exists()
assert not tuple(tmp_path.rglob(".tmp-*"))
unconfigure_codex_settings(path)
assert tomlkit.parse(path.read_text()) == {"model": "old"}
def test_wrapper_and_persistent_provider_agree_except_credential_source(tmp_path: Path) -> None:
path: Final = tmp_path / "config.toml"
configure_codex_settings(GATEWAY, KEY, MODEL, path)
provider: Final = tomlkit.parse(path.read_text())["model_providers"]["litellm"]
args: Final = agent_launch_args("codex", GATEWAY)
overrides: Final = dict(argument.split("=", 1) for argument in args[1::2])
for field in ("name", "base_url", "wire_api", "supports_websockets", "requires_openai_auth"):
assert json.loads(overrides[f"model_providers.litellm.{field}"]) == provider[field]
assert overrides["model_providers.litellm.http_headers"] == "{}"
assert overrides["model_providers.litellm.env_key"] == '"OPENAI_API_KEY"'
@pytest.mark.parametrize("version", [
"codex-cli 0.129.0", "codex-cli 0.129.1", "codex-cli 0.130.0", "codex-cli 1.0.0",
" \ncodex-cli 0.129.0\n",
])
def test_version_guard_accepts_the_fixed_release_and_newer_stable_versions(version: str) -> None:
assert codex_settings_module.require_safe_codex(version=lambda: version) is None
@pytest.mark.parametrize("version", [
None, "", "codex-cli 0.99.0", "codex-cli 0.128.99", "codex-cli 0.129.0-alpha.1",
"codex-cli 1.0.0-beta.1", "0.129.0", "codex-cli 0.129.0 extra", "unparseable-sk-version-secret",
])
def test_version_guard_refuses_missing_unsafe_or_unrecognized_versions(version: str | None) -> None:
with pytest.raises(CodexSettingsError) as caught:
codex_settings_module.require_safe_codex(version=lambda: version)
assert "0.129.0" in str(caught.value)
assert "sk-version-secret" not in str(caught.value)
@pytest.mark.parametrize("output,returncode", [
(None, 0),
("codex-cli 0.129.0\n", 7),
])
def test_version_probe_handles_missing_or_failed_executable(
fake_codex_version: Callable[[str | None, int], Path], output: str | None, returncode: int
) -> None:
fake_codex_version(output, returncode)
assert codex_settings_module._codex_version() is None
@pytest.mark.parametrize("output,returncode", [
(None, 0),
("codex-cli 0.128.0\n", 0),
("codex-cli 0.129.0-alpha.1\n", 0),
("unparseable-sk-version-secret\n", 0),
("codex-cli 0.129.0\n", 7),
])
def test_writer_checks_the_installed_codex_before_replacing_a_key_or_receipt(
tmp_path: Path, fake_codex_version: Callable[[str | None, int], Path],
output: str | None, returncode: int,
) -> None:
path: Final = tmp_path / "config.toml"
path.write_text('model = "original"\n')
configure_codex_settings(GATEWAY, "sk-existing-gateway", MODEL, path)
state: Final = codex_configure_state_path(path)
before: Final = (path.read_bytes(), state.read_bytes())
fake_codex_version(output, returncode)
with pytest.raises(CodexSettingsError) as caught:
configure_codex_settings(GATEWAY, KEY, "replacement-model", path)
assert "0.129.0" in str(caught.value)
assert KEY not in str(caught.value) and "sk-version-secret" not in str(caught.value)
assert (path.read_bytes(), state.read_bytes()) == before
assert not tuple(tmp_path.rglob(".tmp-*"))
def test_undo_does_not_require_codex_to_remain_installed(
tmp_path: Path, fake_codex_version: Callable[[str | None, int], Path]
) -> None:
path: Final = tmp_path / "config.toml"
original: Final = 'model = "original"\n'
path.write_text(original)
configure_codex_settings(GATEWAY, KEY, MODEL, path)
fake_codex_version(None, 0)
outcome: Final = unconfigure_codex_settings(path)
assert outcome.restored and not outcome.kept
assert tomlkit.parse(path.read_text()) == tomlkit.parse(original)
assert not codex_configure_state_path(path).exists()