2c773a12a4
git-subtree-dir: python/robomp git-subtree-mainline:4f6e70f779git-subtree-split:553fd1cfcf
135 lines
5.2 KiB
Python
135 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from robomp.config import Settings, reset_settings_cache
|
|
|
|
|
|
def test_settings_load_from_env(env: dict[str, str]) -> None:
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.bot_login == "robomp-bot"
|
|
assert cfg.repo_allowlist == frozenset({"octo/widget"})
|
|
assert cfg.allows("octo/widget")
|
|
assert cfg.allows("Octo/Widget")
|
|
assert not cfg.allows("other/widget")
|
|
|
|
|
|
def test_settings_missing_required(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None:
|
|
"""Empty out every credential source: validator MUST trip the
|
|
'no GitHub access configured' branch. The `env` fixture keeps the other
|
|
required fields satisfied so we isolate the credential-validator path."""
|
|
monkeypatch.setenv("GITHUB_TOKEN", "")
|
|
monkeypatch.setenv("ROBOMP_GH_PROXY_URL", "")
|
|
monkeypatch.setenv("ROBOMP_GH_PROXY_HMAC_KEY", "")
|
|
reset_settings_cache()
|
|
with pytest.raises(ValidationError, match="no GitHub access configured"):
|
|
Settings() # type: ignore[call-arg]
|
|
|
|
|
|
def test_orchestrator_mode_loads_proxy_config(env: dict[str, str]) -> None:
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.github_token is None
|
|
assert cfg.gh_proxy_url == "http://gh-proxy.invalid:8081"
|
|
assert cfg.gh_proxy_hmac_key is not None
|
|
assert cfg.gh_proxy_hmac_key.get_secret_value().startswith("test-hmac-key")
|
|
|
|
|
|
def test_rejects_token_and_proxy_together(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None:
|
|
monkeypatch.setenv("GITHUB_TOKEN", "x")
|
|
reset_settings_cache()
|
|
with pytest.raises(ValidationError):
|
|
Settings() # type: ignore[call-arg]
|
|
|
|
|
|
def test_rejects_proxy_url_without_key(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None:
|
|
monkeypatch.setenv("ROBOMP_GH_PROXY_HMAC_KEY", "")
|
|
reset_settings_cache()
|
|
with pytest.raises(ValidationError):
|
|
Settings() # type: ignore[call-arg]
|
|
|
|
|
|
def test_proxy_mode_loads_pat(proxy_env: dict[str, str]) -> None:
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.github_token is not None
|
|
assert cfg.github_token.get_secret_value() == "ghp_test_token_value_xxxxxxxxxxxxxxxx"
|
|
assert cfg.gh_proxy_url is None
|
|
assert cfg.gh_proxy_hmac_key is None
|
|
|
|
|
|
def test_allowlist_csv_parsing(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None:
|
|
monkeypatch.setenv("ROBOMP_REPO_ALLOWLIST", " alpha/one ,beta/two, ,gamma/three ")
|
|
reset_settings_cache()
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.repo_allowlist == frozenset({"alpha/one", "beta/two", "gamma/three"})
|
|
|
|
|
|
def test_blank_replay_token_treated_as_disabled(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None:
|
|
monkeypatch.setenv("ROBOMP_REPLAY_TOKEN", "")
|
|
reset_settings_cache()
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.replay_token is None
|
|
|
|
|
|
def test_whitespace_replay_token_treated_as_disabled(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None:
|
|
monkeypatch.setenv("ROBOMP_REPLAY_TOKEN", " ")
|
|
reset_settings_cache()
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.replay_token is None
|
|
|
|
|
|
def test_real_replay_token_preserved(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None:
|
|
monkeypatch.setenv("ROBOMP_REPLAY_TOKEN", "abc")
|
|
reset_settings_cache()
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.replay_token is not None
|
|
assert cfg.replay_token.get_secret_value() == "abc"
|
|
|
|
|
|
def test_blank_bot_login_rejected(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None:
|
|
monkeypatch.setenv("ROBOMP_BOT_LOGIN", " ")
|
|
reset_settings_cache()
|
|
with pytest.raises(ValidationError):
|
|
Settings() # type: ignore[call-arg]
|
|
|
|
|
|
def test_model_pool_single(env: dict[str, str]) -> None:
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.model_pool == (cfg.model,)
|
|
assert cfg.pick_model() == cfg.model
|
|
|
|
|
|
def test_model_pool_csv_parses(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None:
|
|
monkeypatch.setenv(
|
|
"ROBOMP_MODEL",
|
|
" p-codex/gpt-5.4 , p-anthropic/claude-sonnet-4-6 ,, p-anthropic/claude-opus-4-7 ",
|
|
)
|
|
reset_settings_cache()
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.model_pool == (
|
|
"p-codex/gpt-5.4",
|
|
"p-anthropic/claude-sonnet-4-6",
|
|
"p-anthropic/claude-opus-4-7",
|
|
)
|
|
|
|
|
|
def test_pick_model_covers_full_pool(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None:
|
|
"""With a 3-item pool and 500 picks, each option appears at least once."""
|
|
monkeypatch.setenv("ROBOMP_MODEL", "a,b,c")
|
|
reset_settings_cache()
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
seen = {cfg.pick_model() for _ in range(500)}
|
|
assert seen == {"a", "b", "c"}
|
|
|
|
|
|
def test_max_concurrency_default_is_8(env: dict[str, str]) -> None:
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.max_concurrency == 8
|
|
|
|
|
|
def test_task_timeout_hard_grace_env_parses(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None:
|
|
monkeypatch.setenv("ROBOMP_TASK_TIMEOUT_HARD_GRACE_SECONDS", "12.5")
|
|
reset_settings_cache()
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.task_timeout_hard_grace_seconds == 12.5
|