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
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
192 lines
5.6 KiB
Python
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}"
|