from pathlib import Path from types import SimpleNamespace from unittest.mock import AsyncMock, Mock import pytest import agentci.application.runtime as runtime_module from agentci.application.runtime import Runtime from agentci.config.settings import Settings 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] ) @pytest.mark.parametrize( ("opencode_error", "gitea_error", "expected_error"), [ (None, None, None), (RuntimeError("opencode close failed"), None, "opencode close failed"), (None, RuntimeError("gitea close failed"), "gitea close failed"), ], ids=["success", "opencode-failure", "gitea-failure"], ) async def test_close_releases_provider_clients_in_order( opencode_error: Exception | None, gitea_error: Exception | None, expected_error: str | None, ) -> None: events: list[str] = [] value = runtime( ClosingClient("opencode", events, opencode_error), ClosingClient("gitea", events, gitea_error), ) if expected_error is None: await value.close() else: with pytest.raises(RuntimeError, match=expected_error): 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(schemas_dir=tmp_path / "schemas") 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) repository_constructor.assert_called_once_with(settings.database_path) 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=prompts.schemas_dir, 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