"""Helper script for tui_llm_dispatch.robot integration tests. Each subcommand is a self-contained check that prints a sentinel on success. Uses a stub LLM and real SQLite DB — no API keys required. Follows the same fixture pattern as helper_session_tell_llm.py. """ # ruff: noqa: E402 from __future__ import annotations import sys from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch _SRC = str(Path(__file__).resolve().parents[1] / "src") if _SRC not in sys.path: sys.path.insert(0, _SRC) from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from cleveragents.application.services.session_service import PersistentSessionService from cleveragents.application.services.session_workflow import SessionWorkflow from cleveragents.infrastructure.database.models import Base from cleveragents.infrastructure.database.repositories import ( SessionMessageRepository, SessionRepository, ) # --------------------------------------------------------------------------- # Stub LLM — deterministic responses without real API calls # --------------------------------------------------------------------------- _STUB_TEXT = "Hello from the TUI integration stub LLM." class _StubLLM: """Minimal LLM stub compatible with LangChain chat model interface.""" def invoke(self, messages: Any, **kwargs: Any) -> Any: class _Resp: content = _STUB_TEXT def __init__(self) -> None: self.tool_calls: list[Any] = [] self.response_metadata: dict[str, Any] = { "usage": {"input_tokens": 5, "output_tokens": 10} } return _Resp() # --------------------------------------------------------------------------- # Fixture helpers — mirrors helper_session_tell_llm.py pattern # --------------------------------------------------------------------------- def _make_db() -> tuple[Any, Any]: """Create in-memory SQLite DB; return (session_factory, get_db callable).""" engine = create_engine("sqlite:///:memory:", echo=False) Base.metadata.create_all(engine) factory = sessionmaker(bind=engine, expire_on_commit=False) db_session = factory() def get_db() -> Any: return db_session return factory, get_db def _make_service(get_db: Any) -> PersistentSessionService: """Build a PersistentSessionService from repository instances.""" return PersistentSessionService( session_repo=SessionRepository(get_db), message_repo=SessionMessageRepository(get_db), ) def _make_workflow( service: PersistentSessionService, ) -> SessionWorkflow: """Build a SessionWorkflow using the stub LLM via llm_factory injection.""" stub = _StubLLM() return SessionWorkflow( session_service=service, llm_factory=lambda _actor_name: stub, ) # --------------------------------------------------------------------------- # Subcommands # --------------------------------------------------------------------------- def cmd_create_session() -> None: """Verify _create_tui_session() creates a real DB-backed session.""" from cleveragents.tui import commands as cmd_mod _, get_db = _make_db() service = _make_service(get_db) container = MagicMock() container.session_service.return_value = service with patch.object(cmd_mod, "get_container", return_value=container): sid = cmd_mod._create_tui_session() assert sid != "default", "Expected a real session ID, got 'default'" assert len(sid) > 0, "session_id must not be empty" session_obj = service.get(sid) assert session_obj is not None, "Session not found in database" assert session_obj.session_id == sid print("tui-create-session-ok") def cmd_build_facade() -> None: """Verify _build_tui_facade() returns a non-None A2aLocalFacade.""" from cleveragents.a2a.facade import A2aLocalFacade from cleveragents.tui import commands as cmd_mod _, get_db = _make_db() service = _make_service(get_db) container = MagicMock() container.session_service.return_value = service container.plan_lifecycle_service.side_effect = RuntimeError("not needed") container.resource_registry_service.side_effect = RuntimeError("not needed") container.tool_registry.side_effect = RuntimeError("not needed") with ( patch.object(cmd_mod, "get_container", return_value=container), patch("cleveragents.a2a.cli_bootstrap._facade_instance", None), ): facade = cmd_mod._build_tui_facade() assert facade is not None, "_build_tui_facade() must return a facade" assert isinstance(facade, A2aLocalFacade), ( f"Expected A2aLocalFacade, got {type(facade)}" ) print("tui-build-facade-ok") def cmd_dispatch_message() -> None: """Verify facade.dispatch(message/send) with stub LLM persists and responds.""" from cleveragents.a2a.facade import A2aLocalFacade from cleveragents.a2a.models import A2aRequest _, get_db = _make_db() service = _make_service(get_db) workflow = _make_workflow(service) facade = A2aLocalFacade( services={"session_service": service, "session_workflow": workflow} ) session = service.create(actor_name="openai/gpt-4o") session_id = session.session_id request = A2aRequest( method="message/send", params={"session_id": session_id, "message": "hello integration"}, ) response = facade.dispatch(request) assert response.error is None, f"Unexpected error: {response.error}" assert response.result is not None, "Expected result dict" assistant_msg: str = response.result.get("assistant_message", "") assert len(assistant_msg) > 0, "Expected non-empty assistant message" assert _STUB_TEXT in assistant_msg, ( f"Expected stub text {_STUB_TEXT!r} in response, got: {assistant_msg!r}" ) messages = service.get_messages(session_id) # get_messages() returns SessionMessage objects — access .role attribute roles = [m.role.value for m in messages] assert "user" in roles, "User message not persisted" assert "assistant" in roles, "Assistant message not persisted" print("tui-dispatch-message-ok") def cmd_no_actor_error() -> None: """Verify SessionActorNotConfiguredError is raised when no actor configured. The A2A facade re-raises domain exceptions (SessionActorNotConfiguredError, SessionNotFoundError, DatabaseError) so callers can handle them by type. This mirrors the CLI pattern in _facade_dispatch() in session.py. """ from cleveragents.a2a.facade import A2aLocalFacade from cleveragents.a2a.models import A2aRequest from cleveragents.domain.models.core.session import ( SessionActorNotConfiguredError, ) _, get_db = _make_db() service = _make_service(get_db) workflow = SessionWorkflow(session_service=service) facade = A2aLocalFacade( services={"session_service": service, "session_workflow": workflow} ) session = service.create(actor_name=None) session_id = session.session_id request = A2aRequest( method="message/send", params={"session_id": session_id, "message": "hello"}, ) try: facade.dispatch(request) raise AssertionError("Expected SessionActorNotConfiguredError to be raised") except SessionActorNotConfiguredError: pass # expected — facade re-raises domain exceptions print("tui-no-actor-error-ok") def cmd_database_error() -> None: """Verify _run_llm_dispatch returns a friendly message on DatabaseError. Simulates a DatabaseError raised by the facade and confirms the TUI dispatch function converts it to a user-readable error string rather than propagating the exception. """ from unittest.mock import MagicMock from cleveragents.core.exceptions import DatabaseError from cleveragents.tui.app import _run_llm_dispatch facade = MagicMock() facade.dispatch.side_effect = DatabaseError("disk full") result = _run_llm_dispatch(facade, "test-session", "hello") assert "Database error" in result, ( f"Expected 'Database error' in result, got: {result!r}" ) print("tui-database-error-ok") def cmd_session_not_found() -> None: """Verify _run_llm_dispatch returns a friendly message on SessionNotFoundError. Simulates the session being deleted between creation and dispatch (e.g. concurrent process or manual DB edit) and confirms the TUI surfaces a clear error message rather than crashing. """ from unittest.mock import MagicMock from cleveragents.domain.models.core.session import SessionNotFoundError from cleveragents.tui.app import _run_llm_dispatch facade = MagicMock() facade.dispatch.side_effect = SessionNotFoundError("session gone") result = _run_llm_dispatch(facade, "test-session", "hello") assert "Session not found" in result, ( f"Expected 'Session not found' in result, got: {result!r}" ) print("tui-session-not-found-ok") # --------------------------------------------------------------------------- # Dispatch # --------------------------------------------------------------------------- COMMANDS = { "create-session": cmd_create_session, "build-facade": cmd_build_facade, "dispatch-message": cmd_dispatch_message, "no-actor-error": cmd_no_actor_error, "database-error": cmd_database_error, "session-not-found": cmd_session_not_found, } if __name__ == "__main__": if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS: print(f"Usage: {sys.argv[0]} [{' | '.join(COMMANDS)}]", file=sys.stderr) sys.exit(1) COMMANDS[sys.argv[1]]()