fix(tui): correct PersistentSessionService constructor and return type in TUI LLM dispatch
CI / push-validation (pull_request) Successful in 46s
CI / helm (pull_request) Successful in 51s
CI / build (pull_request) Successful in 1m16s
CI / lint (pull_request) Successful in 1m48s
CI / typecheck (pull_request) Successful in 1m55s
CI / security (pull_request) Successful in 1m56s
CI / quality (pull_request) Successful in 2m3s
CI / integration_tests (pull_request) Failing after 5m43s
CI / unit_tests (pull_request) Failing after 6m54s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s

- Fix robot/helper_tui_llm_dispatch.py: use SessionRepository +
  SessionMessageRepository instead of wrong session_factory kwarg,
  use llm_factory injection instead of # type: ignore monkey-patch
- Fix _build_tui_facade() return type: Any -> A2aLocalFacade | None
- Rebase onto latest master to eliminate scope bloat (50 -> 6 files)

Refs: #11230
This commit is contained in:
2026-05-18 11:40:15 +00:00
parent 4e176c5517
commit cf98a96a32
2 changed files with 85 additions and 80 deletions
+80 -78
View File
@@ -2,9 +2,11 @@
Each subcommand is a self-contained check that prints a sentinel on success.
Uses a FakeListLLM stub and real SQLite DB — no API keys required.
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
@@ -16,49 +18,75 @@ _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,
)
# ---------------------------------------------------------------------------
# Shared stub helpers
# 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_fake_llm() -> Any:
"""Return a FakeListLLM stub with canned responses."""
try:
from langchain_community.llms import FakeListLLM
return FakeListLLM(responses=["Hello from FakeListLLM"] * 20)
except ImportError:
return None
def _make_in_memory_db() -> Any:
"""Return a fresh in-memory SQLAlchemy session factory."""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from cleveragents.infrastructure.database.models import Base
engine = create_engine("sqlite:///:memory:")
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)
return sessionmaker(bind=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_session_service(session_factory: Any) -> Any:
from cleveragents.application.services.session_service import (
PersistentSessionService,
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),
)
return PersistentSessionService(session_factory=session_factory)
def _make_workflow(session_service: Any, fake_llm: Any) -> Any:
from cleveragents.application.services.session_workflow import SessionWorkflow
workflow = SessionWorkflow(session_service=session_service)
if fake_llm is not None:
# Patch _resolve_llm to return the stub so no API keys are needed
workflow._resolve_llm = lambda *_a, **_kw: fake_llm # type: ignore[method-assign]
return workflow
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,
)
# ---------------------------------------------------------------------------
@@ -70,8 +98,8 @@ def cmd_create_session() -> None:
"""Verify _create_tui_session() creates a real DB-backed session."""
from cleveragents.tui import commands as cmd_mod
session_factory = _make_in_memory_db()
service = _make_session_service(session_factory)
_, get_db = _make_db()
service = _make_service(get_db)
container = MagicMock()
container.session_service.return_value = service
@@ -81,7 +109,6 @@ def cmd_create_session() -> None:
assert sid != "default", "Expected a real session ID, got 'default'"
assert len(sid) > 0, "session_id must not be empty"
# Verify the session actually exists in the DB
session_obj = service.get(sid)
assert session_obj is not None, "Session not found in database"
assert session_obj.session_id == sid
@@ -94,8 +121,8 @@ def cmd_build_facade() -> None:
from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.tui import commands as cmd_mod
session_factory = _make_in_memory_db()
service = _make_session_service(session_factory)
_, 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")
@@ -116,52 +143,32 @@ def cmd_build_facade() -> None:
def cmd_dispatch_message() -> None:
"""Verify CleverAgentsTuiApp dispatches normal text and renders response."""
from unittest.mock import MagicMock
"""Verify facade.dispatch(message/send) with stub LLM persists and responds."""
from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.a2a.models import A2aRequest, A2aResponse
from cleveragents.a2a.models import A2aRequest
# Build a real facade backed by in-memory DB + FakeListLLM
session_factory = _make_in_memory_db()
service = _make_session_service(session_factory)
fake_llm = _make_fake_llm()
workflow = _make_workflow(service, fake_llm)
_, get_db = _make_db()
service = _make_service(get_db)
workflow = _make_workflow(service)
facade = A2aLocalFacade(
services={
"session_service": service,
"session_workflow": workflow,
}
services={"session_service": service, "session_workflow": workflow}
)
# Create a real session
session = service.create(actor_name="openai/gpt-4o")
session_id = session.session_id
# Build a minimal mock persona state
persona = MagicMock()
persona.name = "default"
persona.actor = "openai/gpt-4o"
persona.scoped_projects = []
persona.scoped_plans = []
persona_state = MagicMock()
persona_state.active_persona.return_value = persona
persona_state.current_preset.return_value = "balanced"
# We test the facade dispatch directly (no Textual event loop needed)
request = A2aRequest(
method="message/send",
params={"session_id": session_id, "message": "hello integration"},
)
response: A2aResponse = facade.dispatch(request)
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"
# Verify messages persisted in DB
messages = service.get_messages(session_id)
roles = [m["role"] for m in messages]
assert "user" in roles, "User message not persisted"
@@ -171,33 +178,28 @@ def cmd_dispatch_message() -> None:
def cmd_no_actor_error() -> None:
"""Verify that SessionActorNotConfiguredError is handled gracefully."""
"""Verify facade returns error response when no actor is configured."""
from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.a2a.models import A2aRequest
session_factory = _make_in_memory_db()
service = _make_session_service(session_factory)
_, get_db = _make_db()
service = _make_service(get_db)
workflow = SessionWorkflow(session_service=service)
# Create session with NO actor configured
session = service.create(actor_name=None)
session_id = session.session_id
from cleveragents.application.services.session_workflow import SessionWorkflow
workflow = SessionWorkflow(session_service=service, provider_registry=None)
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"},
)
response = facade.dispatch(request)
# Facade should return an error response (not raise) for no-actor
assert response.error is not None, "Expected error response for no-actor session"
print("tui-no-actor-error-ok")
+5 -2
View File
@@ -7,9 +7,12 @@ from collections import defaultdict
from collections.abc import Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from typing import TYPE_CHECKING, Any
from cleveragents.application.container import get_container
if TYPE_CHECKING:
from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.tui.app import CleverAgentsTuiApp, textual_available
from cleveragents.tui.persona.registry import PersonaRegistry
from cleveragents.tui.persona.state import PersonaState
@@ -223,7 +226,7 @@ class TuiCommandRouter:
return f"Import failed: {exc}"
def _build_tui_facade() -> Any:
def _build_tui_facade() -> A2aLocalFacade | None:
"""Build and wire an :class:`~cleveragents.a2a.facade.A2aLocalFacade` for the TUI.
Follows the same pattern as ``_facade_dispatch`` in