179 lines
6.4 KiB
Python
179 lines
6.4 KiB
Python
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import pytest
|
|
|
|
import agentci.runtime as runtime_module
|
|
from agentci.config import Settings
|
|
from agentci.runtime import Runtime
|
|
|
|
|
|
class ClosingClient:
|
|
def __init__(self, name: str, events: list[str], error: Exception | None = None) -> None:
|
|
self.name = name
|
|
self.events = events
|
|
self.error = error
|
|
|
|
async def close(self) -> None:
|
|
self.events.append(self.name)
|
|
if self.error is not None:
|
|
raise self.error
|
|
|
|
|
|
def runtime(opencode: object, gitea: object) -> Runtime:
|
|
return Runtime(
|
|
settings=SimpleNamespace(), # type: ignore[arg-type]
|
|
repository=SimpleNamespace(), # type: ignore[arg-type]
|
|
gitea=gitea, # type: ignore[arg-type]
|
|
git=SimpleNamespace(), # type: ignore[arg-type]
|
|
opencode=opencode, # type: ignore[arg-type]
|
|
worker=SimpleNamespace(), # type: ignore[arg-type]
|
|
)
|
|
|
|
|
|
async def test_close_releases_provider_clients_in_order() -> None:
|
|
events: list[str] = []
|
|
value = runtime(ClosingClient("opencode", events), ClosingClient("gitea", events))
|
|
|
|
await value.close()
|
|
|
|
assert events == ["opencode", "gitea"]
|
|
|
|
|
|
async def test_close_still_releases_gitea_when_opencode_close_fails() -> None:
|
|
events: list[str] = []
|
|
value = runtime(
|
|
ClosingClient("opencode", events, RuntimeError("opencode close failed")),
|
|
ClosingClient("gitea", events),
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="opencode close failed"):
|
|
await value.close()
|
|
|
|
assert events == ["opencode", "gitea"]
|
|
|
|
|
|
async def test_close_propagates_gitea_close_failure_after_opencode_closes() -> None:
|
|
events: list[str] = []
|
|
value = runtime(
|
|
ClosingClient("opencode", events),
|
|
ClosingClient("gitea", events, RuntimeError("gitea close failed")),
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="gitea close failed"):
|
|
await value.close()
|
|
|
|
assert events == ["opencode", "gitea"]
|
|
|
|
|
|
async def test_build_runtime_wires_components_without_starting_real_clients(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
token_file = tmp_path / "gitea-token"
|
|
password_file = tmp_path / "opencode-password"
|
|
token_file.write_text("token\n")
|
|
password_file.write_text("password\n")
|
|
settings = Settings(
|
|
_env_file=None, # type: ignore[call-arg]
|
|
data_dir=tmp_path / "data",
|
|
gitea_url="https://gitea.example/",
|
|
gitea_token_file=token_file,
|
|
opencode_url="https://opencode.example/",
|
|
opencode_server_password_file=password_file,
|
|
install_scripts=["python"],
|
|
max_concurrent_jobs=4,
|
|
)
|
|
initialize = AsyncMock()
|
|
repository = SimpleNamespace(initialize=initialize)
|
|
gitea = SimpleNamespace()
|
|
git = SimpleNamespace()
|
|
opencode = SimpleNamespace()
|
|
development = SimpleNamespace()
|
|
prompts = SimpleNamespace()
|
|
services = SimpleNamespace()
|
|
worker = SimpleNamespace()
|
|
repository_constructor = Mock(return_value=repository)
|
|
gitea_constructor = Mock(return_value=gitea)
|
|
git_constructor = Mock(return_value=git)
|
|
opencode_constructor = Mock(return_value=opencode)
|
|
development_constructor = Mock(return_value=development)
|
|
prompt_constructor = Mock(return_value=prompts)
|
|
services_constructor = Mock(return_value=services)
|
|
worker_constructor = Mock(return_value=worker)
|
|
monkeypatch.setattr(runtime_module, "Repository", repository_constructor)
|
|
monkeypatch.setattr(runtime_module, "Gitea", gitea_constructor)
|
|
monkeypatch.setattr(runtime_module, "Git", git_constructor)
|
|
monkeypatch.setattr(runtime_module, "OpenCode", opencode_constructor)
|
|
monkeypatch.setattr(runtime_module, "DevelopmentEnvironment", development_constructor)
|
|
monkeypatch.setattr(runtime_module, "PromptLibrary", prompt_constructor)
|
|
monkeypatch.setattr(runtime_module, "WorkflowServices", services_constructor)
|
|
monkeypatch.setattr(runtime_module, "Worker", worker_constructor)
|
|
|
|
built = await runtime_module.build_runtime(settings)
|
|
|
|
assert runtime_module.__file__ is not None
|
|
package_dir = Path(runtime_module.__file__).parent
|
|
repository_constructor.assert_called_once_with(
|
|
settings.database_path, package_dir / "migrations"
|
|
)
|
|
initialize.assert_awaited_once_with()
|
|
gitea_constructor.assert_called_once_with("https://gitea.example", "token")
|
|
git_constructor.assert_called_once_with(
|
|
gitea_url="https://gitea.example",
|
|
username=settings.bot_username,
|
|
token="token",
|
|
askpass_path=settings.askpass_path,
|
|
commit_name=settings.bot_name,
|
|
commit_email=settings.bot_email,
|
|
)
|
|
opencode_constructor.assert_called_once_with(
|
|
base_url="https://opencode.example",
|
|
username=settings.opencode_server_username,
|
|
password="password",
|
|
schemas_dir=package_dir / "prompts" / "schemas",
|
|
health_directory=settings.workspaces_dir,
|
|
required_models=(
|
|
(settings.plan_model, settings.plan_variant),
|
|
(settings.implement_model, settings.implement_variant),
|
|
(settings.explore_model, settings.explore_variant),
|
|
(settings.research_model, settings.research_variant),
|
|
),
|
|
timeout_seconds=settings.turn_timeout_seconds,
|
|
)
|
|
development_constructor.assert_called_once_with(
|
|
scripts=["python"],
|
|
scripts_dir=settings.install_scripts_dir,
|
|
tools_dir=settings.dev_tools_dir,
|
|
timeout_seconds=settings.install_script_timeout_seconds,
|
|
python_version=settings.python_version,
|
|
dotnet_channel=settings.dotnet_channel,
|
|
)
|
|
prompt_constructor.assert_called_once_with()
|
|
services_constructor.assert_called_once_with(
|
|
settings=settings,
|
|
repository=repository,
|
|
gitea=gitea,
|
|
git=git,
|
|
opencode=opencode,
|
|
prompts=prompts,
|
|
development=development,
|
|
)
|
|
worker_constructor.assert_called_once_with(
|
|
repository=repository,
|
|
gitea=gitea,
|
|
opencode=opencode,
|
|
services=services,
|
|
poll_seconds=settings.worker_poll_seconds,
|
|
max_concurrent_jobs=4,
|
|
workspaces_dir=settings.workspaces_dir,
|
|
bot_username=settings.bot_username,
|
|
)
|
|
assert settings.data_dir.is_dir()
|
|
assert settings.workspaces_dir.is_dir()
|
|
assert built.repository is repository
|
|
assert built.gitea is gitea
|
|
assert built.git is git
|
|
assert built.opencode is opencode
|
|
assert built.worker is worker
|