Files
cleveragents-core/features/steps/_tui_mock_helpers.py
T
HAL9000 6b2a97ecda
CI / load-versions (pull_request) Successful in 14s
CI / push-validation (pull_request) Successful in 24s
CI / lint (pull_request) Successful in 45s
CI / quality (pull_request) Successful in 58s
CI / security (pull_request) Successful in 1m4s
CI / typecheck (pull_request) Successful in 1m12s
CI / build (pull_request) Successful in 40s
CI / helm (pull_request) Successful in 38s
CI / unit_tests (pull_request) Successful in 4m51s
CI / docker (pull_request) Successful in 1m29s
CI / integration_tests (pull_request) Successful in 10m12s
CI / coverage (pull_request) Successful in 9m40s
CI / status-check (pull_request) Successful in 3s
refactor(tui-tests): extract mock-Textual infrastructure to _tui_mock_helpers.py
Move _MOCK_TEXTUAL_KEYS, _build_mock_textual, _install_mock_textual,
_restore_modules, _make_persona_state, _cleanup_tmpdir, and
_FakeCommandRouter out of tui_app_coverage_steps.py into a shared
_tui_mock_helpers.py module so both step files can import them without
duplication and the coverage steps file stays within the 500-line budget.

ISSUES CLOSED: #6361
2026-06-17 00:21:34 -04:00

192 lines
5.6 KiB
Python

"""Shared TUI mock-Textual infrastructure helpers for step-definition files."""
from __future__ import annotations
import importlib
import shutil
import sys
import tempfile
from pathlib import Path
from types import ModuleType
from typing import Any, cast
from unittest.mock import MagicMock
_MOCK_TEXTUAL_KEYS = [
"textual",
"textual.app",
"textual.containers",
"textual.widgets",
]
def _build_mock_textual():
"""Build mock textual modules that satisfy the app import gate."""
mock_textual = ModuleType("textual")
mock_textual_app = ModuleType("textual.app")
mock_textual_containers = ModuleType("textual.containers")
mock_textual_widgets = ModuleType("textual.widgets")
class MockApp:
"""Minimal App stand-in for the Textual base class."""
def __init__(self, *args, **kwargs):
self._widgets = {}
def query_one(self, selector, widget_type=None):
if selector in self._widgets:
return self._widgets[selector]
if widget_type is not None:
widget = widget_type(id=selector.lstrip("#"))
self._widgets[selector] = widget
return widget
return MagicMock()
class MockVertical:
def __init__(self, *args, **kwargs):
pass
def __enter__(self):
return self
def __exit__(self, *args):
pass
class MockHeader:
def __init__(self, *args, **kwargs):
pass
class MockFooter:
def __init__(self, *args, **kwargs):
pass
class MockStatic:
def __init__(self, *args, **kwargs):
self._text = ""
self.display = False
self._classes: set[str] = set()
def update(self, text):
self._text = text
def add_class(self, name):
self._classes.add(name)
def remove_class(self, name):
self._classes.discard(name)
def has_class(self, name):
return name in self._classes
class MockInput:
"""Minimal Input stand-in for the Textual base class."""
value = ""
def __init__(self, *args, **kwargs):
self.value = ""
self._classes: set[str] = set()
def add_class(self, name):
self._classes.add(name)
def remove_class(self, name):
self._classes.discard(name)
def has_class(self, name):
return name in self._classes
cast(Any, mock_textual_app).App = MockApp
cast(Any, mock_textual_containers).Vertical = MockVertical
cast(Any, mock_textual_widgets).Header = MockHeader
cast(Any, mock_textual_widgets).Footer = MockFooter
cast(Any, mock_textual_widgets).Static = MockStatic
cast(Any, mock_textual_widgets).Input = MockInput
return {
"textual": mock_textual,
"textual.app": mock_textual_app,
"textual.containers": mock_textual_containers,
"textual.widgets": mock_textual_widgets,
}
def _install_mock_textual(context):
"""Inject mock textual into sys.modules and reload the app module."""
mocks = _build_mock_textual()
context._tui_saved_modules = {}
for key in _MOCK_TEXTUAL_KEYS:
context._tui_saved_modules[key] = sys.modules.pop(key, None)
for key, mod in mocks.items():
sys.modules[key] = mod
# Reload widget modules so they pick up the mock Static/Input base class
import cleveragents.tui.widgets.help_panel_overlay as hp_mod
import cleveragents.tui.widgets.persona_bar as pb_mod
import cleveragents.tui.widgets.prompt as prompt_mod
import cleveragents.tui.widgets.reference_picker as rp_mod
import cleveragents.tui.widgets.slash_command_overlay as sco_mod
importlib.reload(hp_mod)
importlib.reload(pb_mod)
importlib.reload(prompt_mod)
importlib.reload(rp_mod)
importlib.reload(sco_mod)
import cleveragents.tui.app as app_mod
importlib.reload(app_mod)
context._tui_app_mod = app_mod
context._tui_mock_static = mocks["textual.widgets"].Static
def _restore_modules(context):
"""Restore original sys.modules and reload the app module."""
for key, val in getattr(context, "_tui_saved_modules", {}).items():
if val is None:
sys.modules.pop(key, None)
else:
sys.modules[key] = val
# Reload widget modules so they pick up the real Static/Input base class again
import cleveragents.tui.widgets.help_panel_overlay as hp_mod
import cleveragents.tui.widgets.persona_bar as pb_mod
import cleveragents.tui.widgets.prompt as prompt_mod
import cleveragents.tui.widgets.reference_picker as rp_mod
import cleveragents.tui.widgets.slash_command_overlay as sco_mod
importlib.reload(hp_mod)
importlib.reload(pb_mod)
importlib.reload(prompt_mod)
importlib.reload(rp_mod)
importlib.reload(sco_mod)
import cleveragents.tui.app as app_mod
importlib.reload(app_mod)
def _make_persona_state(context):
"""Create a real PersonaState backed by a temp directory."""
from cleveragents.tui.persona.registry import PersonaRegistry
from cleveragents.tui.persona.state import PersonaState
tmp = tempfile.mkdtemp()
context._tui_tmpdir = tmp
registry = PersonaRegistry(config_dir=Path(tmp))
registry.ensure_default()
return PersonaState(registry=registry)
def _cleanup_tmpdir(context):
tmp = getattr(context, "_tui_tmpdir", None)
if tmp:
shutil.rmtree(tmp, ignore_errors=True)
class _FakeCommandRouter:
"""Test command router that returns predictable responses."""
def handle(self, raw, *, session_id):
return f"handled:{raw}"