from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
from witty_service.api import services as services_module
from witty_service.api.services import ServiceContainer
from witty_service.adapter.http_client import AdaptorHttpClient
from witty_service.domain.errors import DomainError
class BackendStub:
pass
def test_service_container_initializes_session_manager() -> None:
repository = MagicMock()
container = ServiceContainer(repository=repository, workspace_store=MagicMock())
assert container.session_manager._repository is repository
def test_get_sandbox_backend_reuses_existing_backend() -> None:
backend = BackendStub()
container = ServiceContainer(
repository=MagicMock(),
workspace_store=MagicMock(),
sandbox_backends={"docker": backend},
)
assert container.get_sandbox_backend("Docker") is backend
def test_get_sandbox_backend_creates_missing_backend(monkeypatch) -> None:
backend = BackendStub()
monkeypatch.setattr(
services_module,
"create_sandbox_backend",
lambda sandbox_type: backend,
)
container = ServiceContainer(repository=MagicMock(), workspace_store=MagicMock())
assert container.get_sandbox_backend("local_process") is backend
assert container.sandbox_backends["local_process"] is backend
def test_get_agent_manager_for_agent_requires_existing_agent() -> None:
repository = MagicMock()
repository.get_agent.return_value = None
container = ServiceContainer(repository=repository, workspace_store=MagicMock())
with pytest.raises(DomainError) as exc_info:
container.get_agent_manager_for_agent("missing")
assert exc_info.value.code == services_module.AGENT_NOT_FOUND
def test_get_agent_manager_for_agent_uses_agent_sandbox(monkeypatch) -> None:
backend = BackendStub()
repository = MagicMock()
repository.get_agent.return_value = SimpleNamespace(sandbox_type="docker")
monkeypatch.setattr(
services_module,
"create_sandbox_backend",
lambda sandbox_type: backend,
)
container = ServiceContainer(repository=repository, workspace_store=MagicMock())
manager = container.get_agent_manager_for_agent("agent-1")
assert manager._repository is repository
assert manager._sandbox_backend is backend
def test_get_insight_http_client_returns_injected_client() -> None:
insight_http_client = MagicMock(spec=AdaptorHttpClient)
container = ServiceContainer(
repository=MagicMock(),
workspace_store=MagicMock(),
insight_http_client=insight_http_client,
)
assert container.get_insight_http_client() is insight_http_client
def test_get_insight_http_client_requires_enabled_insight() -> None:
container = ServiceContainer(repository=MagicMock(), workspace_store=MagicMock())
with pytest.raises(DomainError) as exc_info:
container.get_insight_http_client()
assert exc_info.value.code == "INSIGHT_DISABLED"
def test_get_insight_facade_reuses_single_instance() -> None:
container = ServiceContainer(repository=MagicMock(), workspace_store=MagicMock())
first = container.get_insight_facade()
second = container.get_insight_facade()
assert first is second
def test_ensure_dir_exists_creates_sqlite_parent(tmp_path) -> None:
db_path = tmp_path / "nested" / "witty.db"
services_module._ensure_dir_exists(f"sqlite:///{db_path}")
services_module._ensure_dir_exists("postgresql://example")
assert db_path.parent.exists()
def test_build_default_services_creates_insight_http_client_when_enabled(monkeypatch) -> None:
monkeypatch.setenv("WITTY_INSIGHT_ENABLED", "true")
monkeypatch.setenv("WITTY_INSIGHT_BASE_URL", "http://127.0.0.1:7396")
monkeypatch.setenv("WITTY_INSIGHT_TIMEOUT_SECONDS", "7.5")
monkeypatch.setenv("WITTY_INSIGHT_BEARER_TOKEN", "secret-token")
monkeypatch.setattr(services_module, "create_sqlite_engine", lambda *_args, **_kwargs: MagicMock())
monkeypatch.setattr(services_module, "init_db", lambda *_args, **_kwargs: None)
monkeypatch.setattr(services_module, "create_session_factory", lambda *_args, **_kwargs: MagicMock())
container = services_module.build_default_services()
assert isinstance(container.insight_http_client, AdaptorHttpClient)
assert container.insight_http_client.base_url == "http://127.0.0.1:7396"
assert container.insight_http_client._timeout == 7.5
assert container.insight_http_client._default_headers == {
"Authorization": "Bearer secret-token"
}
@pytest.mark.asyncio
async def test_service_container_close_closes_insight_http_client() -> None:
insight_http_client = MagicMock(spec=AdaptorHttpClient)
insight_http_client.close = AsyncMock()
container = ServiceContainer(
repository=MagicMock(),
workspace_store=MagicMock(),
insight_http_client=insight_http_client,
)
await container.close()
insight_http_client.close.assert_awaited_once_with()
@pytest.mark.asyncio
async def test_service_container_close_is_noop_without_insight_http_client() -> None:
container = ServiceContainer(repository=MagicMock(), workspace_store=MagicMock())
await container.close()