Files
cleveragents-core/features/steps/tui_app_coverage_steps.py
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

477 lines
18 KiB
Python

"""Step definitions for tui_app_coverage.feature targeting cleveragents.tui.app."""
from types import SimpleNamespace
from behave import given, then, when
from features.steps._tui_helpers import _submit_text
from features.steps._tui_mock_helpers import (
_FakeCommandRouter,
_cleanup_tmpdir,
_install_mock_textual,
_make_persona_state,
_restore_modules,
)
# ---------------------------------------------------------------------------
# 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 be hidden on mount")
def step_slash_overlay_hidden_on_mount(context):
from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay
overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay)
assert overlay.visible is False
assert overlay._text == ""
# ---------------------------------------------------------------------------
# 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: 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)
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# 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
# ---------------------------------------------------------------------------
# action_escape cascade (issue #6450)
# ---------------------------------------------------------------------------
@when("I show the actor selection overlay")
def step_show_actor_selection(context):
from cleveragents.tui.widgets.actor_selection_overlay import (
ActorSelectionOverlay,
)
overlay = context._tui_app.query_one("#actor-selection", ActorSelectionOverlay)
overlay.show()
@when('I show the help panel overlay for context "{context_name}"')
def step_show_help_panel_context(context, context_name):
from cleveragents.tui.widgets.help_panel_overlay import HelpPanelOverlay
panel = context._tui_app.query_one("#help-panel", HelpPanelOverlay)
panel.show_context(context_name)
@when('I show the slash overlay for query "{query}"')
def step_show_slash_overlay(context, query):
from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay
overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay)
commands = [SimpleNamespace(command=query, description=f"/{query} command")]
overlay.set_commands(query, commands)
@when('I show the reference picker overlay for query "{query}"')
def step_show_reference_picker(context, query):
from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay
picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay)
picker.set_suggestions(query, [query])
@when("I call action_escape on the app")
def step_call_action_escape(context):
context._tui_app.action_escape()
@then("the actor selection overlay should be hidden")
def step_actor_overlay_hidden(context):
from cleveragents.tui.widgets.actor_selection_overlay import (
ActorSelectionOverlay,
)
overlay = context._tui_app.query_one("#actor-selection", ActorSelectionOverlay)
assert overlay.visible is False
@then("the help panel should remain visible")
def step_help_panel_visible(context):
from cleveragents.tui.widgets.help_panel_overlay import HelpPanelOverlay
panel = context._tui_app.query_one("#help-panel", HelpPanelOverlay)
assert panel.visible is True
assert panel._text
@then("the slash overlay should remain visible")
def step_slash_overlay_visible(context):
from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay
overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay)
assert overlay.visible is True
assert overlay._text
@then("the slash overlay should be hidden")
def step_slash_overlay_hidden(context):
from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay
overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay)
assert overlay.visible is False
assert overlay._text == ""
@then("the reference picker overlay should remain visible")
def step_reference_picker_visible(context):
from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay
picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay)
assert picker.visible is True
assert picker._text
@then("the reference picker overlay should be hidden")
def step_reference_picker_hidden(context):
from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay
picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay)
assert picker.visible is False
assert picker._text == ""
@then('the prompt text should be "{text}"')
def step_prompt_text_equals(context, text):
from cleveragents.tui.widgets.prompt import PromptInput
prompt = context._tui_app.query_one("#prompt", PromptInput)
assert prompt.value == text
# ---------------------------------------------------------------------------
# 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"
)
# ---------------------------------------------------------------------------
# THEME class variable (issue #4742)
# ---------------------------------------------------------------------------
@then('the app class should have THEME set to "{theme}"')
def step_theme_class_var(context, theme: str) -> None:
assert theme == context._tui_app.THEME, (
f"Expected THEME='{theme}', got '{context._tui_app.THEME}'"
)
# ---------------------------------------------------------------------------
# on_input_changed: live overlay update steps
# ---------------------------------------------------------------------------
def _trigger_input_changed(context, text: str) -> None:
"""Set prompt text and fire on_input_changed."""
from cleveragents.tui.widgets.prompt import PromptInput
prompt = context._tui_app.query_one("#prompt", PromptInput)
prompt.text = text
event = SimpleNamespace()
context._tui_app.on_input_changed(event)
@when('I trigger on_input_changed with text "{text}"')
def step_trigger_input_changed(context, text: str) -> None:
_trigger_input_changed(context, text)
@when("I trigger on_input_changed with empty text")
def step_trigger_input_changed_empty_text(context) -> None:
_trigger_input_changed(context, "")
@then('the slash overlay should show only commands matching "{query}"')
def step_slash_overlay_filtered(context, query: str) -> None:
from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay
overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay)
assert overlay._text, "Slash overlay text should not be empty"
# All displayed commands must start with the query
for cmd in overlay._commands:
assert cmd.command.startswith(query), (
f"Command '{cmd.command}' does not start with '{query}'"
)
@then("the reference picker should be reset to empty")
def step_ref_picker_reset(context) -> None:
from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay
picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay)
assert picker._text == "", (
f"Expected reference picker to be empty after reset, got: '{picker._text}'"
)
@then("the slash overlay should be reset to all commands")
def step_slash_overlay_all_commands(context) -> None:
from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay
overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay)
# "reset to all commands" means the overlay returned to its default hidden state
# (set_commands with empty query calls hide(), clearing _text and _commands)
assert not overlay._visible, "Slash overlay should be hidden after reset"
assert overlay._text == "", "Slash overlay text should be empty after reset"