rewrite phase 1

This commit is contained in:
2026-07-22 23:10:23 +02:00
parent 7527831af6
commit 98ac4abca1
89 changed files with 9179 additions and 2795 deletions
+132 -43
View File
@@ -1,61 +1,150 @@
import os
from pathlib import Path
import pytest
from pydantic import ValidationError
from agentci.config import Settings
def test_parses_comma_delimited_install_scripts() -> None:
settings = Settings(
_env_file=None, # type: ignore[call-arg]
install_scripts=" python, dotnet, company-tools, ",
@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", "boundaries"),
[
("plan_review_rounds", (1, 20)),
("implement_review_rounds", (1, 20)),
("turn_timeout_seconds", (60,)),
("install_script_timeout_seconds", (1,)),
("worker_poll_seconds", (0.1,)),
("max_concurrent_jobs", (1, 32)),
],
)
def test_accepts_documented_numeric_boundaries(
field: str, boundaries: tuple[int | float, ...]
) -> None:
for boundary in boundaries:
assert getattr(settings(**{field: boundary}), field) == boundary
@pytest.mark.parametrize(
("field", "value"),
[
("plan_review_rounds", 0),
("plan_review_rounds", 21),
("implement_review_rounds", 0),
("implement_review_rounds", 21),
("turn_timeout_seconds", 59),
("install_script_timeout_seconds", 0),
("worker_poll_seconds", 0.09),
("max_concurrent_jobs", 0),
("max_concurrent_jobs", 33),
],
)
def test_rejects_values_outside_numeric_boundaries(field: str, value: int | float) -> None:
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 settings.install_scripts == ["python", "dotnet", "company-tools"]
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"
def test_parses_comma_delimited_install_scripts() -> None:
value = settings(install_scripts=" python, dotnet, company-tools, ")
assert value.install_scripts == ["python", "dotnet", "company-tools"]
@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(_env_file=None, install_scripts=value) # type: ignore[call-arg]
settings(install_scripts=value)
def test_empty_install_scripts_disable_setup() -> None:
settings = Settings(
_env_file=None, # type: ignore[call-arg]
install_scripts="",
@pytest.mark.parametrize("value", ["", None, []])
def test_empty_install_scripts_disable_setup(value: object) -> None:
assert settings(install_scripts=value).install_scripts == []
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 settings.install_scripts == []
def test_defaults_research_variant_to_high(monkeypatch) -> None:
monkeypatch.delenv("AGENTCI_RESEARCH_VARIANT", raising=False)
settings = Settings(_env_file=None) # type: ignore[call-arg]
assert settings.research_variant == "high"
def test_defaults_explore_agent_to_luna_low() -> None:
settings = Settings(_env_file=None) # type: ignore[call-arg]
assert settings.explore_model == "openai/gpt-5.6-luna"
assert settings.explore_variant == "low"
def test_defaults_to_two_concurrent_jobs() -> None:
settings = Settings(_env_file=None) # type: ignore[call-arg]
assert settings.max_concurrent_jobs == 2
@pytest.mark.parametrize("value", [0, 33])
def test_rejects_unsafe_job_concurrency(value: int) -> None:
with pytest.raises(ValidationError):
Settings(_env_file=None, max_concurrent_jobs=value) # type: ignore[call-arg]
def test_reads_comma_delimited_install_scripts_from_environment(monkeypatch) -> None:
monkeypatch.setenv("AGENTCI_INSTALL_SCRIPTS", "python,dotnet")
settings = Settings(_env_file=None) # type: ignore[call-arg]
assert settings.install_scripts == ["python", "dotnet"]
assert value.research_variant == "high"
@pytest.mark.parametrize(
@@ -63,4 +152,4 @@ def test_reads_comma_delimited_install_scripts_from_environment(monkeypatch) ->
)
def test_requires_provider_qualified_opencode_models(field: str) -> None:
with pytest.raises(ValidationError, match="provider/model"):
Settings(_env_file=None, **{field: "model-only"}) # type: ignore[call-arg]
settings(**{field: "model-only"})