144 lines
4.5 KiB
Python
144 lines
4.5 KiB
Python
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from agentci.config.settings import Settings
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def isolate_agentci_environment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
for name in tuple(os.environ):
|
|
if name.startswith("AGENTCI_"):
|
|
monkeypatch.delenv(name)
|
|
|
|
|
|
def settings(**overrides: object) -> Settings:
|
|
return Settings(_env_file=None, **overrides) # type: ignore[arg-type,call-arg]
|
|
|
|
|
|
def test_reads_values_from_an_isolated_environment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("AGENTCI_INSTALL_SCRIPTS", "python,dotnet")
|
|
monkeypatch.setenv("AGENTCI_MAX_CONCURRENT_JOBS", "7")
|
|
|
|
value = settings()
|
|
|
|
assert value.install_scripts == ["python", "dotnet"]
|
|
assert value.max_concurrent_jobs == 7
|
|
|
|
|
|
@pytest.mark.parametrize("field", ["gitea_url", "opencode_url"])
|
|
def test_normalizes_service_urls(field: str) -> None:
|
|
value = settings(**{field: "https://service.example/base///"})
|
|
|
|
assert getattr(value, field) == "https://service.example/base"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("field", "accepted", "rejected"),
|
|
[
|
|
("plan_review_rounds", (1, 20), (0, 21)),
|
|
("implement_review_rounds", (1, 20), (0, 21)),
|
|
("turn_timeout_seconds", (60,), (59,)),
|
|
("install_script_timeout_seconds", (1,), (0,)),
|
|
("worker_poll_seconds", (0.1,), (0.09,)),
|
|
("max_concurrent_jobs", (1, 32), (0, 33)),
|
|
],
|
|
)
|
|
def test_enforces_documented_numeric_boundaries(
|
|
field: str,
|
|
accepted: tuple[int | float, ...],
|
|
rejected: tuple[int | float, ...],
|
|
) -> None:
|
|
for value in accepted:
|
|
assert getattr(settings(**{field: value}), field) == value
|
|
for value in rejected:
|
|
with pytest.raises(ValidationError):
|
|
settings(**{field: value})
|
|
|
|
|
|
def test_reads_and_strips_secret_files(tmp_path: Path) -> None:
|
|
token_file = tmp_path / "token"
|
|
webhook_file = tmp_path / "webhook"
|
|
password_file = tmp_path / "password"
|
|
token_file.write_text(" gitea-token\n")
|
|
webhook_file.write_text(" webhook-secret \n")
|
|
password_file.write_text("opencode-password\n")
|
|
value = settings(
|
|
gitea_token_file=token_file,
|
|
webhook_secret_file=webhook_file,
|
|
opencode_server_password_file=password_file,
|
|
)
|
|
|
|
assert value.gitea_token == "gitea-token"
|
|
assert value.webhook_secret == b"webhook-secret"
|
|
assert value.opencode_server_password == "opencode-password"
|
|
|
|
|
|
def test_missing_secret_file_has_actionable_error(tmp_path: Path) -> None:
|
|
missing = tmp_path / "missing-token"
|
|
value = settings(gitea_token_file=missing)
|
|
|
|
with pytest.raises(RuntimeError, match="Cannot read Gitea token") as raised:
|
|
_ = value.gitea_token
|
|
|
|
assert str(missing) in str(raised.value)
|
|
|
|
|
|
def test_empty_secret_file_is_rejected(tmp_path: Path) -> None:
|
|
empty = tmp_path / "webhook"
|
|
empty.write_text(" \n")
|
|
value = settings(webhook_secret_file=empty)
|
|
|
|
with pytest.raises(RuntimeError, match="webhook secret file") as raised:
|
|
_ = value.webhook_secret
|
|
|
|
assert str(empty) in str(raised.value)
|
|
|
|
|
|
def test_derived_state_paths_follow_data_directory(tmp_path: Path) -> None:
|
|
data_dir = tmp_path / "state"
|
|
value = settings(data_dir=data_dir)
|
|
|
|
assert value.database_path == data_dir / "agentci.sqlite3"
|
|
assert value.workspaces_dir == data_dir / "workspaces"
|
|
|
|
|
|
@pytest.mark.parametrize("value", ["../script", "tools/setup", "python,python"])
|
|
def test_rejects_unsafe_or_duplicate_install_scripts(value: str) -> None:
|
|
with pytest.raises(ValidationError):
|
|
settings(install_scripts=value)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "expected"),
|
|
[
|
|
(" python, dotnet, company-tools, ", ["python", "dotnet", "company-tools"]),
|
|
("", []),
|
|
(None, []),
|
|
([], []),
|
|
],
|
|
)
|
|
def test_normalizes_install_scripts(value: object, expected: list[str]) -> None:
|
|
assert settings(install_scripts=value).install_scripts == expected
|
|
|
|
|
|
def test_agent_defaults_select_expected_capacity_and_research_models() -> None:
|
|
value = settings()
|
|
|
|
assert value.max_concurrent_jobs == 2
|
|
assert (value.explore_model, value.explore_variant) == (
|
|
"openai/gpt-5.6-luna",
|
|
"low",
|
|
)
|
|
assert value.research_variant == "high"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"field", ["plan_model", "implement_model", "explore_model", "research_model"]
|
|
)
|
|
def test_requires_provider_qualified_opencode_models(field: str) -> None:
|
|
with pytest.raises(ValidationError, match="provider/model"):
|
|
settings(**{field: "model-only"})
|