"""Step definitions for tui_app_coverage.feature. These steps target uncovered lines in cleveragents/tui/app.py: - Lines 31-38: Textual import success path (mocked) - Lines 81-100: _TextualCleverAgentsTuiApp class definition + __init__ - Lines 102-112: compose method - Lines 114-121: on_mount method - Lines 123-125: action_help method - Lines 127-129: action_cycle_preset method - Lines 131-142: _refresh_persona_bar method - Lines 144-185: on_input_submitted (all branches) - Line 189: CleverAgentsTuiApp alias """ import importlib import os import shutil import sys import tempfile from pathlib import Path from types import ModuleType, SimpleNamespace from unittest.mock import MagicMock, patch from behave import given, then, when # --------------------------------------------------------------------------- # Mock Textual infrastructure # --------------------------------------------------------------------------- _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 = "" def update(self, text): self._text = text class MockInput: """Minimal Input stand-in for the Textual base class.""" value = "" def __init__(self, *args, **kwargs): self.value = "" mock_textual_app.App = MockApp mock_textual_containers.Vertical = MockVertical mock_textual_widgets.Header = MockHeader mock_textual_widgets.Footer = MockFooter mock_textual_widgets.Static = MockStatic 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) # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("the TUI app module is imported with mocked Textual") def step_import_with_mock_textual(context): """Install mock Textual, reload app module, register cleanup.""" _install_mock_textual(context) context.add_cleanup(lambda: _restore_modules(context)) context.add_cleanup(lambda: _cleanup_tmpdir(context)) # --------------------------------------------------------------------------- # Module-level import gate (lines 31-38) # --------------------------------------------------------------------------- @then("the reloaded app module should have _TEXTUAL_AVAILABLE True") def step_textual_available_true(context): assert context._tui_app_mod._TEXTUAL_AVAILABLE is True @then("the reloaded app module should expose _TextualCleverAgentsTuiApp") def step_has_textual_class(context): assert hasattr(context._tui_app_mod, "_TextualCleverAgentsTuiApp") # --------------------------------------------------------------------------- # textual_available function (lines 43-45) # --------------------------------------------------------------------------- @then("textual_available should return True on the reloaded module") def step_textual_available_returns_true(context): assert context._tui_app_mod.textual_available() is True # --------------------------------------------------------------------------- # SessionView (lines 48-54) # --------------------------------------------------------------------------- @when('I create a SessionView with session_id "{sid}" and transcript entries') def step_create_session_view(context, sid): context._tui_session_view = context._tui_app_mod.SessionView( session_id=sid, transcript=["line1", "line2"] ) @then('the SessionView session_id should be "{sid}"') def step_session_view_id(context, sid): assert context._tui_session_view.session_id == sid @then("the SessionView transcript should contain {count:d} entries") def step_session_view_transcript_count(context, count): assert len(context._tui_session_view.transcript) == count # --------------------------------------------------------------------------- # _CommandRouter protocol (lines 56-61) # --------------------------------------------------------------------------- @when("I create a _CommandRouter implementation") def step_create_command_router_impl(context): class MyRouter: def handle(self, raw, *, session_id): return f"response:{raw}:{session_id}" context._tui_router_impl = MyRouter() @then("calling handle on the implementation should return a response") def step_call_handle(context): result = context._tui_router_impl.handle("test", session_id="s1") assert result == "response:test:s1" # --------------------------------------------------------------------------- # App instantiation (lines 91-100) # --------------------------------------------------------------------------- class _FakeCommandRouter: """Test command router that returns predictable responses.""" def handle(self, raw, *, session_id): return f"handled:{raw}" @given("a mock command router and persona state") def step_create_mock_deps(context): context._tui_cmd_router = _FakeCommandRouter() context._tui_persona_state = _make_persona_state(context) @when("I instantiate the Textual TUI app") def step_instantiate_app(context): AppClass = context._tui_app_mod._ResolvedTuiApp context._tui_app = AppClass( command_router=context._tui_cmd_router, persona_state=context._tui_persona_state, ) @then('the app should have a _session with session_id "{sid}"') def step_app_session_id(context, sid): assert context._tui_app._session.session_id == sid @then("the app should store the command router") def step_app_has_router(context): assert context._tui_app._command_router is context._tui_cmd_router @then("the app should store the persona state") def step_app_has_persona_state(context): assert context._tui_app._persona_state is context._tui_persona_state # --------------------------------------------------------------------------- # Class variables (lines 84-89) # --------------------------------------------------------------------------- @then('the app class should have CSS_PATH set to "{path}"') def step_css_path(context, path): assert path == context._tui_app.CSS_PATH @then("the app class should have {count:d} key bindings") def step_bindings_count(context, count): assert len(context._tui_app.BINDINGS) == count # --------------------------------------------------------------------------- # compose (lines 102-112) # --------------------------------------------------------------------------- @when("I call compose on the app") def step_call_compose(context): context._tui_compose_items = list(context._tui_app.compose()) @then("compose should yield at least {count:d} widgets") def step_compose_count(context, count): assert len(context._tui_compose_items) >= count # --------------------------------------------------------------------------- # on_mount (lines 114-121) # --------------------------------------------------------------------------- @when("I call on_mount on the app") def step_call_on_mount(context): context._tui_app.on_mount() @then("the persona bar should have content set") def step_persona_bar_has_content(context): from cleveragents.tui.widgets.persona_bar import PersonaBar bar = context._tui_app.query_one("#persona-bar", PersonaBar) assert hasattr(bar, "_text") assert bar._text # non-empty @then("the help panel should be hidden on mount") def step_help_panel_hidden_on_mount(context): from cleveragents.tui.widgets.help_panel_overlay import HelpPanelOverlay panel = context._tui_app.query_one("#help-panel", HelpPanelOverlay) assert panel.visible is False assert panel._text == "" @then("the reference picker should have suggestions initialised") def step_ref_picker_initialised(context): from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay) assert hasattr(picker, "_text") @then("the slash overlay should have commands initialised") def step_slash_overlay_initialised(context): from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay) assert hasattr(overlay, "_text") assert overlay._text # non-empty # --------------------------------------------------------------------------- # action_help (lines 123-125) # --------------------------------------------------------------------------- @when("I call action_help on the app") def step_call_action_help(context): context._tui_app.action_help() @when('I set the prompt text to "{text}"') def step_set_prompt_text(context, text): from cleveragents.tui.widgets.prompt import PromptInput prompt = context._tui_app.query_one("#prompt", PromptInput) prompt.value = text @then('the conversation widget should contain "{text}"') def step_conversation_contains(context, text): MockStatic = context._tui_mock_static conv = context._tui_app.query_one("#conversation", MockStatic) assert text in conv._text, f"Expected '{text}' in '{conv._text}'" @then('the help panel text should contain "{text}"') def step_help_panel_contains(context, text): from cleveragents.tui.widgets.help_panel_overlay import HelpPanelOverlay panel = context._tui_app.query_one("#help-panel", HelpPanelOverlay) assert text in panel._text, f"Expected '{text}' in '{panel._text}'" # --------------------------------------------------------------------------- # action_cycle_preset (lines 127-129) # --------------------------------------------------------------------------- @when("I call action_cycle_preset on the app") def step_call_action_cycle_preset(context): context._tui_app.action_cycle_preset() @then("the persona bar content should be refreshed") def step_persona_bar_refreshed(context): from cleveragents.tui.widgets.persona_bar import PersonaBar bar = context._tui_app.query_one("#persona-bar", PersonaBar) assert bar._text # non-empty after refresh # --------------------------------------------------------------------------- # _refresh_persona_bar (lines 131-142) # --------------------------------------------------------------------------- @when("I call _refresh_persona_bar on the app") def step_call_refresh_persona_bar(context): context._tui_app._refresh_persona_bar() @then("the persona bar should display the persona name and scope") def step_persona_bar_shows_name(context): from cleveragents.tui.widgets.persona_bar import PersonaBar bar = context._tui_app.query_one("#persona-bar", PersonaBar) assert "default" in bar._text assert "scope refs" in bar._text # --------------------------------------------------------------------------- # on_input_submitted helpers # --------------------------------------------------------------------------- def _submit_text(context, text): """Set prompt value and fire on_input_submitted.""" from cleveragents.tui.widgets.prompt import PromptInput prompt = context._tui_app.query_one("#prompt", PromptInput) prompt.value = text event = SimpleNamespace() context._tui_app.on_input_submitted(event) # --------------------------------------------------------------------------- # on_input_submitted: empty text (lines 144-150) # --------------------------------------------------------------------------- @when("I submit empty text to the app") def step_submit_empty(context): MockStatic = context._tui_mock_static conv = context._tui_app.query_one("#conversation", MockStatic) context._tui_conv_before = conv._text _submit_text(context, "") @then("the conversation widget should not be updated by input") def step_conv_not_updated(context): MockStatic = context._tui_mock_static conv = context._tui_app.query_one("#conversation", MockStatic) assert conv._text == context._tui_conv_before # --------------------------------------------------------------------------- # on_input_submitted: command mode (lines 152-167) # --------------------------------------------------------------------------- @when('I submit "{text}" to the app') def step_submit_text(context, text): os.environ["CLEVERAGENTS_ALLOW_DANGEROUS_SHELL"] = "1" def restore_env(): os.environ.pop("CLEVERAGENTS_ALLOW_DANGEROUS_SHELL", None) context.add_cleanup(restore_env) _submit_text(context, text) # --------------------------------------------------------------------------- # on_input_submitted: shell returning None (lines 170-171) # --------------------------------------------------------------------------- @when("I submit shell text that produces a None shell result") def step_submit_shell_none(context): from cleveragents.tui.input.modes import InputMode, ModeResult # We patch the InputModeRouter.process to return a shell result with None with patch( "cleveragents.tui.app.InputModeRouter.process", return_value=ModeResult( mode=InputMode.SHELL, expanded_text="!nothing", references=[], shell_result=None, command_result=None, ), ): _submit_text(context, "!nothing") # --------------------------------------------------------------------------- # on_input_submitted: normal text with @ (lines 179-185) # --------------------------------------------------------------------------- @then("the reference picker should have been updated") def step_ref_picker_updated(context): from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay) assert hasattr(picker, "_text") assert picker._text # updated with suggestions # --------------------------------------------------------------------------- # CleverAgentsTuiApp alias (line 189) # --------------------------------------------------------------------------- @then("CleverAgentsTuiApp should be _TextualCleverAgentsTuiApp on the reloaded module") def step_alias_check(context): assert ( context._tui_app_mod.CleverAgentsTuiApp is context._tui_app_mod._ResolvedTuiApp ) assert ( context._tui_app_mod.CleverAgentsTuiApp.__name__ == "_TextualCleverAgentsTuiApp" )