184 lines
6.6 KiB
Python
184 lines
6.6 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]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw_login",
|
|
[
|
|
"roboomp",
|
|
" @roboomp ",
|
|
" @ROBOOMP ",
|
|
"roboomp[bot]",
|
|
"@roboomp[bot]",
|
|
" @ROBOOMP[BOT] ",
|
|
],
|
|
)
|
|
def test_bot_login_normalizes_mention_case_and_app_suffix(
|
|
monkeypatch: pytest.MonkeyPatch, env: dict[str, str], raw_login: str
|
|
) -> None:
|
|
monkeypatch.setenv("ROBOMP_BOT_LOGIN", raw_login)
|
|
reset_settings_cache()
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.bot_login == "roboomp"
|
|
|
|
|
|
def test_maintainer_logins_normalize_csv_entries(
|
|
monkeypatch: pytest.MonkeyPatch, env: dict[str, str]
|
|
) -> None:
|
|
monkeypatch.setenv("ROBOMP_MAINTAINER_LOGINS", " can1357, @ROBOOMP , @Alice[bot] ,, ")
|
|
reset_settings_cache()
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.maintainer_logins == frozenset({"can1357", "roboomp", "alice"})
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("raw_login", "expected"),
|
|
[
|
|
("roboomp", "roboomp"),
|
|
(" @roboomp ", "roboomp"),
|
|
(" @ROBOOMP ", "roboomp"),
|
|
("roboomp[bot]", "roboomp"),
|
|
("@roboomp[bot]", "roboomp"),
|
|
(" @ROBOOMP[BOT] ", "roboomp"),
|
|
],
|
|
)
|
|
def test_maintainer_logins_common_entry_forms(
|
|
monkeypatch: pytest.MonkeyPatch, env: dict[str, str], raw_login: str, expected: str
|
|
) -> None:
|
|
monkeypatch.setenv("ROBOMP_MAINTAINER_LOGINS", raw_login)
|
|
reset_settings_cache()
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.maintainer_logins == frozenset({expected})
|
|
|
|
|
|
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",
|
|
" codex/gpt-5.4 , anthropic/claude-sonnet-4-6 ,, anthropic/claude-opus-4-7 ",
|
|
)
|
|
reset_settings_cache()
|
|
cfg = Settings() # type: ignore[call-arg]
|
|
assert cfg.model_pool == (
|
|
"codex/gpt-5.4",
|
|
"anthropic/claude-sonnet-4-6",
|
|
"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
|